我正在寻找一种使用 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
命令应压缩提交
需要明确的是,这是我正在寻找的:
- 由 git 变基给出的干净线性历史记录
- 从功能分支进行压缩提交,无需不必要的交互性
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
用于各种功能,例如die
和require_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 rebase
和git 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
复制到从Z
A'-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
请注意,执行此操作时不应有未提交的页面或未跟踪的文件。
以下是它的工作原理:
checkout dev
将更新HEAD
、索引(暂存区域)和工作树,以匹配存储库在dev
的状态,reset --soft master
不会影响索引或工作树(这意味着暂存区域中的所有文件都将准确反映存储库在dev
的状态),但会指向HEAD
master
和commit
将提交您的索引(对应于dev
)并向前master
。
&&
可确保脚本在出现任何故障时不会继续。