【问题标题】:Is it possible to stage multiple sets of changes separately in git?是否可以在 git 中分别进行多组更改?
【发布时间】:2018-05-08 11:57:05
【问题描述】:

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

如果我对一个项目进行了很多更改(多个文件中的多个更改),我经常遍历所有大块(使用 git gui)并决定我想要一起暂存的部分,然后将几个大块与一个一起提交提交和描述性评论。然后我继续遍历所有剩余的大块,再次将大块组合成一个共同的提交。

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

问题:有没有更好的方法?

我希望能够通过这些大块头,将它们一一上演到不同的场景中。当一切都准备好后,循环通过临时集,并用适当的注释一一提交。我还想在搭建舞台布景时积累/编辑评论。

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

一种可能性可能是允许将单个大块附加到现有提交,然后重新设置以后的提交,但不知何故使工作区和暂存区有效地保持不变。

这个问题类似于How to create multiple stages in GitBreak up multiple changes into separate commits with git?,但这些讨论并没有真正充分回答我的问题。是的,这是可能的,但似乎必须/应该有更好的方法。

【问题讨论】:

  • 我承认您问的问题并不完全相同,但是我所链接的问题的答案是解决您问题的最佳方法。
  • 请原谅我这么说,但这听起来像是一个非常复杂的工作流程。您真的需要这种级别的粒度吗?
  • 嗨蒂姆,我问这个问题的原因确实是因为我想简化工作流程。实际上这种情况经常出现。假设我正在编辑代码,编辑几个函数以修复错误。在进行编辑时,我注意到不相关的 cmets 有错误,所以我修复了它们。编辑函数意味着编辑定义、调用站点、测试、文档等。现在,我想分别提交每个函数;这样的提交包括该函数的定义、调用站点、文档和测试,但仅此而已。首先找到 foo1 的更改,然后提交它们,然后是 foo2,然后是 foo3(哎呀,我忘了 foo1 的东西)

标签: git


【解决方案1】:

我能找到的最近的是https://github.com/ustramooner/gitstage

在本博文中介绍:http://ben.villagechief.com/2012/01/multiple-git-staging-areas.html

我使用的工作流场景一般是这样的

  1. 运行 git gui
  2. 意识到我需要将提交拆分为 2...duhhhh
  3. 关闭 git gui 而不提交
  4. 运行 gitstage commit1,它会创建一个名为“commit1”的暂存区,然后再次打开 git gui
  5. 记下到目前为止我在 git gui 提交消息文本框中所展示的内容
  6. 关闭 git gui。
  • gitstage 现在将创建当前暂存索引的提交
  • gitstage 还将您在 git gui 中编写的任何消息放入 .git/gitstage/commit1.msg 7 运行 gitstage commit2,它会创建 另一个暂存区并打开 git gui。
  1. 在 gui 中将其他文件/块/行添加到新的暂存区域
  2. 多次重复步骤 4-8。每次交换您写的消息都会保留
  3. 提交:在 gui 中单击 commit 并关闭 gui
  • 这会移除暂存区
  • 提交任何其他剩余的暂存区域
  • 工作流程结束

