选择 git 提交的一部分

Art*_*cto 5 git

每次我想重写提交以仅保留差异的一部分时,我都会经历这个(从我的脑海中浮现出来,有些东西可能会消失):

git rebase -i COMMIT~1
Run Code Online (Sandbox Code Playgroud)

然后选择e修改。

git reset HEAD~1  
git add -p # select the part I want
git checkout HEAD -- :/ # discard changes not selected
git stash
git reset COMMIT
git checkout HEAD~1 -- :/
git commit -u --amend
git stash pop
git commit --amend
git rebase --continue
Run Code Online (Sandbox Code Playgroud)

显然,这是一个糟糕的工作流程。问题是重置允许我选择我想要的更改,但是通过将 HEAD 向后移动,我将无法修改相关提交。

我尝试git checkout HEAD~1 -- :/在提交时执行一次我想要修改的操作并选择反向差异,但它只是变得令人困惑。

有什么更好的选择?

tor*_*rek 3

这里要非常具体(如“我完全执行了这些命令并得到了这个输出”),这一点很重要,因为有很多移动部件。有很多单独的事情需要跟踪。

\n\n

在这个答案中,我将相对快速地浏览所有各个部分。如果您确定您了解所有这些,请直接跳到最后一部分!

\n\n

提交、HEAD索引和工作树

\n\n

首先,请记住 Git 的核心就是提交。提交是安全的:它们被永久且无损地存储。1 因此,一旦您进行了提交,即使您告诉 Git 将其丢弃(例如,通过使用git reset. 其他项目的情况并非如此具体而言, Git不会保证工作树中的工作安全。(您的操作系统可能会也可能不会做出一些保证,例如,在 Mac 上您可以为此设置时间机器。)您塞入索引也称为临时区域,又称为缓存)的工作介于两者之间,但是最好也将其视为非永久性的。

\n\n

接下来,您应该记住,每个(跟踪的)文件在任何时候都有三个活动副本。2 其中两个采用特殊的仅 Git 格式。这三者是:

\n\n
    \n
  1. 当前提交中的版本,您可以使用它来查看git show HEAD:README(如果文件名为README);
  2. \n
  3. 索引/暂存区域中的版本,您可以使用git show :0:README;查看
  4. \n
  5. 当然,还有工作树中的版本,它是一个普通文件,而不是特殊的仅限 Git 的格式。
  6. \n
\n\n

普通文件是可读可写的,您可以在编辑器中查看和更改它。索引中的版本也是一种读/写:git show例如,您可以使用上面的内容查看它,并且可以使用 将工作树版本复制索引中git add。如果您这样做git add README,那就是一个简单的直接写入:工作树版本进入索引。

\n\n

当前(也称为HEAD)提交中的副本是只读的。你根本无法改变它。您只能更改哪个提交是当前提交(此时旧提交仍然存在,您可以继续使用它)。

\n\n
\n\n

1好吧,提交大多是永久性的\xe2\x80\x94,如果没有可以访问它们的名称,它们就会消失。它们是廉洁的,因为如果确实发生了某些事情,Git 会知道并且根本不会让你恢复提交……这是一种改进吗?不过,说真的,您可以从其他地方获取提交,或者通过其他方式检索所有未损坏的文件。

\n\n

2更准确地说,跟踪文件最多有三个副本。一个未追踪的文件是不在索引中(无论是否在当前提交中)但位于工作树中的文件。如果它不在索引中,则显然会删除三个副本中的一个。在这里,我们只关心跟踪的文件。

\n\n
\n\n

提交形成有向图

\n\n

因为每个提交记录其父级(或者对于合并提交,两个或多个父级),所以如果我们给出一些起始提交,我们可以绘制一个图表。我们可以使用当前分支名称找到起始提交,无论是 \xe2\x80\x94masterfeature/tall,例如名称HEAD所附加的 \xe2\x80\x94 。分支名称保存该分支的提示提交的原始哈希 ID。然后,tip 提交本身保存其父提交的原始哈希 ID,因此我们说分支名称指向该提交,而该提交指向其父提交,当然父提交也指向某个地方,给出:

