是否可以在 git 中分别暂存多组更改?

Jim*_*ton 12 git

这是一个关于我多年来使用 git 开发的经常使用的 git 工作流程的问题(请求建议)。

如果我对一个项目做了很多更改(多个文件中的多个更改),我经常会迭代所有的大块(使用 git gui)并决定我想要将哪些大块放在一起,然后通过一个提交和一个提交将多个大块一起提交描述性评论。然后我继续检查所有剩余的帅哥,再次将帅哥分组到一个共同的提交中。

但是经常出现的一个问题是,在第三次或第四次迭代时,我发现了一个应该在之前的提交之一中的块。在这种情况下,我暂时将暂存的更改提交到名为 TMP 的内容,提交有问题的块“修改”,然后继续暂存剩余的块,并使用消息“修改 TMP”进行评论。完成所有更改后,我返回并 rebase -i 并重新排序并适当地将“修改...”提交压缩在一起。

问: 有更好的方法吗?

我希望能够将这些帅哥一一地分成不同的场景。当所有内容都已暂存时,循环遍历暂存集,并通过适当的注释将它们一一提交。我还想在构建舞台布景时积累/编辑评论。

存在这样的功能吗?这叫什么?

一种可能性可能是允许将单个块附加到现有提交,然后对以后的提交进行变基,但以某种方式使工作区和暂存区实际上保持不变。

这个问题类似于如何在 Git 中创建多个阶段使用 git 将多个更改分解为单独的提交?但这些讨论并没有真正充分回答我的问题。是的,这是可能的,但似乎必须/应该有更好的方法。

tor*_*rek 6

简短的答案是“不,没有更好的方法”\xe2\x80\x94,但你git worktree add也许可以尝试一下。(这会让你遇到不同的问题,但它可能根本不是问题。)

\n\n

问题是只有一个索引1 Git 称之为索引,有时也称为暂存区缓存。同时,任何承诺一旦做出,就根本无法更改。连变化都没有git commit --amend 提交(我们稍后会讨论这一点)。

\n\n
\n\n

1这并不完全正确。特别是,如果您使用,您将获得每个工作树git worktree add一个索引。Git 还允许各种命令使用临时索引文件;诸如 之类的东西,甚至是极其复杂的 变体,都使用它,因为始终使用索引作为输入,但可以指向临时索引。但实际上使用临时索引会太棘手。git stashgit commitgit write-tree

\n\n
\n\n

当您使用git add -p或某些更高级的 GUI 来交互地选择特定更改(差异块或单独的行或其他内容)以添加到索引时,您将在索引中创建一个在其他地方不会出现的文件。

\n\n

想象一个非常简单的存储库,只有一个文件,README. 您克隆存储库并打开master. 情况是这样的:

\n\n
HEAD      index    work-tree\n------    ------    ------\nREADME    README    README\n
Run Code Online (Sandbox Code Playgroud)\n\n

所有三个副本README都是相同的(尽管秘密地,Git 的 HEAD 和索引版本被压缩,并且实际上共享底层磁盘存储,因此 README 的磁盘上只有两个映像,压缩的 Gitty 映像和未压缩的普通映像) )。

\n\n

现在您启动您最喜欢的编辑器并修改README. 我们称其README;1为具有可怕的语法2来识别“文件的不同版本”。:-) 现在你有这个:

\n\n
HEAD      index    work-tree\n------    ------    ------\nREADME    README    README;1\n
Run Code Online (Sandbox Code Playgroud)\n\n

但是,您做了一个很大的更改,因此您决定以交互方式添加一些更改的内容,使用git add -p或其他方式。一旦你这样做了,你最终会得到这样的结果:

\n\n
HEAD      index    work-tree\n------    ------    ------\nREADME    README;2  README;1\n
Run Code Online (Sandbox Code Playgroud)\n\n

也就是说,现在,从字面上看,您正在同时使用该文件的三个不同版本:您无法更改的已提交版本;以及您无法更改的已提交版本。您更改的工作树;和索引一个,你创建的弗兰肯斯坦混合体HEAD和工作树版本的弗兰肯斯坦式混合体。

\n\n

由于只有一个索引,因此只有一个位置可用于创建该文件的中间版本。所以你必须创建它,然后提交它。奉献创造提交,它会获取一个新的唯一哈希 ID,然后使该新提交成为 HEAD 提交,这样您现在就拥有了:

\n\n
HEAD      index    work-tree\n------    ------    ------\nREADME;2  README;2  README;1\n
Run Code Online (Sandbox Code Playgroud)\n\n

这释放了索引以创建文件的另一个新变体:第二个版本README被安全保存,完全不可更改(并且大部分是永久的),在HEAD

\n\n
\n\n

2事实上,这就是 VMS 中版本化文件的语法。

\n\n
\n\n

在评论中,mkrieger1 链接到一个问题,其中的答案建议使用 的修复和自动压缩功能git rebase,包括使用git commit --fixup记录自动压缩的提交。这些,比如git commit --amend,利用了 Git 分支名称的一个非常有用的属性。

\n\n