【讨论】:

    【解决方案2】:

    简短的回答是“不,没有更好的方法”——但您或许可以尝试使用git worktree add。 (这会让你遇到一个不同的问题,但它可能毕竟不是问题。)

    问题是只有一个 index,1 Git 称之为 the 索引,或者有时是暂存区 或 缓存。同时,任何提交,一旦做出,就永远无法改变。甚至git commit --amend 也没有更改提交(我们稍后会谈到)。


    1这并不完全正确。特别是,如果您使用git worktree add,您将获得一个索引每个工作树。 Git 还允许各种命令使用临时索引文件;像git stash 之类的东西,甚至是更复杂的git commit,使用它是因为git write-tree 总是使用索引作为它们的输入,但可以指向一个临时索引。不过,实际上使用临时索引太棘手了。


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

    想象一个只有一个文件README的非常简单的存储库。您克隆存储库并位于master。情况如下:

    HEAD      index    work-tree
    ------    ------    ------
    README    README    README
    

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

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

    HEAD      index    work-tree
    ------    ------    ------
    README    README    README;1
    

    但是,您进行了重大更改,因此您决定使用git add -p 或其他方式以交互方式添加一些更改。一旦你这样做了,你就会得到 this:

    HEAD      index    work-tree
    ------    ------    ------
    README    README;2  README;1
    

    也就是说,从字面上看,现在您正在同时处理该文件的三个不同版本:您无法更改的已提交版本;您更改的工作树;和索引一,您创建为 HEAD 和工作树版本的科学怪人混合体。

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

    HEAD      index    work-tree
    ------    ------    ------
    README;2  README;2  README;1
    

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


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


    In a comment,mkrieger1 链接到a question where the answers suggest using the fixup and autosquash features of git rebase, including using git commit --fixup to record a commit for autosquashing。这些,比如git commit --amend,利用了 Git 分支名称的一个非常有用的属性。

    Git 中的分支名称指向恰好一个提交。分支中包含的一组提交是通过从一个提交开始确定的在。每个提交都存储在它的大丑提交哈希 ID 下:分支名称包含提示提交的哈希 ID,每个提交包含其父提交的哈希 ID:

    ... <--E <--F <--G   <-- master
    

    我们说master“指向”提示提交G,它又指向F,依此类推。

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

    ...--E--F--G   <-- master
    

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

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

             H   <-- master
            /
    ...-E--F--G
    

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

    这就是您使用git commit --amend 得到的结果:新提交只是具有作为其父级的当前(嗯,现在是前当前)提交具有的父级。

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

    ...--E--F--G--H--I--J   <-- branch
    

    其中JH 的修正。现在你运行git rebase -i --autosquash &lt;hash of G&gt;,Git 会生成命令:

    pick <hash-of-H>
    pick <hash-of-I>
    pick <hash-of-J>
    

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

                 H'-I'-J'   <-- branch
                /
    ...--E--F--G--H--I--J   [abandoned]
    

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

    pick <hash-of-H>
    fixup <hash-of-J>
    pick <hash-of-I>
    

    执行这些指令会给出:

                 HJ--I'   <-- branch
                /
    ...--E--F--G--H--I--J   [abandoned]
    

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


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

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

    毕竟这可能不是问题。请记住,在 Git 中,分支非常便宜:创建一个 new 分支只需要一个磁盘块,包含一个 41 字节的文件。 (未来的实现可能会改变这些细节,但分支仍然会非常便宜。)

    让我们回到这张图:

    ...--E--F--G--H--I--J   <-- branch (HEAD)
    

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

    ...--E--F--G--H--I--J   <-- branch (HEAD), br2
    

    我们现在可以随意添加、复制、变基或其他任何内容。新的提交完全是只读的并且大部分是永久的,它们是安全的,并且总是与旧的提交分开。新名称 br2 可以安全地保留原始提交,无论我们如何处理它们。或者,我们可以将 HEAD 切换为 br2 并让旧名称 branch 保持原始提交的安全:

    ...--E--F--G--H--I--J   <-- branch, br2 (HEAD)
    

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

    ...--E--F--G--H--I--J   <-- branch
                \
                 HJ--I'   <-- br2 (HEAD)
    

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

    Git 禁止您拥有两个使用 same 分支的工作树,因为这样它们都将指向同一个提交,但又有两个索引文件和两个工作树。当您在其中一个中进行新提交时,它将使用该索引中的索引,并将 shared 分支更改为指向新提交。结果是这两个中的另一个仍然具有旧的(现在陈旧的)索引和旧的(现在陈旧的)工作树。 Git 作者认为这太令人困惑,并简单地取缔了它。 因为分支是如此便宜,这实际上是相当合理的:只需为每个新工作树创建一个新分支。

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

    【讨论】:

      猜你喜欢
      • 2020-03-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-03-16
      • 2015-03-04
      • 1970-01-01
      • 2014-10-19
      • 1970-01-01
      相关资源
      最近更新 更多