\n\n
... <-parent <-tip <--branch(HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n\n

如果我们用单个大写字母甚至只是一个圆替换大而丑陋的哈希ID o,并且\xe2\x80\x94利用提交是只读的事实,因此永远不会改变\xe2\x80\x94不要打扰绘图正确地提交箭头,知道它们总是向后\xe2\x80\x94我们可以更简单地将其绘制为:

\n\n
...--o--o--o   <-- branch (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n\n

我们要保留分支名称的箭头,因为这些名称会移动

\n\n

进行新的提交git commit

\n\n

如果您以通常的方式进行新的提交,请运行git commitGit:

\n\n
    \n
  • 将索引中的所有内容打包到树中对象中(保存在永久提交中);
  • \n
  • 使用其通常的元数据(您作为作者和提交者、您的日志消息等,以及当前提交作为新提交的父级)进行新提交)\xe2\x80\x94 进行新提交,这允许 Git 计算该提交的哈希 ID新提交;
  • \n
  • 使提交成为您当前的提交提交(以便现在您的提交和索引匹配)。
  • \n
\n\n

如果我们从一个只有三个提交的小型存储库开始,所有提交都在唯一的分支上,master那么我们从以下内容开始:

\n\n
A--B--C   <-- master (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n\n

要添加新的提交D,我们让它指向C,然后更改名称master,使其指向D

\n\n
A--B--C--D   <-- master (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n\n

这里的关键是要记住,索引中的任何内容都将进入您所做的下一个提交。它将当前提交作为其父提交。在本例中,名称master会发生​​更改以记住新提交,因此标识HEAD更改的提交将成为具有新哈希 ID 的新提交。旧的提交仍然存在,只是不存在了HEAD

\n\n

现在我们可以看看git reset,这是一个令人惊讶的复杂命令。我们将具体看看git reset它的拼写形式git reset HEAD~1形式。

\n\n

git reset移动你的头

\n\n

这里的参数git reset是提交哈希,或者指定提交哈希的东西。该名称HEAD表示当前提交:具体来说,找到所附加的当前分支名称HEAD,然后找到它指向的提交。 后缀~1表示返回一个父链接。 因此,如果HEAD附加到mastermaster指向提交,则从toD返回一个。DC

\n\n

然后该命令更改当前分支 \xe2\x80\x94(我们刚刚确定为master\xe2\x80\x94),以便它指向该特定提交。提交本身会发生什么?我们之前已经说过:(还)没有。它只是失去了它的名字

\n\n
        D   [no name - abandoned]\n       /\nA--B--C   <-- master (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n\n

我们现在可以使用名称master来查找 commit C,后者查找B,后者查找A(这是一个提交\xe2\x80\x94,意味着它没有父项\xe2\x80\x94,因此一切都停止在这里)。我们找不到D更多的东西,所以它现在不受保护,最终会被回收;但默认情况下它至少在 30 天内是安全的,在此期间,如果我们有某种方法来识别它,例如在某处写下原始哈希 ID,我们仍然可以使用它。

\n\n

git reset 重置索引

\n\n

当我们跑到git reset上面时,它不只是移动master。它还重置索引。也就是说, before git reset HEAD~1,索引匹配 commit D,因为我们D 索引进行了提交。之后git reset HEAD~1,索引现在与 commit 匹配C。那是因为git reset重新设置了它!Git 从新文件中复制特殊的仅 Git 格式的文件HEAD到索引中(记住,索引以特殊的仅限 Git 格式保存文件)。

\n\n

我们可以告诉git reset 不要重新设置索引,使用--soft. 如果我们这样做,索引将继续匹配提交D(仍然存在,但我们无法从C\xe2\x80\x94 到达它,连接提交的那些行都只能以一种方式,向后)。

\n\n

git reset默认情况下,保持工作树不变

\n\n

每个文件有三个副本,git reset默认情况下更改了其中两个。第三个副本位于工作树中:git reset默认情况下,单独保留第三个副本。

\n\n

我们git reset也可以使用 来重新设置工作树--hard。如果我们这样做,Git 将在修改索引的同时修改工作树。当它将特殊的仅 Git 格式的文件复制到索引以使它们匹配提交时C,它也会将这些文件提取到工作树中。

\n\n

概括:git reset很复杂;它最多可以做三件事

\n\n

运行这种类型git reset(还有更多种类!\xe2\x80\x94,但我们暂时忽略它们)最多可以做三件事,在一项、两项或全部三项之后停止:

\n\n
    \n
  1. 首先,它移动当前分支。如果告诉了--soft,它现在就停止了。
  2. \n
  3. 然后,它重置索引。如果告诉--mixed或不告诉任何事情,就到此为止。
  4. \n
  5. 第三,如果被告知--hard,它会重置工作树(其中的跟踪文件),同时在步骤 2 中重置索引。
  6. \n
\n\n

就我们这里的目的而言,我们主要需要方法 1 --soft:保留索引和工作树;或方法 2,--mixed/ 默认:保留工作树。

\n\n

关于git add -pgit reset -p

\n\n

我们上面注意到,git add在文件上运行只是将工作树版本复制到索引中。这非常简单,但是如果您不想要这样怎么办?当然,您可以复制工作树中的文件,然后将其编辑为您想要添加的形状,然后添加:

\n\n
$ cp README README.save\n$ vim README\n$ git add README\n
Run Code Online (Sandbox Code Playgroud)\n\n

之后您可以将保存的版本放回去,因为索引现在具有您想要提交的版本:

\n\n
$ mv README.save README\n$ git commit\n
Run Code Online (Sandbox Code Playgroud)\n\n

但有一条捷径。您可以使用它git add -p修补索引版本。

\n\n

git add -p在幕后,要做的是区分索引版本和工作树版本。然后,对于每个“差异块”,Git 都会向您显示该块并询问您是否要应用它。如果是这样,Git 将索引版本提取到临时文件中,应用补丁,并将临时文件(而不是工作树版本)复制回索引中。这具有修补索引版本的效果,而不是复制批发索引版本。当您应用各个补丁时,索引版本会越来越接近工作树版本。(如果你是埃利亚的芝诺,你永远无法到达那里。

\n\n

您可以对 执行相同的操作git reset -p,它执行相同类型的 diff,但这次将索引版本与版本进行比较HEAD。当您应用更改时,索引版本会越来越接近实际版本HEAD

\n\n

精挑细选提交

\n\n

Git 中有一个悖论:从真正的意义上来说,提交是保存的快照\xe2\x80\x94,它是保存的索引内容,正如我们一直在这里指出的那样。但是当您git show提交时,您会将其视为补丁

\n\n

其工作方式很简单:由于每次提交都会记录其父级(也是快照),因此 Git 可以简单地将父级与子级进行比较。无论发生什么变化,这就是补丁。

\n\n

实际上git cherry-pick,是将现有提交转换为补丁,然后将补丁应用到当前HEAD提交以进行新提交。3这具有复制所选提交 的效果,而不仅仅是获取其快照:Git 获取更改并将更改应用新的基础。

\n\n
\n\n

3 Git 实际上使用其内部合并机制的全部功能来应用更改,因此它不仅仅是一个简单的补丁,但这是考虑它的简单方法。出于好奇,它是一个三向合并,其中所选择的提交的父级用作合并基础,所选择的提交作为提交--theirs,并且HEAD作为--ours提交。

\n\n
\n\n

变基 = 重置 + 重复选取

\n\n

Git 中的变基,无论是否交互式,都包括从 a 开始git reset --hard,然后执行一系列重复的cherry-pick 操作。4 Git 使用“分离 HEAD”模式技巧,因此当前(正在重新基化)分支的分支名称在 rebase 完成之前实际上不会移动,但原则上,rebase 是这样做的:

\n\n
    \n
  1. 枚举要复制的所有提交:

    \n\n
                 D--E--F   <-- feature (HEAD)\n            /\n...--o--A--B--C   <-- mainline\n
    Run Code Online (Sandbox Code Playgroud)\n\n

    这里,我们可能会选择D通过复制F。(请注意,Git 无法轻松且明智地挑选合并进行变基,因此变基通常只是将它们完全丢弃。)

  2. \n
  3. 重置(并分离)到要复制的第一个提交,例如C. 这个特殊的重置是git reset --hard:它也会重置工作树。

  4. \n
  5. 一次一个地挑选每个要复制的提交。如果在挑选过程中发生合并冲突,请停止并向用户寻求帮助。

  6. \n
  7. 在此过程结束时,将分支名称移动到指向最终复制的提交,此时重新附加 HEAD。原件现在没有正确的名称(但ORIG_HEAD为了方便起见,rebase 会指向它们):

    \n\n
                 D--E--F   [ORIG_HEAD]\n            /\n...--o--A--B--C   <-- mainline\n               \\\n                D\'-E\'-F\'  <-- feature (HEAD)\n
    Run Code Online (Sandbox Code Playgroud)\n\n

    其中\'标记表示这些是副本。

  8. \n
\n\n

交互式变基只是在步骤 2 中开始复制之前添加一个编辑器会话。每个pick命令都告诉 Git 使用提交哈希 ID 进行实际的挑选。如果更改pickedit,Git 将在选择后停止,即使不存在合并问题。如果您将 更改pickreword,Git 会运行cherry-pick 来--edit让您更改提交消息。还有一些特殊情况(squash 和 fixup),这里不再赘述;您可以通过重新排列命令来重新排列每个选择的顺序pick。但从根本上来说,每个提交副本都是精心挑选的。

\n\n
\n\n

4有些 rebase 命令确实会运行git cherry-pick,有些则不会。对于大多数提交来说,这并没有什么区别。只有当git apply -3只有当可能错误应用而不会退回到三向合并构建一个这样的例子并不简单。

\n\n
\n\n

使用git commit --amend

\n\n

上面我们描述了正常的提交过程:

\n\n
A--B--C   <-- master (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n\n

您进行一些更改,使用 复制它们到索引中git add,然后运行git commit,Git 创建新的提交D作为C其父级并master指向D

\n\n
A--B--C   <-- master (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n\n

然而,我们可以告诉git commit使用,不是C,而是使用C父母git commit --amend。这会像以前一样打包索引,但随后会创建D指向 commit 的新提交B

\n\n
     C   [abandoned]\n    /\nA--B--D   <-- master (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n\n

由于 Git是通过从当前分支指向的分支开始查找D提交的,因此如果我们现在查看一组提交,我们将看到、then B、then A。就好像我们以某种方式将提交更改C为提交D\xe2\x80\x94,但我们没有。

\n\n

请注意,这与往常一样使用索引和工作树,因此我们可以将其与交互式变基结合起来。

\n\n

使用交互式变基来构建新的历史

\n\n

假设我们从这个提交链开始:

\n\n
...--F--G--H--I   <-- branch (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n\n

我们在 commit 中做了一些错误的事情G,或者想要拆分它,或者以其他方式做了一些事情。我们可以找到 commit 的 ID F,或者标识 commit 的东西F,然后git rebase -i用它来运行。

\n\n

最容易识别的F是使用HEAD~3,因为~3后缀倒数三:I(0), H(1), G(2), F(3)。所以我们运行:

\n\n
git rebase -i HEAD~3\n
Run Code Online (Sandbox Code Playgroud)\n\n

并获取三个pick命令,这将复制G、then H、then IafterF给我们:

\n\n
       F--G--H   [abandoned]\n      /\n...--E--F\'-G\'-H\'  <-- branch (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n\n

我们至少将第一个更改pickedit,以便 Git 进行挑选然后停止,留下该图的中间版本:

\n\n
       F--G--H   <-- branch\n      /\n...--E--F\'  <-- HEAD\n
Run Code Online (Sandbox Code Playgroud)\n\n

(这是特殊的“分离头”模式)。索引和工作树当前匹配提交F\'

\n\n

我们做错了一些事情\xe2\x80\x94也许太多了,我们想将它拆分\xe2\x80\x94in F\',所以现在我们可以用来git reset -p交互地撤消索引中的事情。或者我们可以运行git reset --mixed HEAD~1丢弃 F\'同时保持工作树更改,然后运行git add -p。我们必须记住我们做了哪一个,因为一个丢弃了 F\'另一个保留它。

\n\n

假设我们保留F\'一会儿,使用git reset -p. 我们稍微改变一下索引。然后我们可以运行git commit --amend一个新的提交,我们称之为F\'\',这就像FandF\'但使用现在索引中的任何内容。因为我们添加--amendgit commit,所以我们将F\'\'s 父级设置为E,而不是F\'

\n\n
       F--G--H   <-- branch\n      /\n...--E--F\'\'  <-- HEAD\n      \\\n       F\'  [abandoned]\n
Run Code Online (Sandbox Code Playgroud)\n\n

或者,假设我们丢弃F\',使用git reset --mixed

\n\n
       F--G--H   <-- branch\n      /\n...--E   <-- HEAD\n      \\\n       F\'  [abandoned]\n
Run Code Online (Sandbox Code Playgroud)\n\n

现在我们需要git add -p将工作树的更改累积到索引中。完成后,我们运行git commit(不带--amend)并进行新的提交F\'\'

\n\n
       F--G--H   <-- branch\n      /\n...--E--F\'\'  <-- HEAD\n      \\\n       F\'  [abandoned]\n
Run Code Online (Sandbox Code Playgroud)\n\n

无论哪种情况,我们现在都准备好再次运行git add,以从工作树中进行更多更改。让我们也提交它,并调用此提交,I因为刻度线变得很愚蠢:

\n\n
       F--G--H   <-- branch\n      /\n...--E--F\'\'-I  <-- HEAD\n      \\\n       F\'  [abandoned]\n
Run Code Online (Sandbox Code Playgroud)\n\n

现在我们可以运行git rebase --continueGitcherry-pick GtoG\'Hto H\'

\n\n
       F--G--H   <-- branch\n      /\n...--E--F\'\'-I-G\'-H\'  <-- HEAD\n      \\\n       F\'  [abandoned]\n
Run Code Online (Sandbox Code Playgroud)\n\n

一旦所有提交都完成,Git 将移动并重新附加分支名称,我们有:

\n\n
       F--G--H   [ORIG_HEAD]\n      /\n...--E--F\'\'-I-G\'-H\'  <-- branch (HEAD)\n      \\\n       F\'  [abandoned]\n
Run Code Online (Sandbox Code Playgroud)\n\n

这就是我们想要的。

\n