学习 git 概念,而不是命令

2025-05-28

学习 git 概念,而不是命令

交互式 git 教程旨在教您如何 git 工作,而不仅仅是执行哪些命令。

那么,你想使用 git 对吗?

但您不只是想学习命令,您还想了解您正在使用什么?

那么这就是为你准备的!

让我们开始吧!


基于 Rachel M. Carmena 博客文章“如何教授 Git”中的一般概念。

虽然我发现互联网上的许多 git 教程过于注重做什么而不是事情如何运作,但对于两者(以及本教程的来源!)来说,最宝贵的资源是git 书籍参考页面

所以,如果你读完之后仍然感兴趣,那就去看看吧!我希望本教程略有不同的概念能帮助你理解那里详细介绍的所有其他 git 功能。



概述

下图中你可以看到四个盒子。其中一个是独立的,而另外三个则被组合在一起,我称之为“开发环境”

git 组件

不过,我们先从独立的版本库开始。远程仓库是你在与他人共享更改时发送更改的地方,也是你从那里获取更改的地方。如果你用过其他版本控制系统,那就没什么意思了。

开发环境你本地机器上的环境。
它由三个部分组成:工作目录暂存区本地仓库。在开始使用 git 时,我们会进一步了解这些内容。

选择要放置开发环境的位置
只需转到您的主文件夹,或您喜欢放置项目的任何位置即可。不过,您无需为开发环境创建新文件夹。

获取远程存储库

现在我们要获取一个远程存储库并将其中的内容放到您的机器上。

