如何轻松修复过去的提交?

时间:2010-06-23 16:32:31

标签: git rewrite

我刚刚阅读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.

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

13 个答案:

答案 0 :(得分:141)

更新的答案

不久前,--fixup添加了一个新的git commit参数,可用于构造一个带有适合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并使用GIT_EDITOR设置set环境变量。根据其他操作系统的需要进行调整。

使用这个脚本,我可以准确地实现我要求的'修复损坏的源,阶段修复,运行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)

答案 1 :(得分:27)

我的工作是:

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.

保存&amp;退出编辑器,修复程序将被压缩回它所属的提交。

在你做了几次之后,你会在睡眠中几秒钟内完成。交互式变基是真正让我用git卖给我的功能。这对于这个以及更多......非常有用......

答案 2 :(得分:17)

派对有点晚了,但这是一个解决方案,正如作者想象的那样。

将此添加到.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

您可以自动修改别名以隐藏,而不是发出警告。但是,如果修复不能完全应用,则需要在修复冲突后手动弹出存储。手动执行保存和弹出似乎更一致,更少混淆。

答案 3 :(得分:6)

更新:现在可以在此处找到更清晰的脚本版本: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
}

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

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

答案 4 :(得分:6)

修复一个提交:

git commit --fixup a0b1c2d3 .
git rebase --autosquash -i

其中0b1c2d3是您想要修复的提交。

注意:没有-i的git rebase --autosquash没有工作但是-i工作,这很奇怪。

答案 5 :(得分:4)

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

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

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

答案 6 :(得分:3)

我对修复工作流程感到困扰的是,我不得不弄清楚自己哪个提交想要将变更压缩到每次。我创建了一个“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

答案 7 :(得分:1)

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

对于git diff中的每个块,脚本使用git blame查找最后触及相关行的提交,并调用git commit --fixup来编写相应的fixup!提交,基本上做我之前手动做的事情。

如果您发现它很有用,请随时改进并重复它,也许有一天我们会在git中获得这样的功能。我很高兴看到一个工具可以理解如何在交互式rebase引入合并冲突时解决它。

答案 8 :(得分:1)

我写了一个名为gcf的小shell函数来自动执行fixup commit和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~~

修补最新的第二次提交

这是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
  fi

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

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

  echo ">> Performing rebase"
  # --autosquash requires -i, but we can avoid interaction with a dummy EDITOR
  EDITOR=true git rebase --interactive --autosquash --autostash \
                         --preserve-merges "${commit_to_amend}~"
}

alias gcf='git_commit_immediate_fixup'

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

答案 9 :(得分:1)

我推荐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将功能修复到功能分支中 功能。

答案 10 :(得分:1)

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

答案 11 :(得分:0)

我不知道一种自动化方式,但这里的解决方案可能更容易被人为操作:

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

答案 12 :(得分:0)

您可以使用此别名为特定文件创建 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将为fixup!提交myfile.txt提交}上次修改,然后它将rebase --autosquash