你在这里混合了很多东西。特别是,您使用过git stash
and git revert
,同时还进行各种提交。我们需要把它们分开。
What git stash
does
While git stash
can become very complicated,1 its normal use is fairly simple. You create a stash, which is a variety of commit. This has the side effect of "cleaning" the work-tree and index, a la git reset --hard HEAD
(in fact there's a literal git reset --hard HEAD
command inside git stash
). Then you switch to some other commit, git stash apply
the stash, and if all goes well, git stash drop
the stash.
The apply
步骤将您所做的特殊存储提交转换为补丁,然后像这样应用它git apply -3
会,如有必要,进行三向合并。因此,如果您想用普通的方法来模拟简化的过程git commit
你可以这样做——你不需要git reset --hard
部分原因是您为新提交创建了一个真正的分支:
git checkout -b tempbranch
git add -A
git commit -m 'stash my work-in-progress'
git checkout <something else>
git show tempbranch | git apply -3
要删除提交(就像删除存储),您只需删除临时分支:
git branch -D tempbranch
尽管如此,无论您如何保存并应用该存储,提取存储可能会导致执行三向合并。结果,它可以有一个合并冲突。这就是你遇到的情况。
1The main complication is this: git stash
saves, separately, both the current index and the current work-tree, as two commits (on no branch). It is therefore a lot like running git commit
twice. But, when you use git stash apply
, it combines those two commits into one change in the work-tree, by default.
如果您在进行存储之前没有进行任何操作,那么这种复杂性就变得无关紧要。或者,如果您暂存了所有内容,然后没有更改工作树,那么also使这个并发症变得无关紧要。无论如何,如果你应用隐藏而没有--index
,如果需要,应用步骤将优先考虑工作树更改而不是索引更改。
另一个复杂之处是,如果您添加-u
or -a
选项给你的git stash save
步骤,Git 保存一个third犯罪。这些三提交存储更难应用,应谨慎使用。
提交和补丁
在我们继续讨论合并冲突之前,让我们先简单了解一下 Git 提交中的内容以及补丁(或差异)是什么。
A commit是一个快照。这是您告诉 Git 跟踪的所有文件,以您运行时的形式git commit
。更具体地说,这就是index当时。索引,也称为暂存区域,是您告诉 Git 在下一次提交中保存什么内容的地方,使用git add
更新索引中的项目。
A commit is not a patch. It's not a change: it's a snapshot. However, every2 commit has a parent (or "previous") commit, and Git can easily compare a commit against its parent. This turns the commit into a patch:
- 您保存了 A、B 和 Clast time.
- 您保存了 A、B 和 Dthis time.
- 因此,您删除了 C,并添加了 D。
因此提交可以become一个补丁。一个补丁很简单指示:添加这些东西,删除这些其他东西。您(或 Git)可以将补丁应用到其他提交,然后根据结果进行新的提交。如果应用程序删除 C 并添加 D,并且您进行新提交,则newcommit 的补丁是“删除 C 并添加 D”——这也是old提交的补丁。
将提交变成补丁,然后在其他地方再次播放,重复change.
2At least one commit—the first one you ever make—is special because it has no previous commit, i.e., no parent. In addition, merge commits are special because they have two (or even more) parents. But we're mostly interested in ordinary, one-parent commits here.
Because git stash
进行提交,存储提交也有父母。事实上,这就是 Git 将存储变成补丁的方式,与 Git 将普通提交变成补丁的方式非常相似。
Using git cherry-pick
and git revert
(注意:您只使用了git revert
直接,但我将两者都包含在这里,因为它们非常相似。)
When we turn a commit into a patch, as above, and then apply it to another branch,3 this is a git cherry-pick
operation. We're saying "the change we made last time over there, is a good idea: let's make the same change again, over here." We can make the same change to identical files—this always works—or to files that are merely sufficiently similar, so that we can in fact make the same changes.
“应用补丁”和“制作樱桃选择”之间的本质区别在于,“制作樱桃选择”需要源代码提交,而不仅仅是补丁。默认情况下,git cherry-pick
还会自动从中进行新的提交,因此这也可以重新使用原始提交的提交消息。
一旦您了解了补丁以及如何git cherry-pick
有效,什么git revert
确实变得微不足道:它只是反向应用补丁。你将 Git 指向某个特定的提交并告诉它undo无论该提交做了什么。 Git 将提交变成补丁;补丁上写着“删除C并添加D”;然后 Git 删除 D 并添加 C。git cherry-pick
, git revert
默认情况下,根据结果进行新的提交。
3Technically, we need only apply it to another commit. But this gets into the question of what we mean by "branch": see What exactly do we mean by "branch"? https://stackoverflow.com/q/25068543/1256452
合并冲突
合并背后的想法很简单。我们——无论“我们”是谁——做了一些改变,而他们——无论“他们”是谁——做了一些改变——但我们都started来自same快照。我们要求 Git 结合our变化和their变化。例如,假设你们都以F1
, F2
, and F3
, and you更改的文件F1
但他们没有,而且they更改的文件F2
而你没有。这真的很容易组合:把你修改过的F1
以及他们修改后的F2
. In F3
,您更改了文件顶部附近的一行,而他们更改了底部附近的一行。组合起来有点困难,但仍然不是问题:Git 只接受这两个更改。
但有些更改根本无法合并。这些变化是合并冲突.
合并冲突有多种形式。最常见的情况是“您”(在您的提交中)和“他们”(在他们的提交中)都修改同一文件中的同一行时发生。当我们获取提交并将其变成补丁时(“删除 C 并添加 D”),就会发生这种情况,但他们的更改表示keepC 并添加 E 代替。 Git 不知道要使用哪个更改:我们应该保留 C 并添加 D 和 E,还是删除 C 并仅保留 E,还是什么?
但这不是您所得到的:您遇到了修改/删除冲突。当您这样说时,会发生修改/删除冲突,在文件中train.R
,我们应该删除 C 并添加 D——并且they说“扔掉整个train.R
file".
In all在合并冲突的情况下,Git 会举起双手并暂时放弃进行合并。它将冲突的文件写入您的工作树,声明合并冲突,然后停止。现在你的工作是finish合并——通过提出correct快照——然后git add
and git commit
结果。
Merge conflicts can, of course, occur when you're doing a git merge
(and at this time it's pretty clear which is "ours" and which is "theirs").4 But they can also occur during git apply -3
(the -3
means "use three way merge"), git cherry-pick
, and git revert
. If the patch does not apply cleanly, Git can figure out which common base to use and then figure out the two parts of the changes: what's in the patch you are applying, and what changed from the common base to the commit you are on right now, that you're trying to patch with git cherry-pick
or git revert
.
4The "ours" version is, in all cases, the HEAD
version. This is true during rebase as well, but rebase works by doing a repeated series of cherry-picks, and at this time, HEAD
is the new replacement branch you're building. See this comment https://stackoverflow.com/questions/25576415/what-is-the-precise-meaning-of-ours-and-theirs-in-git#comment39946814_25576672 by CommaToast: "since the head is the seat of the mind, which is the source of identity, which is the source of self, it makes a bit more sense to think of whatever HEAD's pointing to as being ‘mine’ ...".
最后一点关于git commit
我上面提到过你通常git add
文件将它们从工作树复制到索引/暂存区域。在合并冲突期间,这也将文件标记为已解决,这就是您的工作edit文件first,然后使用git add
在上面。一旦你修好并git add
-ed所有文件,你可以git commit
由此产生的合并。这完成了合并,或樱桃选择或恢复或变基步骤或任何您正在做的有冲突的事情。
当你跑步时git commit
像这样,在没有特定命名文件的情况下,Git 提交索引(暂存区域)内容。结果,新HEAD
提交与索引匹配。我们稍后将使用这个事实。
如果你跑git commit <path>
,不过,这个自动地暂存指定文件(就像您运行了git add file
),然后承诺——好吧,某物。这部分有点棘手。默认行为就像您运行一样git commit --only <path>
.
With --only
, Git doesn't获取任何其他已暂存的文件。也就是说,如果你已经git add
编辑了五个文件,没有命名README.txt
,然后你运行git commit README.txt
, Git 使用工作树版本进行新提交README.txt
但是HEAD
其他五个文件的版本。换句话说,它只changes指定的文件,保留所有其他文件的先前版本。
With --include
, Git 暂存命名的<file>
也,并提交整个结果。也就是说,这就像做git add <file>
跑步前git commit
。这比下面的更容易解释--only
行为,但当然如果你想要这个,你可以git add <file>
.
但无论哪种情况,一旦进行新的提交,<file>
已经上演,就好像你有git add
-编辑。所以在我们的--only
例如,有五个文件add
-ed,索引现在已更新所有六个文件。当然,还有一个新的HEAD
提交,并且README.txt
现在匹配新的HEAD
提交,所以只有五个差异 staged.
那么,现在让我们看看您做了什么
您显示的第一个命令是这样的:
$ git commit -m "Model package"
[dev ec5e61d] Model package
40 files changed, 1306 insertions(+), 110 deletions(-)
我们没有看到你的任何git add
s,但您必须添加 40 个文件,以便 Git 说“40 个文件已更改”。但你也必须not have git add
-ed 一些东西,我们稍后会看到。
接下来,您将展示:
$ git push
没有输出。目前尚不清楚这会推动什么(如果有的话)(这取决于您的push.default
配置,可能还有当前分支是否有上游设置,等等)。然而,无论你推送了什么,你自己的本地仓库内容都没有改变,并且后来的证据表明推送成功了,推送commitec5e61d
to origin
.
现在您将显示两个带有一些输出的命令:
$ git log
commit ec5e61d2064a351f59f99480f1bf95927abcd419
Author: Me
Date: Mon Feb 6
Model package
这是您所做的承诺:ec5e61d
,在分支上dev
,如图所示git commit
output.
$ git revert ec5e61d2064a351f59f99480f1bf95927abcd419
error: Your local changes to the following files would be overwritten by merge:
model/R/train.R
Please, commit your changes or stash them before you can merge.
Aborting
这很有趣,因为它表明您的工作树已发生更改model/R/train.R
即使你跑了git commit
最近,使ec5e61d
commit.
这意味着无论在index for model/R/train.R
与工作树中现在的内容不匹配model/R/train.R
。我们无法从中准确判断索引中的内容,只是它与工作树不匹配。
When git revert
said Aborting
,这意味着什么也没做:它发现您的工作树不“干净”,即与HEAD
commit ec5e61d
。默认情况下git revert
要求工作树匹配HEAD
commit.
接下来,你跑了:
$ git stash
Saved working directory and index state WIP on dev: ec5e61d Model package
HEAD is now at ec5e61d Model package
The git stash
命令进行了提交——实际上,有两次提交,一次针对索引,一次针对工作树;但索引与HEAD
提交,所以我们真正需要关心的是工作树提交——然后“清理”工作树以匹配HEAD
commit.
换句话说,stash
提交(或者更确切地说,我们关心的两个提交之一)具有工作树版本model/R/train.R
。它还具有已修改(和跟踪)但尚未提交的任何其他文件的工作树版本,以及仍然与HEAD
or ec5e61d
也提交。 (一如既往,每一次提交都有every文件在其中。这就是“快照”的含义。稍后我们将能够更多地了解隐藏的工作树提交和ec5e61d
犯罪。)
Once git stash
已经隐藏了所有这些,它使用git reset --hard HEAD
放弃索引和工作树更改。 (它们被安全地保存在存储中,因此索引和工作树中不再需要它们。)所以现在您的model/R/train.R
工作树中的文件与HEAD
.
现在你重新运行你的git revert
:
$ git revert ec5e61d2064a351f59f99480f1bf95927abcd419
[dev 062f107] Revert "Model package"
40 files changed, 135 insertions(+), 1331 deletions(-)
这一次,由于您要恢复刚刚提交的更改,因此“反向修补”很容易成功(撤消您刚刚所做的事情很容易)。 Git 进行新的提交,062f107
,在分支上dev
,因此最后两次提交是:
... <- ec5e61d <- 062f107 <-- dev
存储本身附加到提交ec5e61d
(每个存储都直接附加到之前的提交HEAD
在你藏匿的时候)。
现在您尝试应用存储,但是由于合并冲突而失败:
$ git stash apply
CONFLICT (modify/delete): model/R/train.R deleted in Updated upstream
and modified in Stashed changes. Version Stashed changes of
model/R/train.R left in tree.
这一点特别有趣,因为这意味着model/R/train.R
一定是deleted通过恢复提交062f107
,这意味着它一定是added提交中ec5e61d
。同时,存储提交在转换为补丁时表示“将此更改更改为model/R/train.R
."
因为 Git 不知道如何将“完全删除此文件”与“进行此更改”结合起来,所以它留下了整个隐藏的工作树版本model/R/train.R
在工作树中。现在你的工作就是弄清楚什么should在工作树中,并且git add
that.
同时,任何otherGit 的更改应作为补丁应用,Gitdid作为补丁应用于所有其他文件。这些变化are已暂存以进行提交,即这些文件的索引已更新。
接下来,你跑了:
$ git stash apply
model/R/train.R: needs merge
unable to refresh index
这尝试再次应用存储,但不能,所以(幸运的是)它什么也没做。这使得未完成的合并未完成。
然后你跑了:
$ git commit model/R/train.R
[dev ed41d20] Resolve merge conflict
1 file changed, 138 insertions(+)
create mode 100644 model/R/train.R
请注意,这有git commit <file>
形式。这git add
s model/R/train.R
从工作树中,然后跳过其他添加的文件并制作一个新的HEAD
commit ed41d20
从结果来看。所以这个新的提交具有与旧的所有相同的文件HEAD 062f107
除了 model/R/train.R
:
... <- ec5e61d <- 062f107 <- ed41d20 <-- dev
现在你跑了git stash apply
再次:
$ git stash apply
On branch dev
Your branch is ahead of 'origin/dev' by 2 commits.
(use "git push" to publish your local commits)
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: scripts/gm.r
就像上次一样,这会将存储提交转换为补丁并尝试应用该补丁。该补丁无法正确应用,因此 Git 尝试三向合并:找到基本版本,找到 HEAD 中的内容(ed41d20
)版本(“我们的”),区分这些版本,并将不同的版本与补丁结合起来。这个差异结果是完全一样补丁,即补丁已经应用。所以你得到的输出git status
最后,它显示了一个修改过的文件,以及两次提交——当然,这些必须是062f107
and ed41d20
——你有那个origin
还没有。
最后,您展示这些:
$ git stash list
stash@{0}: WIP on dev: ec5e61d Model package
$ git stash show
model/R/train.R | 79 ++++++++++++++++++++++----------------------
scripts/gm.r | 87 ++++++++++++++++++++++++++++++++-----------------
2 files changed, 97 insertions(+), 69 deletions(-)
which finish out our knowledge of what's in the stash: it's just like commit ec5e61d
except for two files, model/R/train.R
and scripts/gm.r
. The git diff
instructions for converting from commit ec5e61d
into the work-tree commit in the stash5 say to delete 69 original lines, and insert 97 replacement lines, in those two files.
就是这样git stash apply
尝试过,只是无法删除any完全删除的行model/R/train.R
,所以它只是把整个保存的工作树stash@{0}
文件版本model/R/train.R
在工作树中。
5Again, earlier, we proved that the index commit in the stash matches the ec5e61d
commit exactly. Thus, we can ignore the index commit, and concentrate only on the work-tree commit.
接下来做什么
你什么should此时,做什么取决于你想要什么结果。我无法给你正确答案,但我can给你重要的问题:
- What commits你想拥有吗?您希望在每次提交中获得哪些源代码树快照?
- 其他人是否已经有提交的副本
ec5e61d
? (即,其他人是否有权访问您将提交推送到的上游存储库ec5e61d
?)
Commit ec5e61d
两者都存在your存储库,以及您将其推送到的存储库git push
命令。在另一个存储库中,名称dev
可能指向ec5e61d
。它有文件model/R/train.R
与其父提交相比,作为一个新文件,以及对其他 39 个文件进行了额外更改或新创建。
Commit 062f107
仅存在于您的存储库中。这是完全的提交撤销ec5e61d
,因此,它可能没有用,除非您需要确保完全退出ec5e61d
来自任何其他存储库。
Commit ed41d20
也仅存在于您的存储库中。它是其中所有内容的副本parent of ec5e61d
除了它有隐藏版本model/R/train.R
added.
您当前的工作树大部分匹配ed41d20
除了它有scripts/gm.r
修改,无论补丁中发生了什么变化ec5e61d
和存储提交,也在工作树中以相同的方式更改。并且,自从ed41d20
大多数情况下与父项匹配ec5e61d
,父级之间的差异ec5e61d
工作树相当于你改变的任何东西scripts/gm.r
在藏匿处,加上藏匿的版本model/R/train.R
.
你还拥有藏匿的东西。如果有用,请保留它,直到不再有用为止。一旦确定它不再有用,请使用git stash drop
删除它(这一步是极其很难撤消,因此请务必确保已完成)。
您可能很想放弃最近的两次提交(恢复以及仅添加一个文件版本的提交)model/R/train.R
)完全。在这种情况下你可以git reset --hard origin/dev
放弃这两个提交并将工作树恢复到提交时的状态ec5e61d
.
如果您是上游存储库的唯一用户(这是一个非常大的“如果”),并提交ec5e61d
本身并没有多大用处,你可能想丢弃that也提交 - 但要小心,不仅因为非常大的“如果”部分,而且还因为特定的提交变得更难恢复 - 尽管不像存储那么难。只要您仍然拥有藏匿、恢复和git stash apply
结果很容易重复:您只需运行相同的操作即可git
命令。