写点什么

交互式教程!带你深入理解 Git 原理

  • 2021-07-31
  • 本文字数:18022 字

    阅读完需:约 59 分钟

交互式教程!带你深入理解 Git 原理

综述


如下图所示,有四个模块。这个独立的我们称之为 远程仓库,其他靠在一起的三个我们称之为 开发环境



从独立的这个模块开始说,远程仓库 是你推送你的改动并且希望与他人共享的地方,同样你也可以从中获取到他人的改动。如果你使用过其他的版本控制系统,那这个概念并没有什么新鲜的。


开发环境 是在你本地计算机上的。它的三个部分指的是你的 工作目录 、暂存区 和 本地仓库 。在开始使用 Git 前,我们将先了解有关这些概念的知识。


请选择一个要放置你的 开发环境 的位置。可以转到你的主目录,或者你想放你的项目的任何地方,不需要为你的开发环境创建新文件夹。


创建一个远程仓库


现在我们要创建一个 远程仓库 并将仓库的内容导入你的计算机。


我建议使用这个 Git 仓库:(https://github.com/UnseenWizzard/git_training.git).


这里可以使用这条命令拉取仓库代码:git clone https://github.com/UnseenWizzard/git_training.git

接下来的操作需要你将本地开发环境的改动提交到远程仓库,由于这个示例仓库不允许他人操作,因此建议你将这个仓库 fork 到你个人的 github 下,这个 fork 按钮位于页面的右上角.


现在,在你自己的 github 中已经有了这个_远程仓库_的副本,然后就可以将它克隆到你本地开发环境中了。


我们可以使用这条命令:git clone https://github.com/{YOUR USERNAME}/git_training.git


如下图所示,这个远程仓库已经被拷贝到了两个位置,即你的 工作目录 和 本地仓库 。


现在你可以看到 git 是如何进行 分布式 版本控制的。本地仓库 是 远程仓库 的副本,其行为与之类似,唯一的区别是你没有与任何人分享。


git clone 还在你执行该命令的目录下创建了一个新的文件夹. 这里对应的应该是 git_training,打开它。



添加一些新内容


有人已经把Alice.txt添加进了 远程仓库 。她看上去有点孤单,所以让我们创建一个Bob.txt去陪陪她。


这一步就是将文件添加到你的 工作目录 中。


现在在你的工作目录中有两种文件,一种是 git 知道的 跟踪文件 ,另一种是 git 不知道的 未跟踪文件 。

要查看你的 工作目录 中发生了什么,请运行git status,它将告诉你当前所处的分支、你的 本地仓库 是否与 远程仓库 不同,以及 跟踪 和 未跟踪 文件的状态。


你会看到Bob.txt是未跟踪的,此外git status 还会告诉你如何更改它。


在下面的图片中,你可以看到当你根据建议执行git add Bob.txt时会发生什么:你已将该文件添加到 暂存区 ,该区域用于存放你希望放入仓库的所有更改。



当你添加了所有的更改(现在只添加了 Bob)后,就可以将刚才所做的操作提交到 本地仓库 。

你提交的改动具有一定的意义,因此当你执行git commit时,文本编辑器将打开,并允许你编写一条消息,告诉他人你刚才所做改动的意义。当你保存并关闭时,你的 提交 将被添加到 本地仓库 。



同样你也可以直接在命令行中编辑你的 commit 信息,比如:git commit -m "Add Bob"。如果你想写出更规范化的 commit 信息,你可以花时间看看 good commit messages。


现在,你的更改已经提交到了本地仓库中,只要没有其他人需要这些更改,或者你还没有准备共享它们,就可以将这些更改保存在本地仓库中。


如果需要通过 远程仓库 共享你提交的改动,你需要push它们。



一旦运行git push,你的改动将被推送到   远程仓库 ,push之后的状态如下图所示。



修改内容


到目前为止,我们只是添加了一个新文件。显然,版本控制中更有趣的部分是更改文件。


Alice.txt中包含了一些文本,但是 Bob.txt是空的,所以让我们在Bob.txt中加上Hi!! I'm Bob. I'm new here.


如果你现在执行 git status, 你会看到 Bob.txt 已经更新了。


此时这些改动仅仅位于你的 工作目录 中。


如果你想看看你的 工作目录 中发生了什么更改,运行git diff,便可以看到如下内容:


diff --git a/Bob.txt b/Bob.txtindex e69de29..3ed0e1b 100644--- a/Bob.txt+++ b/Bob.txt@@ -0,0 +1 @@+Hi!! I'm Bob. I'm new here.
复制代码


像之前那样继续执行 git add Bob.txt 。这些改动则被移到了 暂存区 。


如果想看看刚刚发生的改动,再次执行git diff !你会注意到这次控制台输出是空的。这是因为git diff只会展示你的 工作目录 中的更改。


为了显示哪些更改已经被暂存,执行git diff --staged,我们将看到与之前类似的输出。


我刚注意到我们在"Hi"后面加了两个感叹号“!!”。我不喜欢这样,让我们再次修改Bob.txt文件将它改成'Hi!'。


如果现在运行 git status,我们将看到有两个更改:一个是我们已经 暂存 的,另一个是我们刚刚做的,它还只保存在 工作目录 中。


我们可以执行git diff来查看一下 工作目录 和 暂存区 在我们修改后都发生了什么变化。


diff --git a/Bob.txt b/Bob.txtindex 8eb57c4..3ed0e1b 100644--- a/Bob.txt+++ b/Bob.txt@@ -1 +1 @@-Hi!! I'm Bob. I'm new here.+Hi! I'm Bob. I'm new here.
复制代码


由于这是我们想要的改动,因此这里执行git add Bob.txt 去暂存这个文件当前的状态。


现在我们可以提交刚刚的改动了。这里我选择直接命令行执行 git commit -m "Add text to Bob"。对这种小的变化,写一行 commit 信息就够了。


如我们所知,这些改动现在是在 本地仓库 中。我们可能还想知道我们刚刚的提交,以及之前在 本地仓库 提交过的改动。


我们可以通过比较这些 commit 来做到这一点。git 中的每个 commit 都有一个唯一的哈希值作为他的引用。


如果我们执行一下 git log,我们不仅可以看到所有提交的列表,包括它们的 hash、Author 和 Date,还可以看到本地仓库的状态和远程分支的最新本地信息。


执行git log后结果如下所示:


commit 87a4ad48d55e5280aa608cd79e8bce5e13f318dc (HEAD -> master)Author: {YOU} <{YOUR EMAIL}>Date:   Sun Jan 27 14:02:48 2019 +0100
Add text to Bob
commit 8af2ff2a8f7c51e2e52402ecb7332aec39ed540e (origin/master, origin/HEAD)Author: {YOU} <{YOUR EMAIL}>Date: Sun Jan 27 13:35:41 2019 +0100
Add Bob
commit 71a6a9b299b21e68f9b0c61247379432a0b6007cAuthor: UnseenWizzard <nicola.riedmann@live.de>Date: Fri Jan 25 20:06:57 2019 +0100
Add Alice
commit ddb869a0c154f6798f0caae567074aecdfa58c46Author: Nico Riedmann <UnseenWizzard@users.noreply.github.com>Date: Fri Jan 25 19:25:23 2019 +0100
Add Tutorial Text
Changes to the tutorial are all squashed into this commit on master, to keep the log free of clutter that distracts from the tutorial
See the tutorial_wip branch for the actual commit history
复制代码


这里我们看到一些有趣的事情:


  • 前两次提交是我做的。

  • 在远程仓库中,你最初添加 Bob 的提交是在 HEAD 指针指向的 master 分支上。在讨论分支和获取远程更改时,我们将再次讨论这个问题。

  • 本地仓库中最新的提交是我们刚刚做的,现在我们知道了它的哈希值。


请注意,你实际的 commit 的哈希值可能与之不同。如果你想知道 git 具体是如何精确制定这些 IDs 的,请阅读 this interesting article.


要比较这个提交和之前的一个提交,可以执行 git diff <commit>^! (这里的^! 会告诉 git 去和它之前的一个提交比较)。在这里我们执行 git diff 87a4ad48d55e5280aa608cd79e8bce5e13f318dc^!.


我们也可以执行 git diff 8af2ff2a8f7c51e2e52402ecb7332aec39ed540e 87a4ad48d55e5280aa608cd79e8bce5e13f318dc 去比较任意两个 commit 提交。请注意这里的格式是git diff <from commit> <to commit>, 也就是说我们新的提交是后面这个。


在下面的图表中,你会再次看到在不同 git 命令操作时,当前文件的的不同状态。



既然我们确定这些改动是符合预期的,那么请继续执行git push吧。


分支


git 中分支的应用是不可或缺的一部分,这同时也是 git 之所以强大的原因之一。


其实从一开始,我们就已经在使用分支了。


当你将远程仓库clone到你的开发环境时,它已经自动创建了一个主分支:master


当你将改动 合并 到 master 之前,git 的大多数工作流程都是在一个分支上进行更改。


通常你都会在自己的分支上工作,直到你完成工作并确认无误后,再将这些更改合并到 master 中。


许多 git 仓库管理器如 GitLab 和 GitHub 还支持将分支保护起来, 即不允许任何人在此分支上提交改动。通常 master  分支是默认受保护的。


别担心,当我们需要这些内容时,我们再更详细地讨论。


当你只想自己尝试一些事情,而不是修改 master 分支的状态,或者是你无权修改 master 时,你可以自己创建一个分支并对其进行一些更改。


分支是位于 本地仓库 和 远程仓库 中的。当你创建新分支时,分支的内容将是当前正在处理的分支的提交状态的副本。


让我们在 Alice.txt做一些改动! 不如在第二行放些文字怎么样?


我们希望共享这个改动,但不希望提交到 master ,因此可执行命令git branch <branch name> 来创建一个分支。


创建一个名为 change_alice的分支,你可以执行git branch change_alice


这条命令会在你的 本地仓库 下添加一个新分支。


虽然你的 工作目录 和 暂存区 并不真正关心分支,但其实你的commit操作总是将改动提交到当前所在的分支。


你可以将 git 中的 分支 视作很多指针,他们指向一系列的提交。当 执行commit时,会添加到你当前指针指向的地方。


只是添加一个分支,并不能直接把你带到那里,它只是创建一个这样的指针。


实际上,你当前所处的 本地仓库 可以看作另一个指针,称为 HEAD,它指向你当前所在的分支和提交。

如果这听起来很复杂,下面的图表将有助于理清一些思绪:



要切换到我们的新分支,你必须使用git checkout change_alice。这只需将 HEAD 移动到你所指定的分支。


通常你都希望在创建分支后立即切换到该分支,因此checkout 命令有一个方便的-b 选项,它允许你直接checkout一个新的分支而不必事先创建它。

因此,创建并切换一个 change_alice 分支, 我们可以执行 git checkout -b change_alice.



你将发现,你的 工作目录 没有改变, 实际上我们 修改 Alice.txt 与当前分支无关。现在你可以像之前在 master 分支上那样,执行 add 及 commit去提交我们在Alice.txt上的改动,这将会暂存并最终将改动提交到change_alice分支上。


现在只有一件事你还没有做,就是去执行git push将你的改动提交到远程仓库。


你将会看到下面的错误日志以及解决这个问题的建议:


fatal: The current branch change_alice has no upstream branch.To push the current branch and set the remote as upstream, use(当前分支change_alice没有上游分支,要推送当前分支的改动并设置远端作为上游分支,可以使用如下命令行:)
git push --set-upstream origin change_alice
复制代码


看到这里,我们不想根据提示盲目照做。我们是来了解实际情况的,那么什么是 上游分支 和 远端分支?


还记得之前我们 cloned远程仓库吗?当时仓库内容不仅包含本教程和 Alice.txt文件 ,实际上还有两个分支。


一个是我们刚开始在上面操作的 master ,还有一个我命名为"tutorial_wip"的分支,在这个分支上,我提交了本教程所做的所有更改。


当我们将远程仓库中的内容复制到你的开发环境中时,还进行了一些额外的操作,如下:


Git 将本地仓库的远程仓库设置为克隆的远程仓库,默认名称为 origin


你的本地仓库可以跟踪多个远程,它们可以有不同的名称,但本教程中我们使用的是origin 。


然后它将两个远程分支复制到你的本地仓库,最后为你切换到了 master。


这时候另一个隐含的步骤发生了。当你切换 一个与远程分支完全匹配的分支名称时,你将获得一个新的链接到远程分支的本地分支。这个远程分支就是你这个本地分支的 上游分支 。


在前面的图表中,你可以看到你拥有的本地分支。你可以通过运行git branch查看本地分支的列表。

如果还想查看你本地仓库知道的远程分支,可以使用 git branch -a 列出所有分支。



现在我们可以执行之前命令窗口建议的git push --set-upstream origin change_alice命令了,将分支上的更改 push 到一个新的远程节点上。这将在远程仓库上创建一个 change_alice分支,并会将我们的本地的 change_alice分支跟踪该新的远程分支。


如果我们希望本地分支跟踪远程仓库上已经存在一个分支,还有另一个选择。也许一位同事已经推动了一些改动,而我们本地分支也解决了一些问题,我们希望将两者结合起来。那么,我们就可以通过使用git branch --set-upstream-to=origin/change_alice,将change_alice分支的上游分支设置为一个新的远程分支,然后跟踪这个远程分支。


在这之后,在 github 上查看一下你的 远程仓库 ,你的分支已经在那里了,可以供其他人查看和使用。

接下来,我们将了解如何将其他人的改动引入到你的开发环境中,但首先我们需要更多地练习使用分支,学习从远程仓库获取改动也会用到的一些概念。


合并


由于你和其他人通常都在各自分支上工作,所以我们需要讨论如何通过合并这些分支来将更改从一个分支转移到另一个分支。


我们刚刚在change_alice 分支上改变了 Alice.txt, 我对我们做的改动很满意.


如果你执行git checkout master, 那么我们在另一个分支上的commit 将不存在。要将这些改动更新到 master 中,我们需要将change_alice分支合并进 master.


请注意,合并总是会将一个特定的分支合并到你当前所在的分支。


快进合并


如果我们已经执行checked out切换到了 master, 那便可以合并分支了:git merge change_alice


Alice.txt文件中没有其他的冲突改动,并且我们在 master 上也没有任何改动,这便称之为快进合并。


在下面的图表中,你可以看到快进合并意味着 master 的指针简单地移动到change_alice所在的位置。


第一张图显示了merge之前的状态,master 仍然处于最初的提交状态,而在另一个分支上,我们又进行了一次提交。



第二张图显示了 merge之后的状态。



合并冲突分支


让我们试试更复杂的东西。


在 master 分支上给 Bob.txt添加一些文本并提交它。


然后执行git checkout change_alice切换分支修改Alice.txt文件并提交。


在下面的图表中,你可以看到我们的提交历史。master 和 change_alice 都源于同一个 commit,但从那时起,它们就分开了,每个分支都有自己的附加提交。



如果现在切换回 master(git checkout master)并执行git merge change_alice 则无法进行快速合并。此时,文本编辑器将打开并需要你更改merge commit的消息,以便将两个分支重新合并在一起,这里你可以使用默认消息。下面的图表显示了我们在 merge之后 git 历史的状态。



这个新提交将我们在change_alice分支上所做的更改引入到了 master 中。


正如你以前所记得的,git 中的修订不仅是文件的快照,还包含文件的来源信息。每个 commit都有一个或多个父提交。我们新的 commit提交,既有来自_master_ 的最后一次提交,也有我们在另一个分支上作为其父级所做的提交。


解决冲突


到目前为止,我们的改动还没有相互干扰。


让我们制造一个 冲突 然后 解决 它吧。


创建并切换到一个新的分支,这个你已经知道怎么做了,也可以简单地执行git checkout -b,这里我们将新分支命名为bobby_branch


基于这个分支,我们在Bob.txt文件上做一些改动。现在第一行应该还是Hi!! I'm Bob. I'm new here.,我们将它改成 Hi!! I'm Bobby. I'm new here.


保留并提交你的改动之后,切换到 master 分支。我们修改同样的这行内容为 Hi!! I'm Bob. I've been here for a while now.,然后提交这个改动。


现在再将你的新分支合到 master 分支上。


当你尝试这么做的时候,你会看到下面这样的提示:


Auto-merging Bob.txtCONFLICT (content): Merge conflict in Bob.txtAutomatic merge failed; fix conflicts and then commit the result.(自动合并Bob.txt发生冲突(内容):Bob.txt文件冲突自动合并失败;修复冲突然后提交结果。)
复制代码


两个分支都修改了同样的一行内容,git 无法自动处理这样的改动。


如果你执行git status ,你将会得到如何继续下一步的一些有帮助的指令。


首先我们必须手动解决冲突。


对于像这样简单的冲突来说,你的文本编辑器就可以做到。但对于有大量文件的改动来说,一个功能强大的编辑工具会让你的工作变的更简单,我建议你的编辑器最好带有版本控制工具和很好的合并视图窗口。


如果你在编辑器中打开文件,你会看到类似下面这样的内容(这里我只截取了之前在第二行中添加的内容):


<<<<<<< HEADHi! I'm Bob. I've been here for a while now.=======Hi! I'm Bobby. I'm new here.>>>>>>> bobby_branch[... whatever you've put on line 2]
复制代码


在最上面你可以看到 Bob.txt 在当前 HEAD 上的改动,下面是我们合并进的分支中发生的改动。


要手动解决冲突,你只需要确保最终保留你所需的内容,并且删掉 git 在文件中引入的特殊行。

所以继续把文件改成这样:


Hi! I'm Bobby. I've been here for a while now.[...]
复制代码


这里所做的改动是我们所需要的,因此执行add Bob.txt暂存这个文件,再 commit提交.


我们已经知道这次提交是为解决冲突所做的。这就是在合并提交时经常出现的 merge commit 。


如果你在解决冲突的过程中意识到你实际上不想使用merge,你可以通过运行 git merge --abort来 中止 它。


变基


Git 有另一种清晰的方法来集成两个分支之间的更改,称为 rebase变基。


我们仍然记得,一个分支总是基于另一个分支。当你创建它的时候,你就从某个地方检出了一个分支。

在刚刚简单合并的示例中,我们在一个特定的 commit 节点从 master 检出了一个分支,然后在 master 和 change_alice 分支上都提交了一些更改。


当一个分支与它所基于的分支发生分歧,并且你希望将最新的更改重新集成到当前分支中时, rebase 提供了一种比 merge 更简洁的方法。


正如我们所看到的,merge 引入了一个 merge commit ,其中两个历史记录再次集成在一起。

简单地说,变基只会改变分支所基于的历史点(commit 节点)。


为此,让我们再次切换到 master 分支,然后基于它创建/签出一个新分支。


我们给它取名 add_patrick,在这个分支上加了一个新的文件Patrick.txt然后提交,并附上提交信息:'Add Patrick'。


当你在这个分支添加提交后,切回到 master ,同样进行更改并提交。这里我在Alice.txt文件里添加了一些文本。


就像在我们的合并示例中,这两个分支的历史在一个共同的祖先处发生了分歧,如下图所示。



现在让我再次执行checkout add_patrick, 并将在 master 上所做的更改添加到我们正在处理的分支中!


当我们执行 git rebase master, 我们将add_patrick分支修改为基于当前 master 分支的状态。

命令行的输出提示很好的告诉了我们发生了什么:


First, rewinding head to replay your work on top of it...(首先,倒回头部,重新将你的工作放置在他的开头...)Applying: Add Patrick
复制代码


我们还记得,在我们的开发环境中, HEAD 指向的是当前提交的指针。


在变基开始之前,它和add_patrick 指向的是同一个地方。变基的过程中,它首先移动回共同的祖先处,然后移动到我们想要重新建立的分支的头部。


因此, HEAD 从 0cfc1d2 的提交移动到位于 master 头部的 7639f4b 提交处。然后变基会将我们在 add_patrick 分支上所做的每一次提交都应用到该分支中。


更确切地说, git 在 HEAD 移回分支的共同祖先之后,会存储你在分支上所做的每一次提交(包括不同的改动,以及提交文本、作者等)


之后,它会切换到你基于它创建的新分支,然后将存储的所有改动在你的新分支上作一个新的提交。

因此,在我们最初的简化视图中,可以假设在 rebase 之后, 0cfc1d2 提交不再指向历史上的共同祖先,而是指向了 master 的头部。


事实上,0cfc1d2 提交已经不存在了,add_patrick 分支以一个新的 0ccaba8 提交开始,它的祖先是 master 的最新提交。


它看起来,就像我们的 add_patrick 是基于当前的 master,而不是它的旧版本,其实在这里我们重新书写了分支的历史。


在本教程的最后,我们将学习更多关于重写历史的知识,以及什么时候适合这样做,什么时候不适合。



当你基于一个共享的分支,如 master 检出一个自己的开发分支,在上面工作时,Rebase 是一个非常强大的工具。


使用Rebase,有助于你经常整合其他人所做的更改并将其推送到 master ,同时保持一个清晰的线性历史记录,以便在需要将你的改动作放入共享分支时进行 fast-forward merge 快进合并。


保持线性的历史记录在查看提交日志时比使用 merge commits 混合提交(通常只使用默认文本)更有用。(可以尝试 git log --graph 或查看 GitHub or GitLab 的分支视图)


解决冲突


在使用merge合并分支时,如果两个提交更改了文件的同一个部分,则可能会发生冲突。


但是,当你在rebase 时遇到冲突时,你不会在额外的 merge commit 中修复它,而是在当前所在的提交中简单地解决它。


同样地,直接根据原始分支的当前状态进行更改。


实际上,rebase 解决冲突与 merge的方法非常相似,如果你不记得怎么做了,请回顾该部分内容。

唯一的区别是,由于你没有引入 merge commit (合并提交),因此没有必要提交你的解决方案。只需使用add 将改动添加到暂存区,然后执行 git rebase --continue,冲突将在刚刚的提交中解决。


在合并时,当你执行 git rebase --abort之前,你始终可以停止并放弃到目前为止所做的一切。


根据远程仓库更新本地开发环境


到目前为止,我们只学会了如何创建和分享我们的改动。


你刚刚做的所有操作通常也是其他人会做的,因此我们需要知道怎么从远程仓库获取到他们提交的改动。


过了这么久,让我们再来回顾一下 git 的组件:



就像你的开发环境一样,其他人也都有他们的开发环境。



所有这些开发环境中保留着他们自己的本地改动和暂存的改动,这些更改在某个时刻提交到他们的本地仓库,最后被推送到远程。


在下面的例子中,我们将使用 GitHub 提供的在线工具来模拟其他人在我们工作时对 remote (远端)进行的更改。


进入你通过 fork 拷贝了此资源的 github.com 地址并且打开Alice.txt 文件。


找到编辑按钮,进行更改,并直接通过网站提交。



在这个库中,我已在一个名为fetching_changes_sample的分支上将远程更改添加到了Alice.txt ,但是在你的仓库中,你可以直接在 master分支上更改文件。


获取改动


我们还记得,当你使用 git push时,你会将本地仓库的更改同步到远程仓库中。


要把 远程 所做的更改保存到本地仓库中,请使用 git fetch


这会将远程服务器上的任何更改(包括提交和分支)保存到你的本地仓库中。


注意,此时,更改还没有集成到本地分支中,因此也还没有集成到你的 工作目录 和 暂存区 中。



如果现在运行 git status ,你将看到另一个很好的 git 命令示例,它告诉你到底发生了什么:


> git statusOn branch fetching_changes_sampleYour branch is behind 'origin/fetching_changes_sample' by 1 commit, and can be fast-forwarded.  (use "git pull" to update your local branch)
在fetching_changes_sample分支上,你的分支落后于远程的'origin/fetching_changes_sample'一个提交,可以被快进合并(使用“git pull”命令去更新你的本地分支)
复制代码


拉取改动


在我们的 工作目录 或 暂存区 没有改动时,我们可以执行 git pull 将来自远程的更改拉取到我们的工作区。


Pulling 拉取操作也会隐式地fetch拉取 远程仓库,但有时单独执行fetch是个好主意。

例如,当你要同步任何新的远程分支时,或者你希望你在 origin/master分支上执行git rebase 之前确保你的本地仓库是最新的时候。



在我们执行 pull之前,让我们改动一个本地文件看看会发生什么。


这里我们同样在本地工作区修改 Alice.txt 文件。


如果你现在试着执行 git pull,你会看到下面这样的报错:


> git pullUpdating df3ad1d..418e6f0error: Your local changes to the following files would be overwritten by merge:        Alice.txtPlease commit your changes or stash them before you merge.(错误:被跟踪的本地文件alice.txt在合并时会被重写,请在合并提交(commit)或存储(stash)你的改动)Aborting
复制代码


你无法拉取任何改动,因为在你执行 pull 操作时,你的本地工作区间文件的改动会被这次提交重写。


此时其中一个方法是,在你最终提交这些本地改动前,将他们添加到一个储藏区,这里有一个好用的工具: git stash


存储改动


如果某些时候,你有不想放入 commit 中的本地改动,或者在尝试以不同的角度解决问题时希望将其存储在某个位置,则可以使用 stash将这些更改储藏起来。


git stash 储藏的基本上是你在 工作目录 中做的一些更改。


你可以使用命令 git stash将工作目录的任何修改放入储藏区,然后使用命令git stash pop 将最近储藏的改动取出并再次应用在你的 工作目录 上。


正如 stash 命令这个堆栈名字一样,git stash pop 命令会在再次应用之前删除最新存储的改动。


如果你希望保留储藏的改动,你可以使用git stash apply,这样在你应用更改之前便不会将其从堆栈中删除。


要检查当前已有的存储,你可以使用 git stash list 列出所有储藏的条目,使用 git stash show 显示储藏的最新的更改。


另一个很便捷的命令是 git stash branch {BRANCH NAME},它会从保存更改时的 HEAD 处创建一个分支,并将你准备储藏的改动应用在这个分支上。


现在我们知道了 git stash的作用,让我们运行它来删除本地工作目录中 Alice.txt的改动,这样我们就可以继续使用git pull 从远程获取更改了。


然后,再执行git stash pop 把本地的改动取回。


因为我们 pull 的远程仓库和储藏的更改都修改了 Alice.txt 文件,所以这里需要解决冲突,就像前面 merge 或rebase提到的那样的做法来解决,然后通过 addcommit提交这次改动就好了。


拉取有冲突的内容


现在我们已经理解了怎么通过 fetch 和 pull去拉取远程改动到我们的开发环境,接下来让我们去制造一些冲突。


不用推送你改动的Alice.txt文件,直接回到你的 github.com 上的远程仓库。


这里我们继续修改Alice.txt文件然后提交这次改动。


现在,实际上在我们的本地和远程仓库已经有两了个冲突。


不要忘了执行git fetch来查看远程修改,而不是直接执行pull


如果你现在执行git status 你会看到,这两个分支上都有一处与另一个分支不同的改动


> git statusOn branch fetching_changes_sampleYour branch and 'origin/fetching_changes_sample' have diverged,and have 1 and 1 different commits each, respectively.  (use "git pull" to merge the remote branch into yours)分支fetching_changes_sample与origin/fetching_changes_sample发生分歧,各自都有一次提交。(使用git pull 将远程的分支合并进你的分支)
复制代码


此外,我们在两个分支上都对同一个文件做了修改,从而制造出了这个需要我们解决的合并冲突。

当你执行git pull,本地和远程仓库会发生冲突,这和之前合并两个分支时出现的情况一样。


由此,你可以将远程仓库和本地仓库出现的冲突当成是基于一个分支创建了另一个分支,这两个分支出现冲突。


本地分支是基于你上次 fetched 拉取远程分支时的状态。


从这个角度看,从获取远程改动要做的这两个选择是很有意义的:


当你执行git pull时,本地和远程分支会被合并。就像合并分支一样,这里会创建一个合并的 commit 提交。


因为任何一个本地分支都是基于其各自的远程版本,所以我们也可以对它进行rebase变基,这样我们在本地所做的任何更改都会看起来像是基于远程仓库的最新改动。


为此,我们可以执行 git pull --rebase (或者简写成 git pull -r)。


正如变基一节中阐述的,保持一个清晰的历史记录很重要,这就是为什么我建议无论何时你执行git pull,最好选择执行git pull -r


你也可以告诉 git 在使用git pull用 rebase而不是merge 作为它的默认方式,通过pull.rebase指令如git config --global pull.rebase true可完成设置。


如果你在我前几段第一次提到获取更新时已经执行了 git pull,那么现在可以运行git pull -r来获取远程更改,这样我们新的提交看起来像是在拉取了远程之后才更新的。


当然,就像使用rebase(或 merge)一样,你也需要在 git pull前解决引入的冲突。


拣选


恭喜你!你已经完成了更高级的功能!现在你已经了解了如何使用所有典型的 git 命令,以及它们是如何工作的。希望这有助于你理解下面要说的概念。接下来,让我们开始学习如何拣选我们的提交吧!


回顾前面的章节内容,你应该还大致记得什么是 commit 提交,对吧?


以及当你rebase一个分支时,你的提交是怎么带着同样的改动和信息作为一个新的 commit 提交的?


无论何时,只要你想从一个分支获取一些更改并将它们应用到另一个分支,你都需要cherry-pick拣选这些提交并将它们放到你的分支上。


git cherry-pick 允许你对单个提交或一系列提交执行操作。


就像 rebase 的过程一样,这实际上会将这些提交的更改放入当前分支的新提交中。


让我们看一下有一个及多个提交时,执行cherry-pick的示例。


下图显示了我们做任何操作之前的三个分支。假设我们想从add_patrick分支获取一些更改到 change_alice分支。遗憾的是,它们都还没有合进 master,所以我们不能仅仅通过 rebase来获得这些更改(我们可能也不需要其他分支的其余的改动)。



因此让我们执行 git cherry-pick 拣选 63fc421 的这次提交. 下图清晰地显示了运行 git cherry-pick 63fc421时发生的事情:



如你所见,分支上出现了一个新的带着我们需要改动的 commit 提交。


这时请注意,就像我们之前所见的合并分支改动一样,在执行下一步命令前,我们需要解决执行cherry-pick时出现的任何冲突。

解决完冲突后,和之前一样,你可以继续执行拣选cherry-pick,或者决定直接--abort中止该命令。


下图显示了使用拣选(cherry-pick)选择一系列提交而不是单个提交。你只需调用形式为git cherry-pick <from>..<to>的命令,或者如下面的示例调用git cherry-pick 0cfc1d2..41fbfa7



改写历史


我现在再问一遍,你还记得rebase 吗?不记得的话可以跳回去快速浏览一遍,因为下面学习如何改写历史需要使用到这部分知识哦!


你已经知道了一个基础的 commit 提交包含了你的改动、提交信息和一些其他的内容。


所谓分支的'历史' 就是由所有提交组成的。


但是假设你刚刚提交了一个commit,然后发现你忘了添加一个文件,或者你犯了一个错误,而这个改变给你留下了一个坏的代码,怎么办呢?


这种情况下,我们将简要介绍两种解决方式,使它看起来像从未发生过一样。


让我们执行git checkout -b rewrite_history,切换到一个新的分支。


现在让我们在 Alice.txt 和 Bob.txt文件中做一些改动,然后执行 git add Alice.txt。然后带上信息"This is history" 提交(git commit)。


等等,我刚刚做了什么?你应该知道我刚刚做错什么了吧!


  • 我忘了添加Bob.txt文件。

  • 我也没有提交一个友好的 commit 信息。(友好的书写 commit 可见:good commit message)


修改最后一次提交


一次性解决这两个问题的办法是是修改(amend )我们刚刚提交的 commit。


修改最新的提交基本上就像创建一个新的提交一样。


在我们做任何事情之前,先看看你最近的提交(git show {COMMIT})。COMMIT 中输入 commit 的 hash 值(hash 值可以通过调用命令行 git commitgit log看到),或者直接使用 HEAD 。


就像git log打印出的结果一样,你会看到消息、作者、日期,当然还有改动。


现在让我们修正我们在那次提交中所做的改动。


执行git add Bob.txt将改动添加到暂存区, 然后执行 git commit --amend.


接下来你会看到最新的提交被展开,新的改动被添加到暂存区已有的改动中。commit 提交消息的编辑器被打开。


在编辑器中,你将看到前面的提交消息。


你可以把它修改成更友好的提交信息。


做完这些之后,执行git show HEAD看一下最新的提交。


正如你预期的那样,提交的哈希改变了。原来的提交不见了,取而代之的是一个新的提交,它包含了组合后的更改和新的提交消息。


请注意其他提交数据(如 author 和 date)与原始提交相比是保持不变的。如果需要,你也可以在修改时使用--author={AUTHOR} 和--date={DATE} 来修改。


祝贺你!你第一次成功地改写了历史!


交互式变基


通常,当我们执行git rebase时,我们变基到一个分支上。当我们执行git rebase origin/master之类的操作时,实际是将一个指针重新定位到到该分支的 HEAD 指针上。


事实上只要我们喜欢,我们可以 rebase变基到任何的 commit 上


请记住,提交包含了之前的历史记录


和许多其他命令一样,git rebase有一个“交互式”模式。


与其他大多数命令不同的是,交互式的 rebase可能是你经常使用的东西,因为它允许你随意改变历史。


尤其是如果你的更改有许多的小提交,这样一旦你犯了错误,你可以很容易地跳回原位,那么交互式的 rebase 将是你最亲密的盟友。


话不多说!让我们做点什么吧!


切换回你的 master 分支,然后执行git checkout 检出一个新分支。


像之前一样,我们会在Alice.txt和 Bob.txt文件上做一些更改,然后添加git add Alice.txt

然后我们执行 git commit来提交它,提交信息可以是"Add text to Alice"


现在不改变那次提交,我们继续执行git add Bob.txtgit commit,这里我用的提交信息是"Add Bob.txt"


为了让事情变得更有趣,我们在Alice.txt文件上做一些改动,然后提交:执行git add 和git commit,带上提交信息"Add more text to Alice"。


如果我们现在执行git log查看一下分支历史(或者也可以用git log --oneline快速的查看一下),我们将看到在 master 上的三个提交。


我这边看起来是这样的:


> git log --oneline0b22064 (HEAD -> interactiveRebase) Add more text to Alice062ef13 Add Bob.txt9e06fca Add text to Alicedf3ad1d (origin/master, origin/HEAD, master) Add Alice800a947 Add Tutorial Text
复制代码


我们想做下面两件事情,为了学习不同的内容,这将有点不同于上一节的amend


  • Alice.txt的两次改动作为一次 commit 提交

  • 保持修改 bob 文件那次 commit 信息中 bob 的名字,将Bob.txt后面的 .txt 信息去掉。


要更改这三个新提交,我们需要重新定位到它们之前的提交。对我来说,这个提交 hash 值是df3ad1d,但是我们也可以引用当前 HEAD 的往前第三次提交:HEAD~3


要启动一个交互式的rebase,我们使用git rebase -i {COMMIT}, 这里执行的是 git rebase -i HEAD~3


你将看到你选择的编辑器显示如下内容:


pick 9e06fca Add text to Alicepick 062ef13 Add Bob.txtpick 0b22064 Add more text to Alice
# Rebase df3ad1d..0b22064 onto df3ad1d (3 commands)## Commands:# p, pick = use commit# r, reword = use commit, but edit the commit message# e, edit = use commit, but stop for amending# s, squash = use commit, but meld into previous commit# f, fixup = like "squash", but discard this commit's log message# x, exec = run command (the rest of the line) using shell# d, drop = remove commit## These lines can be re-ordered; they are executed from top to bottom.## If you remove a line here THAT COMMIT WILL BE LOST.## However, if you remove everything, the rebase will be aborted.## Note that empty commits are commented out
复制代码


每次调用命令时请留意,git 有关于你如何执行这些操作的说明。


最常用的命令是reword、 squash 和 drop。(还有pick,默认情况下是这些。)


花点时间想想你看到了什么,以及我们将用什么实现我们的两个目标,等你思考一下。

有计划了吗?完美!


在开始进行更改之前,请注意这样一个事实,提交是从最旧到最新的顺序列出的,因此 git log 输出的方向相反。


我将从简单的更改开始进行,因此我们先来修改中间这次提交的提交信息。


pick 9e06fca Add text to Alicereword 062ef13 Add Bob.txtpick 0b22064 Add more text to Alice
# Rebase df3ad1d..0b22064 onto df3ad1d (3 commands)[...]
复制代码


现在把 Alice.txt的两次改动改为一次 commit 提交。


显然,我们要做的是将最后一个提交 压缩 到第一个中,所以让我们pick那次提交后,用squash命令更改 Alice.txt的提交。在我这个例子中 hash 是 0b22064 。


pick 9e06fca Add text to Alicereword 062ef13 Add Bob.txtsquash 0b22064 Add more text to Alice
# Rebase df3ad1d..0b22064 onto df3ad1d (3 commands)[...]
复制代码


结束了吗?这能满足我们的要求吗?

不会的,对吧?文件中的说明告诉我们:


# s, squash = use commit, but meld into previous commit
复制代码


所以我们到目前为止所做的,将会合并第二次 Alice 提交和 Bob 提交的更改。这不是我们想要的。

交互式rebase另一个强大的功能就是改变提交的顺序。


如果你仔细阅读了评论告诉你的内容,你已经知道怎么做了:简单地移动行!


谢天谢地,你使用了你最喜欢的文本编辑器,所以请继续将第二个 Alice commit 移到第一个之后。


pick 9e06fca Add text to Alicesquash 0b22064 Add more text to Alicereword 062ef13 Add Bob.txt
# Rebase df3ad1d..0b22064 onto df3ad1d (3 commands)[...]
复制代码


这样就可以了,然后关闭编辑器,告诉 git 开始执行命令。


接下来发生的事情就像一个普通的rebase:从启动时引用的提交开始,列出的每个提交都将一个接一个地被应用。


现在没有发生这样一种情况,但是当你重新排序实际的代码改动时,可能会发生,即你在rebase期间遇到冲突。毕竟你们可能混淆了彼此之间的变化。

像平常一样解决它们就好。


在应用第一次提交后,编辑器将打开,并允许你为Alice.txt的合并改动添加一条新的提交消息。我已经删掉了两个旧提交的文本,换成了"Add a lot of very important text to Alice"。


关闭编辑器完成提交后,它将再次打开以允许你更改Add Bob.txt的提交信息。这里我们把“.txt”删除然后关闭编辑器继续。


就这样!你再次改写了历史。这一次比amend的改动要大得多!


如果你再次查看git log,你将看到有两个新提交取代了我们之前的三个提交。现在你已经熟练使用了 rebase的操作,并且达到了我们的预期。


> git log --oneline105177b (HEAD -> interactiveRebase) Add Bobed78fa1 Add a lot very important text to Alicedf3ad1d (origin/master, origin/HEAD, master) Add Alice800a947 Add Tutorial Text
复制代码


公共历史,为什么你不应该重写它,万一要重写如何保证安全


如上所述,如果你的工作流程涉及大量小改动的提交,更改分支历史是非常有用的一部分。


虽然所有的小改动有助于简化你的工作,例如,验证你的测试组件是否通过时,如果不通过,只删除或修改这些特定的更改。而你在HelloWorld.java 上面做的 100 个提交可能不是你想和别人分享的东西。


你最想与他们分享的,是一些带有很好的提交信息的格式良好的更改,告诉你的同事你为此做了什么。

只要所有这些小的提交只存在于你的开发环境中,你就完全可以安全地执行git rebase -i并将历史更改为你想要的内容。


当涉及到改变公共历史时,事情会变得麻烦。这意味着改动任何已经进入远程仓库的内容。

这时改动已经成为公共的了,而其他人的分支可能是基于那段历史。你通常是不想改变远程分支的内容的。


通常的建议是“永远不要重写公共历史!”而当我在这里重复这一点时,我必须承认,在一些情况下,你可能仍然想重写公共历史。


一般这种情况,针对的是历史并不是“真正”公开的。你当然不想在开源项目的主分支上重写历史,或者类似于你公司的发布分支。


你可能想重写历史的地方是你刚刚push出来,只是为了和同事们分享的分支。


你可能正在进行基于主干的开发,但是希望共享一些甚至还没有编译的东西,所以你显然不希望有意地将其放在主分支上。或者你可能有一个工作流,在其中共享功能分支。


特别是功能分支,你希望能经常将它们rebase到当前的主分支上。但正如我们所知 git rebase 会将分支的提交作为新提交添加到我们要合的主分支上。这其实改写了历史。在共享了这个功能分支的情况下,它更是重写了公共历史。


那么,如果我们遵循“永远不重写公共历史”的准则,我们该怎么办?


怎么做到不rebase我们的分支但是希望它仍然可以将改动合并到我们主分支的末尾呢?


不使用共享的功能分支?


诚然,这个答案实际上是一个合理的答案,但你可能仍然无法做到这一点。因此,你唯一能做的就是接受重写公共历史并将更改后的历史“推送”到远程仓库。


如果你只是做了一个git push,你会被通知不允许这样做,因为你的本地分支已经与远程分支分离了。

你需要强制推送更改,并用本地版本覆盖远程版本。


正如我强调的那样,你现在可能已经准备好尝试git push --force 。如果你想安全地重写公共历史,你真的不应该这么做!


你最好使用 --force 的一个更谨慎的用法:--force-with-lease !


--force-with-lease将在 push之前,检查你本地版本的远程分支与实际的远程分支是否匹配。

这样你就可以确保,你不会在重写历史的时候无意中抹去可能是别人push的任何改动!



这里我给你留下一句箴言:


除非你真的确定你在做什么,否则不要重写公共历史。如果你非得这样做,就要保证安全和强制


阅读历史


了解了你的 开发环境 (尤其是 本地仓库 )中不同区域之间的差异,以及提交和历史记录是如何工作的,执行rebase应该不会让你感到畏惧了。


有时事情还是会出问题。在解决冲突时,你可能执行了rebase 操作,意外地接受了错误的文件版本。

可能不是你添加的功能,而是你的同事在文件中添加了一行日志记录。


幸运的是, git 有一个内置的安全功能,称为 Reference Logs ,又名 reflog


每当在本地仓库中更新任何引用(如分支的提示)时,就会添加一个引用日志条目。


所以无论什么时候,在你 commit 改动,rebase 或以其他方式移动 HEAD时,都会有一个记录。


本教程看到这里,你应该已经知道当我们搞乱了一个rebase时,这个方法是如何派上用场的,对吧?

我们知道rebase会将分支的HEAD 移动到我们所基于的位置,然后应用我们的更改。交互式 rebase的工作原理与之类似,但可能会对提交执行某些操作,例如“压缩”或“重写”。


如果你没有在前面提到交互式变基时在分支上练习过,请再次切换回那部分,我们在那里有很多的练习和说明。


让我们看看通过运行 git reflog,我们在这个分支上所做的事情。


你可能会看到很多输出,但顶部的前几行应该与以下内容类似:


> git reflog105177b (HEAD -> interactiveRebase) HEAD@{0}: rebase -i (finish): returning to refs/heads/interactiveRebase105177b (HEAD -> interactiveRebase) HEAD@{1}: rebase -i (reword): Add Bobed78fa1 HEAD@{2}: rebase -i (squash): Add a lot very important text to Alice9e06fca HEAD@{3}: rebase -i (start): checkout HEAD~30b22064 HEAD@{4}: commit: Add more text to Alice062ef13 HEAD@{5}: commit: Add Bob.txt9e06fca HEAD@{6}: commit: Add text to Alicedf3ad1d (origin/master, origin/HEAD, master) HEAD@{7}: checkout: moving from master to interactiveRebase
复制代码


情况正是如此,这里有我们所做的每一件事,从切换分支到rebase


看到我们所做的事情很酷,但是如果我们在某个地方搞砸了,如果不是因为每行开头的 hash 值,那就没办法了。


如果将reflog与上一次查看log 时的输出进行比较,就会发现这些节点就是提交时引用的,我们可以像之前那样去调用它们。


但假设我们实际上不想使用rebase。我们要如何删除这次所做的改变呢?


我们可以将 HEAD 移到rebase之前的点,执行git reset 0b22064


在这个例子中, 0b22064 是rebase之前的提交。通常,你也可以通过HEAD@{4}取“HEAD 四次前”的提交。请注意,如果你在这两者之间切换了分支,或者执行了创建日志项的任何其他操作,则可能会有一个更大的数字。


如果现在查看 log,你将看到三个提交前的原始状态。


但这也许并不是我们想要的。rebase很好,我们只是不喜欢这种更改 Bob commit 信息的方式。


那么我们可以在当前状态下再执行一次rebase -i,就像我们一开始那样。


或使用reflog重新跳回 rebase 之后并从那里“修改”提交。


现在你已经知道怎么做了,自己来试试吧。除此之外,你还知道,有一个reflog命令,它允许你撤销大多数错误的改动。



头图:Unsplash

作者:刘诗琴

原文:https://mp.weixin.qq.com/s/Kxlipuppuzcb9qKx8ZE_sQ

原文:交互式教程!带你深入理解 Git 原理

来源:微医大前端技术 - 微信公众号 [ID:wed_fed]

转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2021-07-31 14:002538

评论

发布
暂无评论
发现更多内容

架构师之路-UML 入门

闻人

学习 架构设计 极客大学架构师训练营 架构总结

ARTS 打卡(20.06.08-20.06.14)

小王同学

软件设计原则作业

Mr.Monkey

极客时间 - 架构师培训 -2 期作业

Damon

ARTS打卡 第1周

Scotty

ARTS 打卡计划

每周学习总结 - 架构师培训2期

Damon

第二课作业

架构师训练营第二章作业

张明森

架构学习总结 - 1 - 软件设计原则

Chasedreamer

依赖倒置原则

elfkingw

如何优雅的理解HBase和BigTable

Rayjun

Java HBase

架构师训练营-第一周作业1-食堂就餐卡系统设计

清风徐徐

极客大学架构师训练营 UML

ARTS_20200_week1

不在调上

ARTS 打卡计划

架构师训练营-第一周学习总结

清风徐徐

gitlab-runner 安装

dudu

设计模式原则思考

张瑞浩

【架构师训练营-周总结-2】

小动物

总结 极客大学架构师训练营

数据科学的门槛将提高,架构设计UML,John 易筋 ARTS打卡Week 04

John(易筋)

架构设计 ARTS 打卡计划 ARTS活动 arts

架构师训练营第二周学习总结

张明森

学习总结

Mr.Monkey

框架设计原则

首次披露我和知识星球老吴的一段对话

池建强

产品思维 产品定位 知识星球

架构师训练营作业(第二周)

默默

极客大学架构师训练营

【荒于嬉】事务的特性及隔离级别

luojiahu

事务

架构师训练营 - 第二周 - 学习总结

stardust20

从车辆工程转行程序员两年,我是这么走过来的

WB

程序员 汽车电子

实践Java如何创建安全的线程池

tingye

多线程 线程池 「Java 25周年」

架构师训练营-学习笔记-第二周

心在飞

极客大学架构师训练营

「架构师训练营」第2周作业

Amy

极客大学架构师训练营 作业

面向对象设计原则

elfkingw

极客大学架构师训练营

《微服务设计》读后感

w0807m

微服务

交互式教程!带你深入理解 Git 原理_文化 & 方法_微医大前端技术_InfoQ精选文章