我建议我们使用这个(如果您还没有在 github 上阅读这篇文章,请访问https://github.com/UnseenWizzard/git_training.git )。

为此我可以使用git clone https://github.com/UnseenWizzard/git_training.git

但是,由于本教程需要您将在开发环境中所做的更改导出到远程仓库,而 GitHub 不允许任何人对任何人的仓库执行此操作,因此您最好现在就创建一个fork。此页面右上角有一个按钮可以执行此操作。

现在您已经有了我的远程存储库的副本,是时候将其放到您的机器上了。

为此我们使用git clone https://github.com/{YOUR USERNAME}/git_training.git

如下图所示,这会将远程仓库复制到两个地方:工作目录本地仓库
现在你明白了 Git 是如何进行分布式版本控制的。本地仓库是远程仓库的副本,其行为与远程仓库相同。唯一的区别是你不会与任何人共享它。

您还git clone可以在调用它的位置创建一个新文件夹。git_training现在应该有一个文件夹了。打开它。

克隆远程仓库

添加新内容

有人已将文件放入远程存储库。它Alice.txt在那里有点孤零零的。让我们创建一个新文件并将其命名为Bob.txt

你刚才的操作是将文件添加到你的工作目录 (Working Directory )。你的工作目录
中有两种文件git 已知的已跟踪文件 ( tracked files) 和git 尚不知道的未跟踪文件 (untracked files)。

要查看工作目录运行中发生的情况git status,它将告诉您当前位于哪个分支、本地存储库是否与远程存储库不同以及已跟踪未跟踪文件的状态

您将看到该文件Bob.txt处于未跟踪状态,git status甚至会告诉您如何更改。
在下图中,您可以看到按照建议执行操作后发生的情况git add Bob.txt:您已将文件添加到暂存区,其中收集了您希望放入存储库的所有更改。

将更改添加到暂存区

当您添加了所有更改(目前仅添加 Bob)后,您就可以将刚刚所做的更改提交到本地存储库

你提交的变更记录是一些有意义的工作,因此当你运行它时,git commit会打开一个文本编辑器,让你输入一条消息,说明你刚刚做了哪些操作。保存并关闭消息文件后,你的提交就会添加到本地仓库中。

提交到本地仓库

您还可以在命令行中直接添加提交消息git commit,方法是像这样调用:git commit -m "Add Bob"。但是因为您想编写好的提交消息,所以您确实应该花时间使用编辑器。

现在,您的更改位于本地存储库中,只要没有其他人需要它们或者您尚未准备好共享它们,这是一个很好的地方。

为了与远程存储库共享您的提交,您需要push它们。

推送到本地仓库

运行后,git push更改将被发送到远程存储库。在下图中,您可以看到运行后的状态push

推送更改后所有组件的状态

做出改变

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

看一下Alice.txt

它实际上包含一些文本,但Bob.txt事实并非如此,所以让我们改变它并放入Hi!! I'm Bob. I'm new here.其中。

git status如果你现在运行,你会看到Bob.txt修改
在此状态下,更改仅在你的工作目录中。

如果您想查看工作目录中发生了什么变化,您可以运行git diff,现在就可以看到以下内容:



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


Enter fullscreen mode Exit fullscreen mode

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.txt
index 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.


Enter fullscreen mode Exit fullscreen mode

由于这种改变正是我们想要的,因此让我们git add Bob.txt暂存文件的当前状态。

现在我们就可以开始commit刚才的操作了。我选择这样做git commit -m "Add text to Bob"是因为我觉得这么小的改动,一行代码就足够了。

我们知道,这些更改现在位于本地存储库中。
我们可能仍然想知道我们刚刚提交了哪些更改,以及之前有哪些更改。

我们可以通过比较提交来做到这一点。git
中的每个提交都有一个唯一的哈希值,用于引用它。

如果我们看一下,git log我们不仅会看到所有提交及其哈希值以及作者日期的列表,我们还会看到本地存储库的状态以及有关远程分支的最新本地信息

现在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 71a6a9b299b21e68f9b0c61247379432a0b6007c 
Author: UnseenWizzard <nicola.riedmann@live.de>
Date:   Fri Jan 25 20:06:57 2019 +0100

    Add Alice

commit ddb869a0c154f6798f0caae567074aecdfa58c46
Author: 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


Enter fullscreen mode Exit fullscreen mode

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

  • 前两次提交都是我做的。
  • 你最初添加 Bob 的提交是远程仓库master分支的当前HEAD。当我们讨论分支和获取远程更改时,我们会再次讨论这一点。
  • 本地存储库中的最新提交是我们刚刚进行的提交,现在我们知道它的哈希值。

请注意,实际的提交哈希值会有所不同。如果你想了解 Git 是如何得出这些修订 ID 的,可以看看这篇有趣的文章

要比较该提交和之前的提交,我们可以执行git diff <commit>^!,其中^!告诉 git 与之前的提交进行比较。因此,在本例中,我运行git diff 87a4ad48d55e5280aa608cd79e8bce5e13f318dc^!

我们也可以这样做git diff 8af2ff2a8f7c51e2e52402ecb7332aec39ed540e 87a4ad48d55e5280aa608cd79e8bce5e13f318dc来获得相同的结果,并且通常用于比较任意两个提交。请注意,这里的格式是git diff <from commit> <to commit>,所以我们的新提交排在后面。

在下图中,您可以再次看到更改的不同阶段,以及适用于文件当前位置的 diff 命令。

更改相关 diff 命令的状态

既然我们确定我们已经做出了我们想要的改变,那就继续吧git push

分枝

git 的另一个优点是,使用分支工作非常简单,并且是使用 git 工作方式不可或缺的一部分。

事实上,自从我们开始以来,我们就一直在致力于一个分支。

当您使用远程存储库clone时,您的开发环境会自动在存储库主分支或分支上启动。

git 的大多数工作流程都包括在分支 (branch)上进行更改,然后merge再将它们合并到master 分支
通常,你会在自己的分支 (branch)上工作,直到你完成并对更改有信心,然后才能将它们合并到master 分支

许多 Git 仓库管理器(例如GitLabGitHub)也允许对分支进行保护,这意味着并非所有人都可以对push分支进行更改。主分支通常默认受到保护。

别担心,当我们需要时,我们会更详细地讨论这些事情。

现在我们想创建一个分支来做一些修改。也许你只是想自己尝试一下,不想影响master分支的工作状态,或者你没有权限push创建master 分支

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

让我们做些改变吧Alice.txt!在第二行放些文字怎么样?

我们想要分享这个变化,但不想立即把它放到mastergit branch <branch name>上,所以让我们使用 为它创建一个分支。

要创建一个名为 的新分支,change_alice您可以运行git branch change_alice

这会将新分支添加到本地存储库

虽然您的工作目录暂存区并不真正关心分支,但您始终会commit关注您当前所在的分支。

你可以将git 中的分支视为指针,指向一系列提交。当你 时commit,你会向当前指向的任何内容添加内容。

仅仅添加一个分支并不能直接带你到达那里,它只是创建了一个指针。
实际上,你的本地仓库的当前状态可以看作是另一个指针,称为HEAD,它指向你当前所在的分支和提交。

如果这听起来很复杂,下面的图表希望能帮助您理清一些事情:

添加分支后的状态

要切换到我们的新分支,您必须使用git checkout change_alice。它的作用只是将HEAD移动到您指定的分支。

由于您通常希望在创建分支后立即切换到分支,因此-bcheckout命令提供了一个便捷的选项,允许您直接创建checkout分支,而不必事先创建它。

因此,为了创建并切换到我们的change_alice分支,我们也可以只调用git checkout -b change_alice

切换分支后的状态

你会注意到你的工作目录没有改变。我们修改的 Alice.txt内容与当前所在的分支无关。
现在你可以像之前在主干上操作一样,将add更改commit暂存此时它仍然与分支无关),并最终将你的更改提交分支。Alice.txtchange_alice

还有一件事你还不能做。尝试将git push你的更改应用到远程存储库

您将看到以下错误,并且 - 由于 git 随时准备提供帮助 - 会给出解决问题的建议:



fatal: The current branch change_alice has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin change_alice 


Enter fullscreen mode Exit fullscreen mode

但我们不想盲目地这么做。我们来这里是为了了解实际情况。那么,上游分支远程分支是什么呢?

还记得我们之前提到的远程仓库cloned吗?当时它不仅包含本教程,还包含两个分支。Alice.txt

我们刚刚开始处理的是主文件,我将其称为“tutorial_wip”,我将对本教程所做的所有更改都提交到该主文件

