我如何轻松修正过去的提交?


115

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

时不时地,我在使用(无关)功能时注意到代码中的错误。然后快速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.

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


您对提交进行“重新排序”是什么意思?如果您要更改历史记录,则自更改的提交以来的所有提交必须不同,但是链接问题的公认答案不会以任何有意义的方式对提交进行重新排序。
CB Bailey 2010年

1
@Charles:我的意思是重新排序,例如:如果我注意到HEAD〜5是损坏的提交,则链接的问题中以下可接受的答案将使HEAD(分支的尖端)成为固定的提交。但是,我希望HEAD〜5是固定的提交-这是在使用交互式资源库并编辑单个提交进行修复时所得到的。
Frerich Raabe

是的,但是rebase命令将重新检出master并将所有后续提交重新设置为固定提交。这不是你的驾驶方式rebase -i吗?
CB Bailey 2010年

实际上,该答案存在潜在的问题,我认为应该是rebase --onto tmp bad-commit master。如所写,它将尝试将错误的提交应用于固定的提交状态。
CB Bailey

这是另一个用于自动执行
修复

Answers:


164

更新的答案

不久前,--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)

2
你可以使用git stashgit stash pop周围的底垫,使其不再需要一个干净的工作目录
托比亚斯Kienzler

@TobiasKienzler:关于使用git stashgit stash pop:你是对的,但不幸的git stash很多在Windows上比它慢是Linux或OS / X。由于我的工作目录通常是干净的,因此我省略了此步骤以不降低命令速度。
Frerich Raabe 2012年

我可以确认这一点,尤其是在处理网络共享时:-/
Tobias Kienzler 2012年

1
真好 我不小心这样做了git rebase -i --fixup,并且它从固定提交作为起点进行了重新构建,因此在我的情况下不需要sha参数。
fwielstra 2012年

1
对于经常使用--autosquash的用户,将其设置为默认行为可能会很有用: git config --global rebase.autosquash true
taktak004

31

我要做的是:

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修复了错误的更改。#向上移动,然后将“ pick”更改为“ f”以表示“ fixup”。
选择400bce4好零钱3。

保存并退出编辑器,此修复程序将被压回到其所属的提交中。

完成几次后,您将在数秒内完成睡眠。交互式rebasing是真正在git上卖给我的功能。这对于此功能以及其他功能都非常有用。


9
显然,您可以将HEAD〜5更改为HEAD〜n以进一步返回。您将不希望混入任何已向上游推送的历史记录,因此我通常输入'git rebase -i origin / master'以确保我仅更改未推送的历史记录。
克里斯·詹金斯

4
这很像我一直做的事。FWIW,您可能对的--autosquash开关感兴趣,该开关git rebase会自动为您重新排序编辑器中的步骤。请参阅我的响应,以获取利用此脚本来实现git fixup命令的脚本。
Frerich Raabe

我不知道您可以重新排序提交哈希,很好!
亚伦·弗兰克

太棒了!只是要确保完成所有基础工作是单独的功能分支。并且不要与普通分支一样混乱。
杰伊·莫迪

22

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

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

您可以将别名修改为自动隐藏,而不发出警告。但是,如果修复程序无法完全应用,则需要在修复冲突后手动弹出存储。手动执行保存和弹出操作似乎更加一致,也减少了混乱。


这很有帮助。对我来说,最常见的用例是将更改固定在上一个提交上,因此git fixup HEAD我为其创建了别名。我也可以为此使用修正。

谢谢!我也最经常在上一次提交时使用它,但是我还有另一个别名可以快速修改。amend = commit --amend --reuse-message=HEAD然后,您可以键入git amendgit amend -a跳过编辑器以获取提交消息。
dschlyter's

3
修改的问题是我不记得该如何拼写。我总是想,是修改还是修改,这不好。

12

要修正一个提交:

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

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

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


2016年--autosquash-i仍然无法正常工作。
乔纳森·克罗斯

1
如手册页所述:仅当使用--interactive选项时,此选项才有效。但是有一种简单的方法可以跳过编辑器:EDITOR=true git rebase --autosquash -i
joeytwiddle

第二步不适用于我,说:请指定要作为基准的分支。
djangonaut

