【问题标题】:How can I easily fixup a past commit?如何轻松修复过去的提交?
【发布时间】:2011-03-07 10:11:52
【问题描述】:

我刚刚阅读了amending a single file in a past commit in git,但不幸的是,接受的解决方案“重新排序”了提交,这不是我想要的。所以这是我的问题:

在处理(不相关的)功能时,我时不时会注意到我的代码中存在错误。快速的git blame 然后显示该错误已在几次提交前被引入(我提交了很多,所以通常不是最近的提交引入了该错误)。此时,我通常会这样做:

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

但是,这种情况经常发生,以至于上面的顺序变得很烦人。特别是“交互式变基”很无聊。上述序列是否有任何捷径,可以让我通过分阶段更改修改过去的任意提交?我非常清楚这会改变历史,但我经常犯错误,以至于我真的很想拥有类似的东西

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.

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

【问题讨论】:

  • “重新排序”提交是什么意思?如果您要更改历史记录,那么自更改的提交以来的所有提交必须 不同,但对链接问题的接受答案不会以任何有意义的方式重新排序提交。
  • @Charles:我的意思是重新排序:如果我注意到 HEAD~5 是损坏的提交,则链接问题中接受的答案将使 HEAD(分支的尖端)成为固定的犯罪。但是,我希望 HEAD~5 成为固定提交 - 这是您在使用交互式变基并编辑单个提交进行修复时得到的。
  • 是的,但是 rebase 命令将重新签出 master 并将所有后续提交 rebase 到固定提交。这不是你驾驶rebase -i 的方式吗?
  • 其实这个答案有潜在的问题,我觉得应该是rebase --onto tmp bad-commit master。如所写,它将尝试将错误提交应用于固定提交状态。
  • 这是另一个自动化修复/变基过程的工具:stackoverflow.com/a/24656286/1058622

标签: git rewrite


【解决方案1】:

更新答案

不久前,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 fixup”工作流程:

#!/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)

【讨论】:

  • 你可以在你的 rebase 周围使用 git stashgit stash pop 不再需要一个干净的工作目录
  • @TobiasKienzler:关于使用git stashgit stash pop:你是对的,但不幸的是git stash 在Windows 上比在Linux 或操作系统上慢得多 /X。由于我的工作目录通常是干净的,因此我省略了这一步以不减慢命令速度。
  • 我可以确认,尤其是在处理网络共享时:-/
  • 不错。我不小心做了git rebase -i --fixup,它从固定提交重新定位为起点,所以在我的情况下不需要 sha 参数。
  • 对于经常使用 --autosquash 的人,将其设置为默认行为可能很有用:git config --global rebase.autosquash true
【解决方案2】:

我做的是:

git add ... # 添加修复。 git commit # 提交了,但是在错误的地方。 git rebase -i HEAD~5 # 检查最后 5 次提交以进行变基。

您的编辑器将打开一个最近 5 次提交的列表,准备好进行干预。变化:

选择 08e833c 好改变 1。 选择 9134ac9 好变化 2。 选择 5adda55 糟糕的变化! 选择 400bce4 好变化 3。 选择 2bc82n1 修复错误更改。

...到:

选择 08e833c 好改变 1。 选择 9134ac9 好变化 2。 选择 5adda55 糟糕的变化! f 2bc82n1 Fix of bad change. # 向上移动,将 'pick' 更改为 'f' for 'fixup'。 选择 400bce4 好变化 3。

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

做几次之后,你会在睡梦中几秒钟内完成。交互式变基是 git 真正卖给我的特性。它对此非常有用,还有更多...

【讨论】:

  • 显然您可以将 HEAD~5 更改为 HEAD~n 以进一步返回。你不会想干预你推送到上游的任何历史记录,所以我通常输入“git rebase -i origin/master”以确保我只更改未推送的历史记录。
  • 这很像我一直做的; FWIW,您可能对git rebase--autosquash 开关感兴趣,它会自动为您重新排序编辑器中的步骤。请参阅我对利用此实现git fixup 命令的脚本的回复。
  • 我不知道你可以重新排序提交哈希,太好了!
  • 太棒了!只是为了确保所有 rebase 工作都完成是单独的功能分支。并且不要乱用像 master 这样的普通分支。
  • 这个!结合dd(用于拉线)和p/P(用于粘贴)之类的vim命令,在控制和简化imo之间取得了很好的平衡。例如,在上面的例子中,我输入ddkP,然后进入插入模式i,将pick 更改为f,按esc 退出插入模式。然后用:wq保存并输入。
【解决方案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

但是,如果您有未暂存的更改,则必须在 rebase 之前存储它们。

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

您可以将别名修改为自动存储,而不是发出警告。但是,如果修复不能完全应用,您将需要在修复冲突后手动弹出存储。手动保存和弹出似乎更加一致且不那么混乱。

【讨论】:

  • 这很有帮助。对我来说,最常见的用例是将更改修复到上一次提交中,所以 git fixup HEAD 是我为其创建别名的地方。我想我也可以使用修正。
  • 谢谢!我也最常在最后一次提交时使用它,但我有另一个别名可以快速修改。 amend = commit --amend --reuse-message=HEAD 然后你可以输入 git amendgit amend -a 并跳过提交消息的编辑器。
  • amend 的问题是我不记得怎么拼写了。我总是要思考,是修改还是修改,这样不好。
