Squash Git 使用 git rebase 以非交互方式提交



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

例如:

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 进行任何更改,我不确定这个脚本是否会做任何事情。但它绝对不会做的是壁球提交。无论如何,我可以以某种方式压制在开发分支上进行的所有提交,这些提交不在主中?

也许最好的办法是变基,然后遵循:

git reset --soft ${SHARED}

reset --soft命令应压缩提交

需要明确的是,这是我正在寻找的:

  1. 由 git 变基给出的干净线性历史记录
  2. 从功能分支进行压缩提交,无需不必要的交互性

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

git merge --squash执行此操作

我在下面用一种首先使用git rebase的方法回答了这个问题,然后是随后的git reset --soft ... && git commit序列。 有关详细信息,请参阅此答案的该部分。 但是,您可以在单个 Git 命令中完成所有"变基和挤压"工作,因为git merge --squash执行您想要的操作:它执行合并操作(合并为动词),而无需进行合并(合并为名词:合并提交)。(您仍然需要一些额外的设置和完成命令 - 最后总共有五个命令,除非您想将其作为脚本尝试。2)

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

...--o--*---------Z   <-- master

A--B--C     <-- dev

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

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

A--B--C      [abandoned]

实际上,提交D是将A--B--C序列转换为变更集以应用于提交*,然后使用三向合并应用该变更集以*作为其合并基础来提交Z时得到的。 当然,这正是普通git merge会做的事情。

问题是常规合并会进行合并提交,从而提供:

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

我们确实想要提交D,但我们希望D是一个普通的单亲提交。 这就是git merge --squash闪耀的地方。 这正是git merge --squash所做的:它合并(动词),但随后进行普通提交——或者更确切地说,让我们进行提交(我们必须运行自己的git commit命令)。

不过,我们还想要别的东西:我们希望D成为分支dev的提示提交,分支master不移动。 我小心翼翼地从上面的合并图中归档了分支名称,因为如果我们以常规方式进行常规合并,我们将在分支master时执行此操作,然后提交D最终会在分支master上结束。 这不是我们想要的。

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

我们首先检查提交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

得到一个"分离的头"。 其他一切都像以前一样工作,因为分离的 HEAD 充当匿名分支。 当我们在git merge --squash之后运行git commit时,我们所做的新提交会在这个未命名的分支上进行提交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

与执行两部分的"先git rebase,然后git reset"序列相比,这样做的好处是,当您执行变基时,它复制了一串提交,您可能会在每个复制的提交上出现合并冲突。如果是这样,您可能也会在 squash 合并中遇到合并冲突 - 但是使用 squash 合并,您将有一个大冲突需要解决一次,而不是多次小冲突一次解决一个提交。

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


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

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

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

2这是一个未经测试的脚本,它只接受一个参数,即要变基和挤压的分支。 要使用脚本,您必须位于要变基和挤压的分支,即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 "$1") || 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做到这一点

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

我正在寻找一种使用 git 变基压制提交的方法......

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

但非交互式。我的问题是,我们可以使用 git merge-base 来找到要"合并到"的共同祖先,然后压制所有未共享的提交吗?

是的。

您的必要步骤(由git rebase执行的步骤)是:

  • 查找合并库或以其他方式标识要复制的提交;
  • 做任何你想复制的事情;和
  • 相应地移动分支标签。

要理解我们需要的操作(以及命令),首先绘制图形(或其有趣的部分):

...--o--*           <-- master

A--B--C   <-- dev

在这里,名称master指向合并基提交。 (我在这里使用了您上面显示的git init; ...序列的轻微变体,其中包含一个在合并基础提交之前至少有一个提交的现有存储库。 不过,让我们通过在master上进行提交来使此图更有趣:

...--o--*--Z        <-- master

A--B--C   <-- dev

最后,您可能希望图形如下所示:

...--o--*--Z        <-- master

D         <-- dev

其中D是组合A-B-C的结果,就像git rebase -i一样,如果您在将除第一个pick之外的所有更改为squash,同时将合并基础提交*

。请注意,此新提交D保存快照及其新提交消息(无论是什么)与为提交C保存的快照相同。 也就是说,组合A-B-C本质上与忘记A和图形B相同,同时保留他们引入的代码,以便D树与C树匹配。

现在,如果master上没有提交Z,您可以使用Pockets 的答案执行此操作。 原因是,如果你坐在提交C上,索引和工作树与C匹配,并且你这样做:

git reset --soft master

这将按原样保留索引和工作树,但将图形更改为:

...--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(非交互式)将A-B-C复制到从ZA'-B'-C',给出:

...--o--*--Z        <-- master

A'-B'-C'  <-- dev

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

以下内容将在master时将dev树放入新的提交中(注意:这不等同于变基,正如@torek下面善意指出的那样):

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

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

以下是它的工作原理:

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

&&可确保脚本在出现任何故障时不会继续。

最新更新