git rebase --autosquash -i HEAD〜2(其中2是您想要更改的+1粘贴的提交数。
Sérgio19年

6

更新:现在可以在以下位置找到脚本的更清洁版本: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(如果需要,也可以使用南瓜)提交。之后,它将使用--autosquash您在参数提交的提交的父项上的标志启动交互式的变基。这将打开您配置的文本编辑器,因此您可以验证所有内容是否都与您期望的一样,只需关闭编辑器即可完成该过程。

之所以使用该if [[ "$1" == HEAD* ]]部分(是从此处借来的),是因为,例如,如果您使用HEAD〜2作为您的提交(您要修复当前更改的提交)引用,则在创建修订提交后,将替换HEAD并且您将需要使用HEAD〜3来引用同一提交。


有趣的选择。+1
VonC

4

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

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

它将/bin/true用作编辑器,而不是/usr/bin/vim。它总是接受git建议的内容,而不会提示。


确实,这正是我从2010年9月30日起在“原始答案” Python脚本答案中所做的事情(请注意,它在脚本底部显示了如何call(["set", "GIT_EDITOR=true", "&&", git, "rebase", "-i" ...)。
Frerich Raabe

4

修复工作流程真正困扰我的是,我必须弄清楚自己想把每次变更压缩到哪个提交中。我创建了一个“ git fixup”命令来帮助解决这个问题。

该命令创建了fixture提交,并添加了魔术,它使用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

仅当可以将分阶段的更改明确地归因于工作树上的特定提交(在主服务器和HEAD之间)时,这才起作用。我发现我经常使用的小更改类型就是这种情况,例如注释中的错别字或新引入(或重命名)方法的名称。如果不是这种情况,它将至少显示候选提交列表。

我用这个了很多在我的日常工作流程,对我的工作的分支快速整合小的改动之前更改的行成的提交。该脚本虽然不尽如人意,但它是用zsh编写的,但是一段时间以来它一直为我做得很好,因为我从来没有觉得需要重写它:

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


2

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

[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


非常聪明,所以我希望git rebase不会自动调用它。
hurikhan77

2

commit --fixuprebase --autosquash是伟大的,但他们没有足够的做。当我有一系列提交时A-B-C,我在工作树中写了一些更改,这些更改属于一个或多个现有提交中,我必须手动查看历史记录,确定哪些更改属于哪些提交,暂存并创建fixup!提交。但是git已经可以访问足够的信息来为我做所有的事情,因此我编写了一个Perl脚本来完成这一工作。

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

如果您觉得它有用,请随时进行改进和迭代,也许有一天我们会git适当地提供这样的功能。我很乐意看到一个工具,该工具可以理解交互式基础库引入合并冲突后应如何解决。


我对自动化也有梦想:git应该尽量将其放回历史,而不会破坏补丁。但是您的方法可能更理智。很高兴看到您尝试过。我会尝试的!(当然,有时修补文件补丁会出现在文件的其他位置,只有开发人员才能知道修补文件属于哪个提交。或者也许测试套件中的新测试可以帮助计算机确定修补程序应该去哪里。)
joeytwiddle

1

我写了一个叫做shell的小函数gcf来自动执行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~~

这是功能。您可以将其粘贴到您的~/.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变基,但是我们通过使用dummy来避免交互EDITOR

--no-fork-point保护提交在极少数情况下不会被丢弃(当您从新分支中分叉,并且某人已经根据过去的提交重新建立基础时)。


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

请参阅我的响应,以获取一个利用此脚本来实现git fixup命令的脚本。
Frerich Raabe

@Frerich Raabe:听起来不错,我不知道--autosquash
Tobias Kienzler 2010年

0

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

电梯间距

您有一个具有一些提交的功能分支。您的队友查看了分支,并指出了一些错误。您已经修复了这些错误,但是您不想将它们全部推到一个显示修复程序的不透明提交中,因为您相信原子提交。代替为手动查找提交SHA git commit --fixup或运行手动交互式变基,而是执行以下操作:

  • git add $FILES_YOU_FIXED

  • git absorb --and-rebase

  • 要么: git rebase -i --autosquash master

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.