Squash Git 使用 git rebase *非交互*提交

Squash Git commits *non-interactively* with git rebase

我正在寻找一种方法来压缩使用 git rebase 的提交,但不是交互式的。我的问题是,我们可以使用 git merge-base 找到 "merge down to" 的共同祖先,然后压缩所有未共享的提交吗?

例如:

git init
# make changes
git add . && git commit -am "first"
git checkout -b dev
# make changes to dev 
# *(in the meantime, someone else makes changes to remote master)*
git add . && git commit -am "second"
SHARED=$(git merge-base dev master)
git rebase ${SHARED}

以上,因为在此期间没有对 master 进行更改,所以我不确定这个脚本是否会做任何事情。但它肯定 不会 做的是压缩提交。无论如何,我是否可以以某种方式压缩在 dev 分支上所做的所有提交,而不是在 master 中?

也许最好的办法是变基,然后跟随它:

git reset --soft ${SHARED}

reset --soft 命令应该压缩提交

明确地说,这是我要找的东西:

  1. 由 git rebase
  2. 给出的干净的线性历史
  3. 在没有不必要交互的情况下压缩来自功能分支的提交

git rebase -i 使用交互性,这是我想避免的

以下将把 dev 树放入 master 的新提交中(注意:这 等同于变基,因为 @torek请在下方指出):

git checkout dev         && \
git reset --soft master  && \
git commit

请注意,执行此操作时不应有未提交的页面或未跟踪的文件。

工作原理如下:

  1. checkout dev 将更新 HEAD、您的索引(暂存区)和工作树以匹配 dev
  2. 的存储库状态
  3. reset --soft master 不会影响您的索引或工作树(这意味着您暂存区中的所有文件将准确反映 dev 处的存储库状态),但会指向 HEADmaster
  4. commit 将提交您的索引(对应于 dev)并向前走 master

&&确保脚本在任何失败时都不会继续。

git merge --squash

做这个

