我怎样才能轻松地修复一个过去的提交?

我刚刚读了 在 git 中修改过去提交的单个文件,但不幸的是,接受的解决方案“重新排序”提交,这不是我想要的。所以我的问题是:

在处理一个(不相关的)特性时,我会时不时地注意到代码中的一个 bug。然后,快速的 git blame显示 bug 在几次提交之前就已经被引入了(我提交了很多次,所以通常不是最近的提交引入了 bug)。这个时候,我通常会这样做:

git stash                      # temporarily put my work aside
git rebase -i <bad_commit>~1   # rebase one step before the bad commit
# mark broken commit for editing
vim <affected_sources>         # fix the bug
git add <affected_sources>     # stage fixes
git commit -C <bad_commit>     # commit fixes using same log message as before
git rebase --continue          # base all later changes onto this

然而,这种情况经常发生,以至于上面的顺序越来越烦人。特别是“交互式 rebase”很无聊。对于上面的顺序,有没有什么捷径可以让我用分段更改修改过去的任意提交?我非常清楚这会改变历史,但我经常犯错,所以我真的很想拥有

vim <affected_sources>             # fix bug
git add -p <affected_sources>      # Mark my 'fixup' hungs for staging
git fixup <bad_commit>             # amend the specified commit with staged changes,
# rebase any successors of bad commit on rewritten
# commit.

也许是一个可以使用管道工具重写提交的智能脚本?

94737 次浏览

我不知道有没有自动化的方法,但是这里有一个解决方案,可能更容易人工化:

git stash
# write the patch
git add -p <file>
git commit -m"whatever"   # message doesn't matter, will be replaced via 'fixup'
git rebase -i <bad-commit-id>~1
# now cut&paste the "whatever" line from the bottom to the second line
# (i.e. below <bad-commit>) and change its 'pick' into 'fixup'
# -> the fix commit will be merged into the <bad-commit> without changing the
# commit message
git stash pop

我要做的是:

git add ...           # Add the fix.
git commit            # Committed, but in the wrong place.
git rebase -i HEAD~5  # Examine the last 5 commits for rebasing.

你的编辑器会打开一个最后5次提交的列表,准备好被干涉。更改:

pick 08e833c Good change 1.
pick 9134ac9 Good change 2.
pick 5adda55 Bad change!
pick 400bce4 Good change 3.
pick 2bc82n1 Fix of bad change.

... 到:

pick 08e833c Good change 1.
pick 9134ac9 Good change 2.
pick 5adda55 Bad change!
f 2bc82n1 Fix of bad change. # Move up, and change 'pick' to 'f' for 'fixup'.
pick 400bce4 Good change 3.

保存并退出您的编辑器,修复将被压缩回它所属的提交中。

这样做几次之后,你就会在睡梦中几秒钟就做完了。交互式重定基是 Git 真正吸引我的特性。它对这个和更多的东西都非常有用。

更新答案

不久前,在 git commit中添加了一个新的 --fixup参数,可以使用适合于 git rebase --interactive --autosquash的日志消息构造提交。因此,修复过去提交的最简单方法是现在:

$ git add ...                           # Stage a fix
$ git commit --fixup=a0b1c2d3           # Perform the commit to fix broken a0b1c2d3
$ git rebase -i --autosquash a0b1c2d3~1 # Now merge fixup commit into broken commit

原始答案

下面是我不久前编写的一个小 Python 脚本,它实现了我在最初的问题中所希望的 git fixup逻辑。该脚本假设您暂存了一些更改,然后将这些更改应用于给定的提交。

注意 : 此脚本是 Windows 特有的,它查找 git.exe并使用 set设置 GIT_EDITOR环境变量。根据需要对其他操作系统进行调整。

使用这个脚本,我可以精确地实现我要求的“修复损坏的源代码、阶段修复、运行 git 修复”工作流:

#!/usr/bin/env python
from subprocess import call
import sys


# Taken from http://stackoverflow.com/questions/377017/test-if-executable-exists-in python
def which(program):
import os
def is_exe(fpath):
return os.path.exists(fpath) and os.access(fpath, os.X_OK)


fpath, fname = os.path.split(program)
if fpath:
if is_exe(program):
return program
else:
for path in os.environ["PATH"].split(os.pathsep):
exe_file = os.path.join(path, program)
if is_exe(exe_file):
return exe_file


return None


if len(sys.argv) != 2:
print "Usage: git fixup <commit>"
sys.exit(1)


git = which("git.exe")
if not git:
print "git-fixup: failed to locate git executable"
sys.exit(2)


broken_commit = sys.argv[1]
if call([git, "rev-parse", "--verify", "--quiet", broken_commit]) != 0:
print "git-fixup: %s is not a valid commit" % broken_commit
sys.exit(3)