当我们将远程存储库中的内容复制到您的开发环境中时,后台会发生一些额外的步骤。

Git 将本地存储库远程设置为您克隆的远程存储库,并赋予其默认名称origin

您的本地存储库可以跟踪多个远程存储库origin,并且它们可以有不同的名称,但在本教程中我们将坚持使用 ,而不用其他任何东西。

然后它将两个远程分支复制到您的本地存储库中,最后为您checked out 控制

执行此操作时,另一个隐式步骤会发生。当您的checkout分支名称与远程分支完全匹配时,您将获得一个链接到远程分支的新本地分支。远程分支是本地分支的上游分支

在上图中,你只能看到你拥有的本地分支。运行 即可查看本地分支列表git branch

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

远程和本地分支

现在,我们可以调用建议git push --set-upstream origin change_alice,并将push分支上的更改复制到新的远程。这将在远程存储库上创建一个change_alice分支,并设置本地以跟踪该新分支。 change_alice

如果我们确实希望分支跟踪远程仓库中已存在的内容,还有另一种选择。也许当我们在本地分支上处理相关内容时,一位同事已经推送了一些更改,而我们想将两者整合起来。那么,我们可以通过使用将分支上游change_alice设置为新的远程仓库git branch --set-upstream-to=origin/change_alice,然后从那里跟踪远程分支。

之后,查看github 上的远程存储库,您的分支将在那里,可供其他人查看和使用。

我们将很快介绍如何将其他人的更改引入到您的开发环境中,但首先我们将更多地使用分支,介绍当我们从远程存储库获取新事物时也会发挥作用的所有概念。

合并

由于您和其他人通常都会在分支上工作,因此我们需要讨论如何通过合并将更改从一个分支转移到另一个分支。

我们刚刚Alice.txtchange_alice分支机构进行了更改,我想说我们对所做的更改感到满意。

如果你转到git checkout mastercommit我们在另一个分支上所做的更改将不会存在。为了将更改导入到 master 分支,我们需要将该merge分支导入到change_alicemaster分支

请注意,您总是会将merge某个分支插入到您当前所在的分支中。

快速合并

因为我们已经checked out掌握了,所以我们现在就可以git merge change_alice

由于没有其他冲突的更改Alice.txt,并且我们也没有对master进行任何更改,因此这将在所谓的快速合并中顺利进行。

在下图中,您可以看到这仅意味着指针可以简单地前进到change_alice指针所在的位置。

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

快速前向合并之前

第二张图表显示了我们的变化merge

快速前向合并后

合并不同的分支

让我们尝试一些更复杂的事情。

Bob.txt在主控新行添加一些文本并提交。

然后git checkout change_alice,改变Alice.txt并坚持。

下图展示了我们现在的提交历史。mastermasterchange_alice都源自同一次提交,但之后它们出现了分歧,各自都有了额外的提交。

不同的提交

如果您现在git merge change_alice无法进行快速合并,那么您最喜欢的文本编辑器将会打开,并允许您更改merge commitGit 即将执行的操作的消息,以便将两个分支重新合并在一起。您现在可以直接使用默认消息。下图显示了我们执行此操作后 Git 历史记录的状态merge

合并分支

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

正如你之前所记得的,git 中的修订版本不仅是文件的快照,还包含它们来自何处的信息。每个修订版本commit都有一个或多个父提交。我们的新提交,既包含来自master 分支merge的最后一个提交,也包含我们在另一个分支上所做的提交作为它的父提交。

解决冲突

到目前为止,我们的更改尚未互相干扰。

让我们引入一个冲突然后解决它。

创建checkout一个新的分支。你知道怎么做,但也许可以尝试一下,git checkout -b这样可以让你的工作更轻松。
我的分支名为bobby_branch

在分支上,我们将对 进行修改Bob.txt
第一行仍应为Hi!! I'm Bob. I'm new here.。将其更改为Hi!! I'm Bobby. I'm new here.

先进行暂存,然后commit进行修改,再进行checkout 母版制作。在这里,我们将同一行更改为Hi!! I'm Bob. I've been here for a while now.修改commit

现在是时候将merge新分支添加到master了。
当你尝试这样做时,你会看到以下输出



    Auto-merging Bob.txt
    CONFLICT (content): Merge conflict in Bob.txt
    Automatic merge failed; fix conflicts and then commit the result.


Enter fullscreen mode Exit fullscreen mode

两个分支上的同一行都发生了变化,git 无法自行处理这个问题。

如果您运行,git status您将获得有关如何继续的所有常见有用说明。

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

对于像这样的简单冲突,你最喜欢的文本编辑器就足够了。对于合并包含大量更改的大文件,一个更强大的工具会让你的工作更轻松,我假设你最喜欢的 IDE 带有版本控制工具和美观的合并视图。

如果你打开,Bob.txt你会看到类似这样的内容(我截断了我们之前可能放在第二行的内容):



    <<<<<<< HEAD
    Hi! 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]


Enter fullscreen mode Exit fullscreen mode

在顶部您可以看到当前 HEAD 中发生的变化Bob.txt,在下面您可以看到我们正在合并的分支中发生的变化。