我在下面用先使用 git rebase 的方法回答了这个问题,然后是后续的 git reset --soft ... && git commit 序列。有关详细信息,请参阅此答案的那一部分。但是,您可以在单个 Git 命令中完成所有 "rebase-and-squash" 工作,因为 git merge --squash 可以满足您的要求:它执行 合并操作 (作为动词合并),而不进行 a merge(作为名词合并:a merge commit)。 (你还需要一些额外的设置和完成命令——总共有五个命令,最后,除非你想把它当作脚本来尝试。2

让我们从示例输入图开始:

...--o--*---------Z   <-- master
         \
          A--B--C     <-- dev

现在,您的最终目标(请参阅其他部分了解详细信息)如下所示:

...--o--*---------Z      <-- master
         \         \
          \         D    <-- dev
           \
            A--B--C      [abandoned]

其中提交 D 实际上是将 A--B--C 序列转换为 变更集 以应用于提交 *,然后应用该变更集,使用 three-way-merge,以 * 作为其 合并基础 提交 Z。当然,这正是常规 git merge 会做的。

问题是常规合并会产生 合并提交,给出:

...--o--*---------Z
         \         \
          \         D
           \       /
            A--B--C

我们确实想要提交D,但我们希望D成为普通,single-parent,提交。这就是 git merge --squash 的亮点。这正是 git merge --squash 所做的 :它合并(动词)但随后进行普通提交——或者更确切地说,使 us 进行提交(我们必须 运行 我们自己的 git commit 命令)。

不过,我们还想要其他东西:我们希望 D 成为分支 dev 的尖端提交,分支 master 未移动。我小心地从上面的合并图中归档了分支 names,因为如果我们以常规方式进行常规合并,我们会在分支 master 上进行合并,并且commit D 然后会在分支 master 上结束。那是不是我们想要的。

现在,有一个非常简单的规则 git commit 在进行新提交时使用:新提交在 当前分支 上进行。因此,假设合并已经完成但还没有提交,我们如何才能让 Git 在正确的分支上进行新的提交 D,即 dev?嗯,我们不能。1 相反,我们应该 squash-merge 在 临时分支 上提交 D

我们首先检查 commit Z,master 的提示,作为一个新的临时分支:

git checkout -b temp-for-squash master

现在我们 运行 git merge --squash dev,用将成为提交的合并结果设置我们的索引 D:

git merge --squash dev

现在我们提交结果,因为--squash意味着--no-commit:

git commit

此时的结果如下所示:

...--o--*---------Z      <-- master
         \         \
          \         D    <-- temp-for-squash (HEAD)
           \
            A--B--C      <-- dev

现在我们要做的就是让标签 dev 指向 temp-for-squash,这就是我们现在所在的位置。我们可以git checkout dev; git reset --hard temp-for-squash做到这一点,但我们可以一步完成:

git branch -f dev HEAD

我在这里特意使用了名称 HEAD,因为——好吧,看看我们在哪里使用了名称 temp-for-squash。我们只在 git checkout 命令中使用过一次。而且,现在我们已经移动 dev,我们不再 需要 temp-for-squash 名称。所以我们根本不用它——让我们把上面的 git checkout 改成:

git checkout --detach master

得到一个"detached HEAD"。其他一切都像以前一样工作,因为分离的 HEAD 充当 anonymous 分支。我们所做的新提交,当我们 运行 git commitgit merge --squash 之后,在这个未命名的分支上进行提交 D,更新 HEAD。然后我们用git branch -f dev移动dev.

最后,我们可能想要 运行 git checkout dev,这样我们就在 dev 分支上(但我们现在可以 git checkout 任何你喜欢的分支名称) .因此,最终的命令序列是:

git checkout --detach master
git merge --squash dev
git commit
git branch -f dev HEAD
git checkout dev

这样做的好处,比 two-part "first git rebase, then git reset" 序列是,当你 变基时,它会复制一个提交链,您可能会在 each 复制的提交上遇到合并冲突。如果是这样,您可能也会在 squash merge 上遇到合并冲突——但是对于 squash merge,您将有一个大冲突需要一次解决,而不是一次解决许多小冲突。

这也是dis这样做的好处。一次提交一个,解决许多小冲突可能更容易。或者一次解决一次大冲突可能更容易。这是您的存储库,这些是您的提交;你来决定。


1实际上至少有两种方法可以做到,但它们都涉及各种作弊:使用脚本命令。最简单的就是直接用git symbolic-ref改写HEAD。我会在这里提到它,但留下解决细节,即为什么它有效,作为练习:

git checkout --detach master
git merge --squash dev
git symbolic-ref HEAD refs/heads/dev
git commit

下一个脚注中有第二种方法。

2这是一个未经测试的脚本,它只接受一个参数,即rebase-and-squash所在的分支。使用您必须 您打算 rebase-and-squash 的分支上的脚本,即 dev。这有点作弊,dev 头的先前值将只是 ORIG_HEAD 而不是 dev@{1},因为我们更新了 dev 几次,如果合并需要帮助,我们将强制用户(即您)执行 git commit.

注意:我对各种函数使用 git-sh-setup,例如 dierequire_clean_work_tree。这意味着您必须使用 git 前端调用此脚本,即将它放在您的 $PATH 中的某处,命名为 git-rebase-and-squash,然后是 运行 git rebase-and-squash.

#! /bin/sh

self="rebase-and-squash"

. git-sh-setup

[ $# = 1 ] || die "usage: $self <branch-or-commit>"

require_clean_work_tree $self

source=$(git rev-parse HEAD) || exit 1
target=$(peel_commitish "") || exit 1

# Everything looks OK.  Set ORIG_HEAD, then move current branch
# to target commit.  We'll rely on ORIG_HEAD to protect the chain.
set_reflog_action $self
git update-ref ORIG_HEAD $source
gut reset --hard $target
if git merge --squash $current_branch; then
    # merge succeeded without assistance: do the commit
    git commit
else
    status=$?
    echo "merge failed - finish the merge and run 'git commit',"
    echo "or use 'git reset --hard ORIG_HEAD' to abort."
    exit $status
fi

使用 git rebasegit reset --soft

对于您最初描述的情况是正确的(并且赞成)。不过,您可能需要一个解释,尤其是因为各种命令的参数可能需要在其他设置中进行一些调整,例如您似乎正在瞄准的更一般的情况。

I am looking for a way to squash commits with git rebase ...

git rebase 所做的只是复制提交,然后将当前分支标签移动到指向 last-copied 提交。您没有理由必须使用 git rebase 本身来执行这些步骤,尽管在某些情况下,这样做会更容易。

but non-interactively. My question is, can we use git merge-base to find the common ancestor to "merge down to" and then squash all commits that are not shared?

是的。

git rebase执行的必要步骤是:

  • 找到合并基础或以其他方式确定要复制的提交;
  • 随心所欲地复制;和
  • 相应地移动分支标签。

要了解我们需要的操作(以及命令),请从绘制图形(或其中有趣的部分)开始:

...--o--*           <-- master
         \
          A--B--C   <-- dev

此处,名称master 指向合并基础提交。 (我使用的是您在上面显示的 git init; ... 序列的轻微变体,在这里,现有存储库至少有一个提交 before 合并基础提交。)让我们做通过在 master 上进行 new 提交,这张图会更有趣一些,不过:

...--o--*--Z        <-- master
         \
          A--B--C   <-- dev

您可能希望您的 最后看起来像这样:

...--o--*--Z        <-- master
         \
          D         <-- dev

其中 D 是组合 A-B-C 的结果,如果您在变基时将除第一个 pick 以外的所有内容更改为 squash,则 git rebase -i 会这样做到合并基础提交 *.

请注意,此新提交 D 已保存快照 及其新提交消息(无论是什么)与 相同 作为提交 C 的保存快照。也就是说,组合 A-B-C 本质上与 忘记 AB graph-wise 相同,同时保持 他们引入了 code,因此 D 的树与 C 的树相匹配。

现在,如果 master 上没有提交 Z,您可以使用 Pockets 的回答来完成。原因是,如果你正坐在提交 C 上,索引和 work-tree 与 C 匹配,你会:

git reset --soft master

这将保留索引和 work-tree,但将 graph 更改为:

...--o--*           <-- dev (HEAD), master
         \
          A--B--C   [abandoned]

然后 运行 git commit 时,您将在 dev 上进行新提交,这将是提交 D:

...--o--*           <-- master
         \
          D         <-- dev

你现在已经完成了。但是如果一个新的提交Z,事情就变了。

如果你想:

...--o--*--Z        <-- master
         \
          D      <-- dev

那么诀窍就是 git reset -soft 到合并基础提交 *,否则做同样的事情。但如果你想:

...--o--*--Z        <-- master
            \
             D      <-- dev

那么你需要做更多的工作。

在这种情况下,不写任何代码的最简单的解决方案可能是 git rebase (non-interactively) 首先将 A-B-C 复制到 A'-B'-C' ,从 Z,给出:

...--o--*--Z        <-- master
            \
             A'-B'-C'  <-- dev

现在您可以git reset --soft master && git commit像以前一样,从git rebase提交C'时创建的索引提交D