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
命令应该压缩提交
明确地说,这是我要找的东西:
- 由 git rebase
给出的干净的线性历史
- 在没有不必要交互的情况下压缩来自功能分支的提交
git rebase -i
使用交互性,这是我想避免的
以下将把 dev
树放入 master
的新提交中(注意:这 不 等同于变基,因为 @torek请在下方指出):
git checkout dev && \
git reset --soft master && \
git commit
请注意,执行此操作时不应有未提交的页面或未跟踪的文件。
工作原理如下:
checkout dev
将更新 HEAD
、您的索引(暂存区)和工作树以匹配 dev
、 的存储库状态
reset --soft master
不会影响您的索引或工作树(这意味着您暂存区中的所有文件将准确反映 dev
处的存储库状态),但会指向 HEAD
在 master
和
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 commit
在 git 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
,例如 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 "") || 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
对于您最初描述的情况是正确的(并且赞成)。不过,您可能需要一个解释,尤其是因为各种命令的参数可能需要在其他设置中进行一些调整,例如您似乎正在瞄准的更一般的情况。
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
本质上与 忘记 A
和 B
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
。
我正在寻找一种方法来压缩使用 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
命令应该压缩提交
明确地说,这是我要找的东西:
- 由 git rebase 给出的干净的线性历史
- 在没有不必要交互的情况下压缩来自功能分支的提交
git rebase -i
使用交互性,这是我想避免的
以下将把 dev
树放入 master
的新提交中(注意:这 不 等同于变基,因为 @torek请在下方指出):
git checkout dev && \
git reset --soft master && \
git commit
请注意,执行此操作时不应有未提交的页面或未跟踪的文件。
工作原理如下:
checkout dev
将更新HEAD
、您的索引(暂存区)和工作树以匹配dev
、 的存储库状态
reset --soft master
不会影响您的索引或工作树(这意味着您暂存区中的所有文件将准确反映dev
处的存储库状态),但会指向HEAD
在master
和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 commit
在 git 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
,例如 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 "") || 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
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
本质上与 忘记 A
和 B
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
。