if call([git, "diff", "--staged", "--quiet"]) == 0:
print "git-fixup: cannot fixup past commit; no fix staged."
sys.exit(4)


if call([git, "diff", "--quiet"]) != 0:
print "git-fixup: cannot fixup past commit; working directory must be clean."
sys.exit(5)


call([git, "commit", "--fixup=" + broken_commit])
call(["set", "GIT_EDITOR=true", "&&", git, "rebase", "-i", "--autosquash", broken_commit + "~1"], shell=True)

更新: 现在可以在这里找到更清晰的脚本版本: https://github.com/deiwin/git-dotfiles/blob/docs/bin/git-fixup

我一直在寻找类似的东西,这个 Python 脚本看起来太复杂了,因此我想出了自己的解决方案:

首先,我的 git 别名如下所示(借自 给你) :

[alias]
fixup = !sh -c 'git commit --fixup=$1' -
squash = !sh -c 'git commit --squash=$1' -
ri = rebase --interactive --autosquash

现在 bash 函数变得非常简单:

function gf {
if [ $# -eq 1 ]
then
if [[ "$1" == HEAD* ]]
then
git add -A; git fixup $1; git ri $1~2
else
git add -A; git fixup $1; git ri $1~1
fi
else
echo "Usage: gf <commit-ref> "
fi
}

这段代码首先处理所有当前的更改(如果您希望自己处理文件,可以删除此部分)。然后创建 fixup (如果需要,也可以使用 squash)提交。之后,它将启动一个交互式的 rebase,在提交作为参数的父级上使用 --autosquash标志。这将打开您配置的文本编辑器,因此您可以验证所有内容都是您所期望的,只需关闭编辑器即可完成该过程。

使用 if [[ "$1" == HEAD* ]]部分(借用自 给你) ,因为如果使用 HEAD ~ 2作为提交(你想用来修复当前更改的提交)引用,那么在修复提交创建之后,HEAD 将被替换,你需要使用 HEAD ~ 3来引用相同的提交。

有点晚了,但这里有一个解决方案,作者想象的工作。

把这个添加到你的. gitconfig:

[alias]
fixup = "!sh -c '(git diff-files --quiet || (echo Unstaged changes, please commit or stash with --keep-index; exit 1)) && COMMIT=$(git rev-parse $1) && git commit --fixup=$COMMIT && git rebase -i --autosquash $COMMIT~1' -"

示例用法:

git add -p
git fixup HEAD~5

但是,如果您有非暂存的更改,则必须在重新基础之前将它们存储起来。

git add -p
git stash --keep-index
git fixup HEAD~5
git stash pop

您可以将别名修改为自动隐藏,而不是提供警告。但是,如果修复程序没有完全应用,那么在修复冲突之后,您需要手动弹出隐藏。手动执行保存和弹出操作似乎更加一致,也不那么令人困惑。

可以使用此别名为特定文件创建 装修

[alias]
...
# fixup for a file, using the commit where it was last modified
fixup-file = "!sh -c '\
[ $(git diff          --numstat $1 | wc -l) -eq 1 ] && git add $1 && \
[ $(git diff --cached --numstat $1 | wc -l) -eq 1 ] || (echo No changes staged. ; exit 1) && \
COMMIT=$(git log -n 1 --pretty=format:"%H" $1) && \
git commit --fixup=$COMMIT && \
git rebase -i --autosquash $COMMIT~1' -"

如果您已经在 myfile.txt中做了一些更改,但是不想将它们放入新的提交中,那么 git fixup-file myfile.txt将为上次修改 myfile.txt的提交创建一个 fixup!,然后它将创建 rebase --autosquash

修复一个承诺:

git commit --fixup a0b1c2d3 .
git rebase --autosquash -i HEAD~2

其中 a0b1c2d3是您想要修复的提交,其中2是您想要更改的提交数 + 1。

注意: git rebase —— autosquash without-i doesn’t work but with-i working,which is  。

commit --fixuprebase --autosquash都很棒,但是他们做的还不够。当我有一个提交 A-B-C的序列,并且我在我的工作树中写了一些属于一个或多个现有提交的更改时,我必须手动查看历史记录,决定哪些更改属于哪个提交,分阶段进行,然后创建 fixup!提交。但是 git 已经能够访问足够的信息,能够为我做所有这些,所以我写了一个 Perl 脚本,它正是这样做的。

对于 git diff中的每个块,脚本使用 git blame来查找最后一次触及相关行的提交,并调用 git commit --fixup来编写适当的 fixup!提交,实际上做的事情与我以前手动做的相同。

如果你觉得它有用,请随时改进和迭代它,也许有一天我们会得到这样一个功能在 git适当。我希望看到一个工具,可以理解如何解决合并冲突时,它已经引入了一个交互式基地。

使用“ null”编辑器可以避免交互阶段:

$ EDITOR=true git rebase --autosquash -i ...

这将使用 /bin/true作为编辑器,而不是 /usr/bin/vim。它总是接受任何 git 建议,没有提示。

我编写了一个名为 gcf的 shell 函数来自动执行补丁提交和 rebase:

$ git add -p


... select hunks for the patch with y/n ...


$ gcf <earlier_commit_id>


That commits the fixup and does the rebase.  Done!  You can get back to coding.

例如,可以使用 gcf HEAD~~修补最新提交之前的第二次提交

这是 函数。你可以把它粘贴到你的 ~/.bashrc

git_commit_immediate_fixup() {
local commit_to_amend="$1"
if [ -z "$commit_to_amend" ]; then
echo "You must provide a commit to fixup!"; return 1
fi


# Get a static commit ref in case the commit is something relative like HEAD~
commit_to_amend="$(git rev-parse "${commit_to_amend}")" || return 2


#echo ">> Committing"
git commit --no-verify --fixup "${commit_to_amend}" || return 3


#echo ">> Performing rebase"
EDITOR=true git rebase --interactive --autosquash --autostash \
--rebase-merges --no-fork-point "${commit_to_amend}~"
}


alias gcf='git_commit_immediate_fixup'

如果需要,它使用 --autostash来存储和弹出任何未提交的更改。

--autosquash需要一个 --interactive重定基,但是我们通过使用一个虚拟的 EDITOR来避免交互。

--no-fork-point保护提交不被悄悄丢弃在 罕见的情况中(当您分叉出一个新的分支,并且有人已经重新基于过去的提交)。

修复工作流程真正困扰我的是,我必须弄清楚自己每次都要做哪些改动。我创建了一个“ git fixup”命令来帮助解决这个问题。

这个命令创建 fixup 提交,附加了使用 Git-deps自动查找相关提交的魔力,因此工作流程通常可以归结为:

# discover and fix typo in a previously committed change
git add -p # stage only typo fix
git fixup


# at some later point squash all the fixup commits that came up
git rebase --autosquash master

只有当阶段性更改可以明确归因于工作树上的特定提交(在 master 和 HEAD 之间)时,这种方法才有效。我发现这种情况经常发生在我使用这种方法进行的一些小改动上,比如注释中的错别字或者新引入(或者重命名)的方法的名称。如果不是这样,它至少会显示一个候选提交列表。

我在我的日常工作流程中使用这个 很多,以便快速地将对以前更改过的行的小更改集成到我的工作分支上的提交中。这个脚本并没有想象中的那么漂亮,而且它是用 zsh 编写的,但是它已经为我做了很长一段时间的工作,我从来没有感觉到有必要重写它:

Https://github.com/valodim/git-fixup

我建议 https://github.com/tummychow/git-absorb:

电梯推销

你有一个功能部门,只有几个提交,你的队友评审过 指出了一些错误。你已经修复了这些错误, 但是您不希望将它们全部推入到一个不透明的提交中,该提交表示 修复,因为您相信原子提交。而不是手动 为 git commit --fixup查找提交 SHA,或运行手册 交互式 rebase,这样做:

  • git add $FILES_YOU_FIXED

  • git absorb --and-rebase

  • 或: git rebase -i --autosquash master

git absorb将自动识别哪些提交是安全的 以及哪些索引更改属于这些提交中的每一个 然后编写补丁! 为每个更改提交。您可以 如果您不信任它,请手动检查它的输出,然后折叠 通过 git 内置的 auto squash 将补丁插入到功能分支中 功能。

下面是基于 接受的答案的 git 别名,其工作原理如下:

git fixup          # fixup staged & unstaged changes into the last commit
git fixup ac1dc0d3 # fixup staged & unstaged changes into the given commit

更新你的 ~/.gitconfig文件并添加这个别名:

[alias]
fixup = "!git add . && git commit --fixup=${1:-$(git rev-parse HEAD)} && GIT_EDITOR=true git rebase --interactive --autosquash ${1:-$(git rev-parse HEAD~2)}~1"

给予

$ git log --oneline
123123 Add foo
234234 Fix biz
123113 Remove fong
123123 Modify bar
123143 Add bar

您可以使用以下命令为 Modify bar创建修复提交

git commit --fixup ':/bar'

它为包含子字符串 bar的最后一次提交创建补丁提交 . 我总是忘记这里的确切语法,而且很难找到,因为显然每个人都知道 SHA 提交的所有内容

然后在你方便的时候运行 rebase -i --autosquash ...来进行实际的修复。

注意: 这使用某种正则表达式,因此 ()和其他特殊字符可能需要引号。