Git 中的分支名称恰好指向一次提交。分支中包含的一组提交是通过从该提交(Git 称为提示提交)开始并向后工作来确定的:该提交有一个父提交,父提交有另一个父提交,依此类推。每个提交都存储在其大而丑陋的提交哈希 ID 下:分支名称包含提示提交的哈希 ID,每个提交包含其父级的哈希 ID:

\n\n
... <--E <--F <--G   <-- master\n
Run Code Online (Sandbox Code Playgroud)\n\n

我们说master“指向”提示 commit G,它又指向F,等等。

\n\n

由于任何提交无法更改,因此内部箭头始终指向向后,并且实际上不需要绘制,这很方便,因为在 stackoverflow 上很难用 ASCII 来做好。:-) 所以我把它画成:

\n\n
...--E--F--G   <-- master\n
Run Code Online (Sandbox Code Playgroud)\n\n

(我将箭头保留在分支名称前面,因为分支名称确实会移动。)

\n\n

现在,这意味着,如果我们创建一个提交,其父级不是正常的“当前提交哈希 ID”,而是当前提交的父级,然后使当前分支名称指向我们刚刚创建的新提交,我们似乎已经替换了当前的提交:

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

我们实际上没有更改任何提交,但看起来就像我们运行时所做的那样git log,因为没有任何东西指向G,Git 不会显示它。Git 首先向我们显示新的提交H,然后返回到提交F,然后返回到E,依此类推。

\n\n

这就是你得到的git commit --amend:新提交只是将当前(好吧,现在是前当前)提交的父级作为其父级。

\n\n

git rebase命令将其提升到一个新的水平:我们可以复制许多提交,而不只是一次提交,并在进行过程中进行一些细微的更改。通过交互式 rebase,我们可以使用 Gitgit cherry-pick通过交互式变基,我们可以在每个要复制的提交上副本可以在您喜欢的任何提交之后进行,但对于像 autosquash 这样的事情,您通常会就地进行副本:

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

哪里J有 的修复程序H。现在你跑git rebase -i --autosquash <hash of G>Git 会生成命令:

\n\n
pick <hash-of-H>\npick <hash-of-I>\npick <hash-of-J>\n
Run Code Online (Sandbox Code Playgroud)\n\n

如果完全直接运行,将导致:

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

但 autosquash 功能并没有运行它们,而是注意到J它本身有一个前缀:作为其单行提交主题fixup! <subject><subject>这部分匹配H\ 的提交主题匹配,因此 autosquash 代码将指令更改为:

\n\n
pick <hash-of-H>\nfixup <hash-of-J>\npick <hash-of-I>\n
Run Code Online (Sandbox Code Playgroud)\n\n

执行这些指令会给出:

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

其中是使用\ 的提交消息HJ自动压缩的H+ 。JH

\n\n
\n\n

现在,这里的问题又是只有一个索引,而您正在该一个索引中构建中间图像。

\n\n

如果你使用git worktree add,你可以创建任意数量的工作树。每个都有自己的索引,当然还有自己单独的工作树。但是 Git 施加了一个非常强的约束:每个工作树必须位于不同的分支上。

\n\n

这也许根本就不是问题。请记住,在 Git 中,分支非常便宜:创建一个分支只需要一个磁盘块,保存一个 41 字节的文件。(未来的实施可能会改变这些细节,但分支机构将仍然便宜得离谱。)

\n\n

让我们回到这张图:

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

我们现在可以创建一个新分支,Git 所做的就是编写一个指向 commit 的文件J。这就是我们添加(HEAD)到绘图中的原因,以便我们知道我们的工作树正在使用哪个分支:

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

我们现在可以添加、复制、变基或任何我们喜欢的方式。新的提交完全是只读的并且大部分是永久的,是安全的并且始终与旧的提交分开。新名称br2可以安全地保留原始提交,无论我们如何处理它们。或者,我们可以切换HEADbr2并让旧名称branch保持原始提交的安全:

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

现在让我们H-I-J像以前一样做一些事情:

\n\n
...--E--F--G--H--I--J   <-- branch\n            \\\n             HJ--I\'   <-- br2 (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n\n

如果你创建一个新的工作树,你可以让它有一个新的分支。新工作树共享所有旧提交和所有旧分支。

\n\n

Git 禁止您拥有两个使用同一分支的工作树,因为这样它们就会都指向同一个提交,但仍然有两个索引文件和两个工作树。当您在其中之一中进行新提交时,将使用该提交中的索引,并将共享分支更改为指向新提交。结果是,这两个中的另一个仍然具有旧的(现在已经过时的)索引和旧的(现在已经过时的)工作树。Git 作者认为这太令人困惑了,所以干脆宣布它为非法。 因为分支是如此便宜,所以这实际上是相当合理的:只需为每个新工作树创建一个新分支。

\n\n

这里的优点是,您不仅拥有多个索引文件,还拥有多个工作树。您可以停止尝试使用每个文件的额外版本来玩索引文件技巧(git add -p):只需使工作树文件看起来像您想要的那样,然后测试它,然后提交它。所有这些都位于临时工作树中,如果它们不能很好地工作,那么它们可能是临时分支。如果效果很好,则使用最好的作为最终结果。一旦您满意,只需删除(rm -rf)所有较小的工作树和(git branch -D)“毕竟没有解决”临时分支。

\n