git checkout master git pull git checkout my-branch git merge master git push
但现在,由于某种原因,其他人在主干上更改的所有文件都被加入到了我的更改中。所以如果我在合并主干后实际上执行push,则会显示我更改了100多个文件而不仅仅是5个。我做错了什么?谢谢。
这里实际上并没有问题:你只是误解了Git的说法。(当然,Git可能会被误解,但在实践中,无论是Git还是其他版本控制系统,这些东西都很难,需要学习和经验。)
关于Git、文件和提交(commit)有一些关键的知识点:
在与Git交互的层面上,Git存储的是提交。像master
这样的分支名称很有用,但它们实际上只是帮助Git(和你)找到提交。稍后我们将看到它的工作原理。
提交确实存储文件,但通常您会一次处理整个提交。您告诉Git:获取给定标识符X的提交,然后您就会得到该提交的所有文件。您要么拥有该提交——因此拥有了所有文件——要么根本没有该提交,因此也没有任何文件。
每个提交都有一个唯一的ID。这个ID是它的哈希ID,它是一个由随机字母和数字组成的一长串丑陋的字符串,例如9fadedd637b312089337d73c3ed8447e9f0aa775
。一旦存在了这个哈希ID,它就代表了那个提交,而永远不会是任何其他提交。
任何一个提交的内容都是完全、完整、100%只读的。提交中存储的文件以及任何提交的元数据都不能被更改。(原因是哈希ID是提交内容的加密校验和。如果您将提交取出来,修改其中的任何位,然后再放回去,您就得到了一个新的、不同的提交,有着新的、不同的哈希ID。旧提交仍然存在:您只是添加了一个更多的提交。)
每个提交的所有文件的快照仅仅是一个快照。也就是说,提交不会存储更改。
但是当您查看提交时,Git经常会向您显示更改。这是一个技巧!但这也是一件好事,因为通常更有趣。
Git之所以可以将提交显示为更改,是因为大多数提交都存储了单个先前或父提交的原始哈希ID。因此,对于给定的任何一个提交X,Git可以反向一步找到在X之前的提交。那个提交也有一个快照。
Git可以提取父快照和子孙快照,然后进行比较。对于每个文件,“相同”的文件,Git不会提示任何内容,而对于“不同”的文件,Git会向您展示一个文本:“从父快照中开始复制这个文件,添加此行,删除那一行,根据需要重复此过程,完成添加和删除后,您将得到该文件的子孙版本。”
如果您有一系列简单的提交记录,依次排列,您可以将它们绘制出来,或者像这样思考:
... <-F <-G <-H ...
这里,H
代表一个用于找到提交的哈希 ID。提交 H
本身包含其父提交的哈希 ID,我们将其称为 G
。这使得 Git 能够找到 G
。G 包含其父提交的哈希 ID,即 F
,这使得 Git 能够找到 F
,以此类推。
像 master
这样的 分支名称 只是持有链中最后一个提交的哈希 ID。最后一个提交向后指向其父提交,其父提交再向后指,以此类推。因此我们可以将其表示为:
...--F--G--H <-- master
我们实际上不需要将一个提交到下一个提交的连接箭头绘制为“箭头”,因为它们不会改变。任何提交的任何部分永远都不会改变。因此,它们始终指向后面。然而,从分支名称出来的箭头则会发生变化。我们可能会开始使用:
...--G--H <-- master
然后添加一个新的分支名称,以便我们可以在不触及master
的情况下进行新的提交:
...--G--H <-- master, dev
但是最终我们将在分支中添加一个新的提交。让我们将特殊名称HEAD
添加到dev
上,以便记住这是我们正在使用的名称 - 当我们运行git checkout dev
时使用的名称 - 并像这样绘制它:
...--G--H <-- master, dev (HEAD)
I
/
...--G--H <-- master, dev (HEAD)
I
指回H
,因为我们在创建I
时,H
是当前的提交。
现在来看一个巧妙的技巧:Git将I
的哈希ID写入分支名称中。被更改的分支名称是HEAD
所附加的分支名称:dev
。因此,现在dev
指向I
而不是H
:
I <-- dev (HEAD)
/
...--G--H <-- master
没有现有的提交发生变化(毕竟没有)。但是我们的新提交I
现在存在,并指向现有的提交H
,现在我们的名称dev
指向提交I
,这是当前提交。
当我们创建新的提交J
时,Git执行相同的操作,给我们:
I--J <-- dev (HEAD)
/
...--G--H <-- master
现在我们可能会运行 git checkout master
和 git pull
(或 git fetch && git merge
)来获取其他人创建的一些新提交。为了对称起见,我将画出其他人创建的两个提交。这也使得我们的 master
分支上移了超过他们的两个新提交:
I--J <-- dev
/
...--G--H
\
K--L <-- master (HEAD)
<代码>当前分支现在是<代码>master,而<代码>当前提交是<代码>L。你可能会想知道为什么我把它们分别画在一行上:这主要是为了强调提交直到<代码>H都在<代码>两个分支上。
这个奇怪的事实——提交可以同时存在于多个分支上——对Git来说有些特别。 I--J <-- dev (HEAD)
/
...--G--H
\
K--L <-- master
I--J
/ \
...--G--H M <-- dev (HEAD)
\ /
K--L <-- master
M
的第一个父提交是提交 J
,像往常一样将分支 dev
推进了一步。新提交 M
的第二个父提交是提交 L
,仍然是分支 master
的尖端提交。名称 master
不会发生任何变化,也没有任何现有提交发生改变(因为没有提交可以修改),但新的合并提交 M
使得提交 K
和 L
以及通过提交 J
到达的提交都在分支 dev
上。
M
中的快照,然后查看提交 J
和 L
中的两个快照。如果文件 F 中的第42行在提交 M
和 J
中匹配,但在提交 M
和 L
中不同,那么第42行“来自”提交 J
:合并保留了来自提交 J
的行。Git现在将向后回溯一个提交,到提交 I
,查看文件 F 中的第42行是否在提交 I
和 J
中匹配。如果它们在此处不同,则Git将说第42行来自制作提交 I
的人,在他们制作提交 I
的日期。M
和 L
中匹配,并且在提交 J
中不同,那么这意味着合并保留了来自提交 L
的第42行。因此,Git应该向后回溯到提交 L
,然后是 K
,以此类推。M
、L
和 J
中都匹配,那么它可能没有改变过,来自于提交 H
,Git将继续向后移动,一次一个提交地进行追溯,以查看它是否在从 G
到 H
过渡中发生了变化,或者是否来自更早期的更改。git blame
(或 git annotate
)。请注意,就像许多 Git 命令一样,它必须逐步通过提交,在时间上向后移动一步。这些提交,一次一个,是存储库中的历史记录。历史记录是提交;提交就是历史记录。
任何合并的结果都是自动正确的文件。未来的合并会假设你所提交的代码是正确的。如果你去掉了他们的更改,这意味着你认为他们的代码有问题,应该被遗忘。
如果事实确实如此,请移除这些代码 - 但最好在另一个单独的提交中执行此操作,而不是直接在合并中进行。
尽管我们没有在这里完全涵盖它,但Chuck Lu的回答提到了快速合并。 假设我们绘制了一系列提交,如下所示:
...--C--D--E <-- branch1 (HEAD)
\
F--G--H <-- branch2
说明我们当前已经检出了分支branch1
,因此也检出了提交E
。如果我们运行git merge branch2
,Git会发现两个分支上最佳的共同提交是 当前 提交E
。在这种情况下,Git不必进行真正的合并。如果有选择的话,Git将执行一个快进操作,这实际上是通过执行git checkout
提交H
,同时将分支名称branch1
向前拖曳:
...--C--D--E
\
F--G--H <-- branch1 (HEAD), branch2
在绘制图形时,现在没有理由保留对角线;在您自己绘制时请随意删除它。
当Git执行此操作时,还会比较旧提交E
中的快照与新当前提交H
。 对于每个更改的文件,它告诉您有关该更改的一些信息。
您可以通过运行以下命令来查看相同的比较:
git diff --stat <hash-of-E> HEAD
现在HEAD
指向提交H
,这个git diff
比较的是E
快照和H
快照之间的区别,与git pull
做的事情完全相同,因此再次打印相同的信息。
当您进行实际的合并(如我们使用M
合并时),您看到的信息是基于您先前提交的比较(J
)和M
中的提交。由于M
合并了来自分支“两侧”的更改,但J
具有您的更改,所以您看到的是他们的更改。不过,您可以运行git diff --stat master dev
以比较提交L
和提交M
:这时,您将看到合并从分支“您的一侧”带来的内容。
通常很难看出真正合并M
中有什么,因为它有两个父提交。实际上需要两个单独的git diff
命令才能正确查看。如果给git show
命令加上-m
标志,它可以自动执行此操作,但我们在这里不涉及它。
master
更为正确。不同的团队在这里会有不同的工作流程。请注意,因为Git将一个共同的合并基础提交与两个提示提交进行比较,所以无论哪种方式合并的结果几乎相同:区别在于哪个父级是第一个父级,以及哪个分支名称通过合并操作得到推进。 - torekgit log
有一个非常有用的标志:--first-parent
。执行git log --first-parent master
告诉Git仅查看每个合并提交的第一个父提交。如果您维护“将开发合并到主分支,不要将主分支合并到开发分支”的工作流程,则此类git log
会向您显示摘要概述,而不是来自功能的所有单个提交。 - torek--first-parent
技巧就无法正常工作。(您可以选择 变基 而不是合并,但这会变得更加复杂。一些 Git 用户非常反对变基。我与一个赞成在我们认为适当的情况下进行变基的团队一起工作,但是...好吧,在这里我们可以说出什么是“正确”的 :-) )。 - torek有两种git合并方式,fast-forward
和no-fast-forward
。
看起来你遇到了no-fast-forward
类型,这将生成一个新的合并提交。
如果您不想生成合并提交,可以尝试使用git rebase。
git checkout master
git pull
git rebase master my-branch (might encounter conflicts here)
git push
您可以在这里找到有关 rebase 的演示动画demo about rebase here。
git merge
呢? - D Malan