要手动解决冲突,您只需确保最终得到一些合理的内容,并且没有 git 引入到文件中的特殊行。

因此继续将文件更改为如下内容:



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


Enter fullscreen mode Exit fullscreen mode

从现在开始,我们所做的就是处理任何变更时所做的。
我们在 时将它们暂存add Bob.txt,然后我们commit

我们已经知道了解决冲突的变更对应的提交。合并提交在合并时始终存在。

如果您在解决冲突的过程中意识到您实际上并不想坚持下去merge,您可以abort通过运行来实现git merge --abort

变基

Git 还有另一种简洁的方式来集成两个分支之间的更改,称为rebase

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

在我们的简单合并示例中,我们在特定的提交处从master分支,然后在masterchange_alice分支上提交了一些更改。

当一个分支与其所基于的分支不同,并且您想要将最新的更改集成回当前分支时,rebase提供一种比 would 更干净的方法merge

正如我们所见,merge引入了合并提交,其中两个历史记录再次整合。

简单地说,变基只是改变了分支所基于的历史记录点(提交)。

为了尝试一下,我们先再次检出master分支,然后基于它创建/检出一个新分支。
我将其命名为“我的” add_patrick,并添加了一个新Patrick.txt文件,并提交了“Add Patrick”的消息。

向分支添加提交后,返回master 分支,进行更改并提交。我向 添加了更多文本Alice.txt

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

重新定基之前的历史记录

现在让我们再次将主分支checkout add_patrick上所做的更改放到我们工作的分支中!

当我们时,我们根据分支的当前状态git rebase master重新建立我们的分支add_patrick

该命令的输出为我们提供了有关其中发生的事情的很好的提示:



    First, rewinding head to replay your work on top of it...
    Applying: Add Patrick


Enter fullscreen mode Exit fullscreen mode

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

它指向的位置与add_patrickrebase 开始之前的位置相同。对于 rebase 来说,它首先会回到共同祖先,然后再移动到我们想要 rebase 的分支的当前头部。

因此HEAD0cfc1d2提交移动到了master分支最顶端的7639f4b提交。 然后 rebase 将我们在分支上所做的每个提交都应用到这个提交上。
add_patrick

更准确地说,在将HEAD移回分支的共同祖先之后,git所做的就是存储您在分支上所做的每个提交的部分(更改、提交文本、作者等)。diff

之后,它会对checkout您正在重新定位的分支进行最新的提交,然后将每个存储的更改应用为新的提交

因此,在我们最初的简化视图中,我们假设 0cfc1d2 提交之后rebase不再指向其历史记录中的共同祖先,而是指向 master 分支的头部。 实际上,0cfc1d2提交已经消失,分支从一个新的0ccaba8提交开始,该提交以master 分支的最新提交作为其祖先。 我们让它看起来像是基于当前master分支而不是其旧版本,但这样做实际上重写了分支的历史记录。 在本教程的最后,我们将学习更多关于重写历史记录的知识,以及何时适合重写,何时不适合重写。
add_patrick
add_patrick

重新设置基准后的历史

Rebase当您在基于共享分支(例如master )的自己的开发分支上工作时,是一个非常强大的工具。

使用 rebase,您可以确保经常集成其他人所做的更改并推送到masterfast-forward merge ,同时保持干净的线性历史记录,以便您在将工作放入共享分支时执行操作。

保持线性历史记录也使得阅读或查看(尝试git log --graph或查看GitHubGitLab的分支视图)提交日志比拥有充斥着合并提交的历史记录更有用,通常只使用默认文本。

解决冲突

就像您可能会遇到冲突一样merge,如果您遇到两次提交更改文件的相同部分。

但是,当您在合并过程中遇到冲突时,rebase您无需在额外的合并提交中修复它,而只需在当前正在应用的提交中解决它即可。
同样,您的更改将直接基于原始分支的当前状态。

实际上,解决冲突的方式与rebase解决冲突的方式非常相似,merge因此,如果您不确定如何解决,请参阅该部分。

唯一的区别是,由于您没有引入合并提交,因此无需进行commit解决方案。只需将add更改提交到暂存环境即可git rebase --continue。冲突将在刚刚应用的提交中得到解决。

就像合并时一样,您可以随时停止并放弃迄今为止所做的一切git rebase --abort

使用远程更改更新开发环境

到目前为止,我们只学习了如何进行和分享更改。

如果您只是自己工作的话,这很适合您所做的事情,但通常会有很多人做同样的事情,并且我们希望以某种方式将他们的更改从远程存储库转移到我们的开发环境中。

因为已经有一段时间了,让我们再看一下 git 的组件:

git 组件

就像您的开发环境一样,使用相同源代码的其他所有人都有自己的开发环境。

许多开发环境

所有这些开发环境都有自己的工作阶段性变化,这些变化在某些时候会committed转移到本地存储库,最终pushed转移到远程

为了举例,我们将使用GitHub提供的在线工具,模拟其他人在我们工作时对远程进行更改。

转到github.comfork上的此 repo并打开该文件。Alice.txt

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

github 编辑