【解决方案4】:

修复一个提交:

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

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

注意:没有 -i 的 git rebase --autosquash 不起作用,但有 -i 起作用,这很奇怪。

【讨论】:

  • 2016 和 --autosquash 没有 -i 仍然不起作用。
  • 如手册页所述:此选项仅在使用 --interactive 选项时有效。 但是有一种简单的方法可以跳过编辑器:EDITOR=true git rebase --autosquash -i
  • 第二步对我不起作用,说:请指定你想要变基的分支。
  • git rebase --autosquash -i HEAD~2 (其中 2 是您想要更改的提交次数 +1 粘贴。
【解决方案5】:

更新:现在可以在此处找到更简洁的脚本版本:https://github.com/deiwin/git-dotfiles/blob/docs/bin/git-fixup

我一直在寻找类似的东西。不过,这个 Python 脚本似乎太复杂了,因此我拼凑了自己的解决方案:

首先,我的 git 别名是这样的(借用自 here):

[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
}

此代码首先暂存所有当前更改(如果您希望自己暂存文件,可以删除此部分)。然后创建修复(也可以使用壁球,如果你需要的话)提交。之后,它会在您作为参数提供的提交的父级上使用 --autosquash 标志启动交互式 rebase。这将打开您配置的文本编辑器,因此您可以验证一切是否符合您的预期,只需关闭编辑器即可完成该过程。

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

【讨论】:

  • 有趣的选择。 +1
【解决方案6】:

修复工作流程真正困扰我的是,我必须自己弄清楚每次我想将更改压缩到哪个提交中。我创建了一个“git 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 之间)时才有效。我发现这种情况经常发生在我使用它的小变化类型上,例如cmets 中的拼写错误或新引入(或重命名)方法的名称。如果不是这样,它至少会显示一个候选提交列表。

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

https://github.com/Valodim/git-fixup

【讨论】:

    【解决方案7】:

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

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

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

    【讨论】:

    • 确实,这正是我在 2010 年 9 月 30 日的“原始答案”Python 脚本答案中所做的(请注意脚本底部是如何写的,call(["set", "GIT_EDITOR=true", "&amp;&amp;", git, "rebase", "-i" ...)。
    【解决方案8】:

    这是基于 accepted answer 的 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"
    

    【讨论】:

      【解决方案9】:

      您可以使用此别名为特定文件创建 fixup

      [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 rebase 不会被自动调用。
      【解决方案10】:

      commit --fixuprebase --autosquash 很棒,但还不够。当我有一系列提交A-B-C 并且我在我的工作树中写了一些属于一个或多个现有提交的更改时,我必须手动查看历史记录,决定哪些更改属于哪些提交,暂存它们并创建 fixup! 提交。但是 git 已经获得了足够的信息来为我完成所有这些工作,所以我写了一个 Perl script 来做这件事。

      对于git diff 中的每个大块,脚本使用git blame 来查找最后触及相关行的提交,并调用git commit --fixup 来编写适当的fixup! 提交,基本上与我手动执行的操作相同之前。

      如果您觉得它有用,请随时对其进行改进和迭代,也许有一天我们会在git 中获得这样的功能。我很想看到一个工具,当它被交互式 rebase 引入时,它可以理解如何解决合并冲突。

      【讨论】:

      • 我也有自动化的梦想:git 应该尽量把它放在历史上,而不会破坏补丁。但你的方法可能更理智。很高兴看到你已经尝试过了。我会试试看! (当然,有时修复补丁会出现在文件的其他位置,只有开发人员知道它属于哪个提交。或者,测试套件中的新测试可能会帮助机器确定修复的位置。)
      【解决方案11】:

      我推荐https://github.com/tummychow/git-absorb:

      电梯间距

      您有一个包含一些提交的功能分支。你的队友评论了 分支并指出了一些错误。您已经修复了错误, 但是您不想将它们全部推入一个不透明的提交中 修复,因为您相信原子提交。而不是手动 查找git commit --fixup 的提交 SHA,或运行手册 交互式变基,这样做:

      • git add $FILES_YOU_FIXED

      • git absorb --and-rebase

      • 或:git rebase -i --autosquash master

      git absorb 将自动识别哪些提交是安全的 修改,以及哪些索引更改属于每个提交。它 然后会写修复!提交每个更改。你可以 如果您不信任它,请手动检查其输出,然后折叠 使用 git 的内置 autosquash 修复功能分支 功能。

      【讨论】:

        【解决方案12】:

        我编写了一个名为 gcf 的小 shell 函数来自动执行修复提交和变基:

        $ 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~~

        这里是the function。您可以将其粘贴到您的~/.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 保护提交不被默默地丢弃在rare situations 中(当您分叉一个新分支,并且有人已经重新设置了过去的提交时)。

        【讨论】:

          【解决方案13】:

          我不知道有一种自动化的方法,但这里有一个更容易被人类机器化的解决方案:

          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 fixup 命令的脚本。
          • @Frerich Raabe:听起来不错,我不知道--autosquash
          【解决方案14】:

          给定

          $ 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 ... 来实际进行修复。

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

          【讨论】:

            猜你喜欢
            • 2019-03-05
            • 2012-01-14
            • 1970-01-01
            • 2021-12-20
            • 1970-01-01
            • 2020-04-07
            • 2014-06-13
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多