Alice.txt在这个存储库中,我在名为的分支上添加了一个远程更改fetching_changes_sample,但在您的存储库版本中,您当然可以只更改文件master

获取更改

我们还记得,当您将对本地存储库git push所做的更改同步远程存储库时。

要将对远程所做的更改放入本地存储库,请使用git fetch

这会将远程上的任何更改(提交以及分支)放入您的本地存储库中。

请注意,此时,更改尚未集成到本地分支,因此也尚未集成到工作目录暂存区

获取更改

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



    git status
    On branch fetching_changes_sample
    Your branch is behind 'origin/fetching_changes_sample' by 1 commit, and can be fast-forwarded.
      (use "git pull" to update your local branch)


Enter fullscreen mode Exit fullscreen mode

拉取变更

由于我们没有工作阶段性的变化,我们现在可以执行将存储库git pull中的更改一直放入我们的工作区域。

拉取操作会隐式地同步远程仓库fetch,但有时单独执行操作也是一个好主意。 例如,当你想同步任何新的远程分支时,或者当你想在执行类似操作之前确保本地仓库是最新的时fetch
git rebaseorigin/master

拉取变更

在此之前,我们pull先在本地更改一个文件,看看会发生什么。

现在让我们也改变一下Alice.txt我们的工作目录!

如果您现在尝试执行,git pull您将看到以下错误:



    git pull
    Updating df3ad1d..418e6f0
    error: Your local changes to the following files would be overwritten by merge:
            Alice.txt
    Please commit your changes or stash them before you merge.
    Aborting


Enter fullscreen mode Exit fullscreen mode

您无法进行任何更改,但工作目录pull中的文件所作的修改也会被您所进行的提交所更改。pull

解决这个问题的一种方法是,在最终完成更改之前,先将更改放到您有信心的程度,然后将add其放到暂存环境commit中,这是了解另一个伟大工具的好时机git stash

存储更改

如果在任何时候您有本地更改但还不想提交,或者想要在尝试不同的角度解决问题时将其存储在某个地方,那么您可以stash删除这些更改。

Agit stash基本上是一个更改堆栈,您可以在其中存储对工作目录的任何更改。

您最常使用的命令是git stash将对工作目录的任何修改放入存储中,以及git stash pop获取存储的最新更改并将其再次应用到工作目录

就像它命名的堆栈命令一样,它会git stash pop在再次应用之前移除最新的存储更改。
如果您想保留存储的更改,可以使用git stash apply,这样在应用更改之前不会将其从存储中移除。

要检查当前情况,stash您可以使用git stash list列出各个条目,并git stash show显示最新条目中的更改stash

另一个非常方便的命令是git stash branch {BRANCH NAME},它创建一个分支,从您存储更改时的 HEAD 开始,并将存储的更改应用到该分支。

现在我们知道了,让我们运行它来工作目录git stash中删除我们对的本地更改,以便我们可以继续通过网站进行所做的更改。Alice.txtgit pull

之后,让我们git stash pop恢复更改。由于我们编辑的
提交和编辑的更改都发生了修改,因此您必须解决冲突,就像在或 中一样 完成后更改。pullstashAlice.txtmergerebase
addcommit

冲突拉动

现在我们已经了解了如何将fetch远程pull 更改引入到我们的开发环境中,现在是时候创建一些冲突了!

不要push提交已更改的内容Alice.txt并返回到github.com上的远程存储库

我们还将再次进行更改Alice.txt并提交更改。

现在我们的本地存储库远程存储库之间实际上存在两个冲突

不要忘记运行git fetch来查看远程更改,而无需pull立即输入。

如果您现在运行git status,您将看到两个分支上都有一个与另一个不同的提交。



    git status
    On branch fetching_changes_sample
    Your 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)


Enter fullscreen mode Exit fullscreen mode

此外,我们在两次提交中都更改了同一个文件,从而引发了merge我们必须解决的冲突。

本地存储库远程存储库git pull之间存在差异时,会发生与两个分支完全相同的事情merge

此外,你可以将远程仓库中的分支与本地仓库中的分支之间的关系视为基于另一个分支创建分支的特殊情况。
本地分支基于你上次创建远程分支时远程fetched分支的状态。

从这个角度考虑,获得远程更改的两个选项非常有意义:

当你将git pull本地远程版本的分支合并时,就像分支一样,这将引入一个 _merge 提交mergedmerging

由于任何本地分支都基于其各自的远程版本,因此我们也可以rebase这样做,以便我们在本地所做的任何更改看起来都像是基于远程仓库中可用的最新版本。
为此,我们可以使用git pull --rebase(或简写git pull -r)。

正如在Rebasing部分中详细描述的那样,保持干净的线性历史记录是有好处的,这就是为什么我强烈建议您在git pull执行时这样做git pull -r

您还可以通过使用这样的命令设置标志来告诉 git 使用rebase而不是作为merge其默认策略git pullpull.rebasegit config --global pull.rebase true

git pull如果您在前几段我第一次提到它时还没有运行,那么现在让我们运行git pull -r以获取远程更改,同时使它看起来像我们的新提交刚刚发生在它们之后。

当然,与正常情况rebase(或merge)一样,您必须解决我们引入的冲突才能git pull完成。

挑选

恭喜!您已成功使用更高级的功能!

现在您了解了如何使用所有典型的 git 命令,更重要的是它们如何工作。

与我仅仅告诉您要输入什么命令相比,这将使以下概念更容易理解。

那么让我们开始学习如何cherry-pick提交吧!

从前面的章节中你还记得 a 的大致commit组成是什么吗?

当您创建分支时,您的提交如何作为具有相同变更集消息的rebase新提交应用

每当您想要从一个分支中获取一些选择性更改并将其应用到另一个分支时,您都需要cherry-pick这些提交并将它们放在您的分支上。

这正是git cherry-pick允许您对单个提交或一系列提交执行的操作。

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

让我们看一个例子,每个例子包含cherry-pick一个或多个提交:

下图展示了我们尚未进行任何操作之前的三个分支。假设我们确实想将一些更改从该add_patrick分支迁移到该change_alice分支。遗憾的是,这些更改尚未迁移到 master 分支,所以我们无法直接rebase迁移到 master 分支来获取这些更改(以及另一个分支上我们可能根本不需要的更改)。

挑选前的分支

因此,我们只git cherry-pick提交63fc421
下图直观地展示了我们运行git cherry-pick 63fc421

挑选单个提交

如您所见,包含我们想要的更改的新提交出现在分支上。

此时请注意,就像我们之前见过的任何其他类型的分支更改一样,在命令执行过程中出现的任何冲突都cherry-pick必须由我们解决,然后命令才能执行。

另外,与所有其他命令一样,您可以在解决冲突后--continue执行该命令,或者完全决定执行该命令。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

然后git commit使用“这是历史”之类的消息,您就完成了。

等等,我说过我们完成了吗?不,你会清楚地看到我们在这里犯了一些错误:

  • 我们忘记将更改添加到Bob.txt
  • 我们没有写好提交信息

修改最后一次提交

一次性解决这两个问题的一种方法就是执行amend我们刚刚做出的提交。

Amend执行最新的提交基本上就像创建一个新的提交一样。

在我们做任何事情之前,先用 看一下你最近的提交git show {COMMIT}。输入提交哈希值(你可能仍然会在调用的命令行中看到它git commit,或者在 中git log),或者只输入 HEAD

就像git log您会看到消息、作者、日期,当然还有更改。

现在让我们看看amend我们在那次提交中做了什么。

git add Bob.txt将更改发送到暂存区,然后git commit --amend

接下来发生的事情是展开您的最新提交,将暂存区中的新更改添加到现有更改中,然后打开提交消息的编辑器。

在编辑器中,你会看到之前的提交信息。
你可以随意修改它。

完成后,再次查看最新的提交git show HEAD

正如你现在肯定已经预料到的,提交哈希值已经不同了。原始提交消失了,取而代之的是一个新的提交,其中包含合并的更改和新的提交消息。

请注意,其他提交数据(例如作者和日期)与原始提交相比没有变化。如果您确实需要,也可以在修改时使用额外的--author={AUTHOR}--date={DATE}标志来更改这些数据。

恭喜!你第一次成功改写了历史!

交互式变基

一般来说,当我们 时git rebase,我们会将其rebase迁移到一个分支。当我们执行类似 的操作时git rebase origin/master,实际上会将 rebase 到该分支的HEAD上。

事实上,如果我们愿意,我们可以rebase进行任何提交。

请记住,提交包含有关其之前的历史记录的信息

与许多其他命令一样git rebase具有交互模式。

与大多数其他功能不同,交互式 rebase功能是您可能会经常使用的功能,因为它允许您根据需要随意更改历史记录。

特别是如果您遵循对更改进行多次小提交的工作流程,这样您就可以在犯错时轻松跳回,交互式 rebase将成为您最亲密的盟友。

废话少说!行动起来!

切换回分支并切换到git checkout新的分支进行工作。

Alice.txt和以前一样,我们将对和进行一些更改Bob.txt,然后进行git add Alice.txt

然后我们git commit使用类似“向爱丽丝添加文本”这样的消息。

现在,我们不再修改那个提交,git add Bob.txt而是git commit同时修改那个提交。消息我使用了“Add Bob.txt”。

为了让事情变得更有趣,我们将对其进行另一项更改Alice.txtgit addgit commit使用“向 Alice 添加更多文本”作为消息。

如果我们现在使用 查看分支的历史记录git log(或者最好使用 快速浏览一下),我们将在mastergit log --oneline上看到我们的三个提交

对我来说它看起来像这样:



git log --oneline
0b22064 (HEAD -> interactiveRebase) Add more text to Alice
062ef13 Add Bob.txt
9e06fca Add text to Alice
df3ad1d (origin/master, origin/HEAD, master) Add Alice
800a947 Add Tutorial Text


Enter fullscreen mode Exit fullscreen mode

关于这一点,我们想修复两个问题,为了学习不同的东西,这两个问题与上一节略有不同amend

  • 将两个更改放在Alice.txt一个提交中
  • 统一命名事物,并从消息中删除有关Bob.txt

要更改这三个新的提交,我们需要变基到它们之前的提交。对我来说,那个提交是df3ad1d,但我们也可以将其引用为当前HEAD中的第三个提交,如下所示HEAD~3

要开始交互, rebase我们使用git rebase -i {COMMIT},所以让我们运行git rebase -i HEAD~3

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



    pick 9e06fca Add text to Alice
    pick 062ef13 Add Bob.txt
    pick 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


Enter fullscreen mode Exit fullscreen mode

请始终注意如何git解释当您调用命令时您可以做的所有事情。

您最常使用的命令可能是但默认情况下是存在的)rewordsquashdroppick

花点时间想想你看到了什么,以及我们要用什么来实现我们上面提到的两个目标。我等着。

有计划了吗?太好了!

在我们开始进行更改之前,请注意,提交是从最旧到最新列出的,因此与git log输出的方向相反。

我将从简单的更改开始,以便我们能够更改中间提交的提交消息。



    pick 9e06fca Add text to Alice
    reword 062ef13 Add Bob.txt
    pick 0b22064 Add more text to Alice
    # Rebase df3ad1d..0b22064 onto df3ad1d (3 commands)
    [...]


Enter fullscreen mode Exit fullscreen mode

现在将两个更改合并Alice.txt到一次提交中。

显然,我们想要将squash后者替换为前者,所以让我们用这个命令来代替pick第二次提交时的 更改Alice.txt。在我的例子中,这个命令是0b22064



    pick 9e06fca Add text to Alice
    reword 062ef13 Add Bob.txt
    squash 0b22064 Add more text to Alice
    # Rebase df3ad1d..0b22064 onto df3ad1d (3 commands)
    [...]


Enter fullscreen mode Exit fullscreen mode

完成了吗?这样就能达到我们想要的效果了吗?

不会吧?正如文件中的注释所说:



    # s, squash = use commit, but meld into previous commit


Enter fullscreen mode Exit fullscreen mode

所以到目前为止,我们所做的就是将 Alice 的第二个提交与 Bob 的提交合并。这不是我们想要的。

我们可以在交互 中做的另一件强大的事情rebase是改变提交的顺序。

如果您仔细阅读了评论,那么您已经知道如何做了:只需移动线条!

值得庆幸的是,您使用的是自己最喜欢的文本编辑器,因此请继续将第二个 Alice 提交移到第一个提交之后。



    pick 9e06fca Add text to Alice
    squash 0b22064 Add more text to Alice
    reword 062ef13 Add Bob.txt
    # Rebase df3ad1d..0b22064 onto df3ad1d (3 commands)
    [...]


Enter fullscreen mode Exit fullscreen mode

这应该可以解决问题,因此关闭编辑器git以开始执行命令。

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

目前还不会发生这种情况,但当你重新排序实际的代码更改时,可能会发生这种情况,在过程中遇到冲突rebase。毕竟,你可能混淆了彼此构建的更改。

像平常一样,只需解决它们即可。

应用第一个提交后,编辑器将打开,并允许您为合并更改的提交添加一条新消息Alice.txt。我删除了两个提交的文本,只添加了“向 Alice 添加了许多非常重要的文本”。

关闭编辑器完成该提交后,它将再次打开,允许您更改提交消息Add Bob.txt。删除“.txt”文件,然后关闭编辑器继续。

就是这样!你又改写了历史。这次比之前改写得更彻底amend

如果你再看一下git log,你会发现有两个新的提交,而不是之前的三个。不过现在你已经熟悉了rebase提交的操作,并且预料到了这一点。



git log --oneline
105177b (HEAD -> interactiveRebase) Add Bob
ed78fa1 Add a lot very important text to Alice
df3ad1d (origin/master, origin/HEAD, master) Add Alice
800a947 Add Tutorial Text


Enter fullscreen mode Exit fullscreen mode

公共历史,为什么你不应该重写它,以及如何安全地重写它

如前所述,更改历史记录是任何涉及在工作时进行大量小提交的工作流程中非常有用的部分。

虽然所有小的原子变化都使您能够非常轻松地验证每次更改后您的测试套件是否仍然通过,如果没有通过,则删除或修改这些特定的更改,但您所做的 100 次提交HelloWorld.java可能不是您想要与人们分享的内容。

您最想与他们分享的是一些结构良好的更改,并带有清晰的提交信息,告诉您的同事您做了什么以及出于什么原因。

只要所有这些小提交仅存在于您的开发环境中,您就可以完美地保存git rebase -i并根据自己的心意更改历史记录。

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

此时,它已经公开,其他人的分支可能基于该历史记录。这通常意味着你不想去打扰它。

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

然而,在所有这些情况下,历史记录并非“真正”公开。你肯定不想在开源项目的主分支或类似公司的发布分支上重写历史记录。

您可能想要重写历史记录的地方是您push编辑的分支,只是为了与一些同事分享。

你可能正在进行基于主干的开发,但想要共享一些尚未编译的代码,所以你显然不想故意将其放到主分支上。
或者你可能有一个共享功能分支的工作流程。

尤其是在功能分支中,你希望经常rebase将它们添加到当前的主分支中。但正如我们所知,agit rebase会将我们分支的提交作为提交添加到我们基于的提交之上。这会重写历史记录。对于共享功能分支,它会重写公共历史记录。

那么,如果我们遵循“永不改写公共历史”的口号,我们应该做什么呢?

永远不要重新设置我们的分支并希望它最终仍然合并到主分支中?

不使用共享功能分支?

诚然,第二个答案确实合理,但你可能仍然无法做到这一点。所以你唯一能做的就是接受将公共历史记录push更改的历史记录重写到远程存储库

如果您只是这样做,git push您将收到通知,告知您不被允许这样做,因为您的本地分支与远程分支有所不同

您将需要force推动更改,并用本地版本覆盖远程。

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

您最好使用--force更谨慎的兄弟--force-with-lease

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

push这样,您可以确保在重写历史记录时不会意外擦除其他人可能做出的任何更改!

推送--force-with-lease 中会发生什么

关于这一点,我将给你们留下一个稍微改变的咒语:

除非你真的确定自己在做什么,否则不要改写公共历史。如果要改写,务必谨慎,并采取强制措施。

阅读历史

了解开发环境中各个区域(尤其是本地存储库)之间的差异以及提交和历史记录的工作方式,这对rebase您来说应该不是什么可怕的事情。

但有时还是会出错。你可能rebase在解决冲突时意外接受了错误版本的文件。

现在,您不再添加任何功能,而是您的同事在文件中添加了一行日志。

幸运的git是,它有一个内置的安全功能(称为“参考日志”,又称)可以为您提供支持reflog

每当本地存储库中更新任何引用(例如分支的尖端)时,都会添加一个引用日志条目。

因此,您进行的任何操作都会被记录下来commit,而且您移动reset或以其他方式移动操作HEAD的时间也会被记录下来。

到目前为止,读完本教程后,您就会明白当我们弄乱了rebase权利时这可能会派上用场吗?

我们知道,a会将我们分支的rebase移动HEAD到我们基于的点,然后 会应用我们的更改。交互的rebase工作原理类似,但可能会对这些提交执行一些操作,例如压缩改写它们。

如果您尚未在我们练习交互式变基的分支上,请再次切换到该分支,因为我们将在那里进行更多练习。

让我们看一下reflog我们在该分支上所做的事情 - 您已经猜到了 - 运行git reflog

您可能会看到很多输出,但顶部的前几行应该类似于此:



git reflog
105177b (HEAD -> interactiveRebase) HEAD@{0}: rebase -i (finish): returning to refs/heads/interactiveRebase
105177b (HEAD -> interactiveRebase) HEAD@{1}: rebase -i (reword): Add Bob
ed78fa1 HEAD@{2}: rebase -i (squash): Add a lot very important text to Alice
9e06fca HEAD@{3}: rebase -i (start): checkout HEAD~3
0b22064 HEAD@{4}: commit: Add more text to Alice
062ef13 HEAD@{5}: commit: Add Bob.txt
9e06fca HEAD@{6}: commit: Add text to Alice
df3ad1d (origin/master, origin/HEAD, master) HEAD@{7}: checkout: moving from master to interactiveRebase


Enter fullscreen mode Exit fullscreen mode

就是这样。我们所做的每一件事,从切换到分支到执行rebase

看到我们所做的事情非常酷,但如果我们在某个地方搞砸了,如果没有每行开头的引用,它本身就毫无用处。

如果将reflog输出与我们上次查看的输出进行比较log,您会看到这些点与提交引用有关,我们可以像那样使用它们。

假设我们实际上并不想进行 rebase,该如何消除它造成的更改呢?

我们转到以开始HEAD之前的点rebasegit reset 0b22064

0b22064在我的情况下是 之前的提交rebase。更一般地,您也可以通过将其引用为四次更改之前的 HEADHEAD@{4}。请注意,如果您在 之间切换了分支或执行了任何其他创建日志条目的操作,则那里的数字可能会更高。

如果您查看现在log,您将看到恢复了三个单独提交的原始状态。

但现在我们意识到这不是我们想要的。这没关系rebase,只是我们不喜欢我们修改 Bob 提交信息的方式。

rebase -i我们可以在当前状态下再做一次,就像我们最初做的那样。

或者我们使用 reflog 并跳回到 rebase 之后并amend从那里提交。

但现在你已经知道怎么做了,所以我就让你自己尝试一下。此外,你也知道,它reflog允许你撤销大多数你可能最终会犯的错误。

文章来源:https://dev.to/unseenwizzard/learn-git-concepts-not-commands-4gjc
PREV
成为算法专家必须了解的 Map、Filter、Reduce 和其他数组迭代器
NEXT
Javascript Array.push 比 Array.concat 快 945 倍 🤯🤔 TDLR 等一下……用 .concat 合并 15,000 个数组需要多长时间…… 基准测试比较 但是为什么 Array.concat 这么慢? 结论:为什么 .push 比 .concat 更快 另一个谜团 我们为什么要在 UI-licious 测试期间执行如此大的数组操作?