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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/3103589/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-10 08:36:52  来源:igfitidea点击:

How can I easily fixup a past commit?

gitrewrite

提问by Frerich Raabe

I just read amending a single file in a past commit in gitbut unfortunately the accepted solution 'reorders' the commits, which is not what I want. So here's my question:

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

Every now and then, I notice a bug in my code while working on an (unrelated) feature. A quick git blamethen reveals that the bug has been introduced a few commits ago (I commit quite a lot, so usually it's not the most recent commit which introduced the bug). At this point, I usually do this:

在处理(不相关的)功能时,我时不时地注意到我的代码中的一个错误。然后快速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

However, this happens so often that the above sequence is getting annoying. Especially the 'interactive rebase' is boring. Is there any shortcut to the above sequence, which lets me amend an arbitrary commit in the past with the staged changes? I'm perfectly aware that this changes the history, but I'm doing mistakes so often that I'd really love to have something like

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

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.

Maybe a smart script which can rewrite commits using plumbing tools or so?

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

回答by Frerich Raabe

UPDATED ANSWER

更新的答案

A while ago, a new --fixupargument was added to git commitwhich can be used to construct a commit with a log message suitable for git rebase --interactive --autosquash. So the simplest way to fixup a past commit is now:

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

ORIGINAL ANSWER

原答案

Here's a little Python script I wrote a while ago which implements this git fixuplogic I hoped for in my original question. The script assumes that you staged some changes and then applies those changes to the given commit.

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

NOTE: This script is Windows-specific; it looks for git.exeand sets the GIT_EDITORenvironment variable using set. Adjust this as needed for other operating systems.

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

Using this script I can implement precisely the 'fix broken sources, stage fixes, run git fixup ' workflow I asked for:

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

回答by Kris Jenkins

What I do is:

我要做的是:

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.

Your editor will open with a list of the last 5 commits, ready to be meddled with. Change:

您的编辑器将打开最近 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.

...to:

...到:

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.

Save & exit your editor, and the fix will be squished back into the commit it belongs with.

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

After you've done that a few times, you'll do it in seconds in your sleep. Interactive rebasing is the feature that really sold me on git. It's incredibly useful for this and more...

这样做几次后,您将在睡梦中几秒钟内完成。交互式变基是在 git 上真正让我受益的功能。这对这个和更多的人来说非常有用......

回答by dschlyter

A bit late to the party, but here is a solution that works as the author imagined.

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

Add this to your .gitconfig:

将此添加到您的 .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 ) && git commit --fixup=$COMMIT && git rebase -i --autosquash $COMMIT~1' -"

Example usage:

用法示例:

git add -p
git fixup HEAD~5

However if you have unstaged changes, you must stash them before the rebase.

但是,如果您有未暂存的更改,则必须在 rebase 之前将它们隐藏起来。

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

You could modify the alias to stash automatically, instead of giving a warning. However, if the fixup does not apply cleanly you will need pop the stash manually after fixing the conflicts. Doing both the saving and popping manually seems more consistent and less confusing.

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

回答by Sérgio

To fixup one commit :

要修复一个提交:

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

where a0b1c2d3 is commit that you want fixup and where 2 is the number of commits +1 pasted that you want to change.

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

Note: git rebase --autosquash without -i doesn't worked but with -i worked, which is strange.

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

回答by Deiwin

UPDATE:A cleaner version of the script can now be found here: https://github.com/deiwin/git-dotfiles/blob/docs/bin/git-fixup.

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

I've been looking for something similar. This Python script seems too complicated, though, therefore I've hammered together my own solution:

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

First, my git aliases look like that (borrowed from here):

首先,我的 git 别名看起来像这样(从这里借来的):

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

Now the bash function becomes quite simple:

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

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

This code first stages all current changes(you can remove this part, if you wish to stage the files yourself). Then creates the fixup(squash can also be used, if that's what you need) commit. After that it starts an interactive rebase with the --autosquashflag on the parent of the commit you give as the argument. That will open your configured text editor, so you could verify that everything is as you expect and simply closing the editor will finish the process.

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

The if [[ "$1" == HEAD* ]]part (borrowed from here) is used, because if you use, for example, HEAD~2 as your commit(the commit you want to fix current changes up with) reference then the HEAD will be displaced after the fixup commit has been created and you would need to use HEAD~3 to refer to the same commit.

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

回答by joeytwiddle

You can avoid the interactive stage by using a "null" editor:

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

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

This will use /bin/trueas the editor, instead of /usr/bin/vim. It always accepts whatever git suggests, without prompting.

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

回答by Valodim

What really bothered me about the fixup workflow was that I had to figure out myself which commit I wanted to squash the change into every time. I created a "git fixup" command that helps with this.

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

This command creates fixup commits, with the added magic that it uses git-depsto automatically find the relevant commit, so the workflow often comes down to:

此命令创建修复提交,并增加了使用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

This only works if the staged changes can be unambiguously attributed to a particular commit on the working tree (between master and HEAD). I find that is the case very often for the type of small changes I use this for, e.g. typos in comments or names of newly introduced (or renamed) methods. If this is not the case, it will at least display a list of candidate commits.

这仅适用于可以明确地将暂存更改归因于工作树上的特定提交(在 master 和 HEAD 之间)。我发现这种情况经常发生在我使用它的小变化类型中,例如注释中的拼写错误或新引入(或重命名)方法的名称。如果不是这种情况,它至少会显示候选提交列表。

I use this a lotin my daily workflow, to quickly integrate small changes to previously changed lines into commits on my working branch. The script is not as beautiful as it could be, and it's written in zsh, but it has been doing the job for me well enough for a good while now that I never felt the need to rewrite it:

我用这个了很多在我的日常工作流程,对我的工作的分支快速整合小的改动之前更改的行成的提交。脚本并不像它想象的那么漂亮,它是用 zsh 编写的,但它已经为我做了很长时间的工作,现在我从来没有觉得有必要重写它:

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

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

回答by Oktalist

commit --fixupand rebase --autosquashare great, but they don't do enough. When I have a sequence of commits A-B-Cand I write some more changes in my working tree which belong in one or more of those existing commits, I have to manually look at the history, decide which changes belong in which commits, stage them and create the fixup!commits. But git already has access to enough information to be able to do all that for me, so I've written a Perl scriptwhich does just that.

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

For each hunk in git diffthe script uses git blameto find the commit that last touched the relevant lines, and calls git commit --fixupto write the appropriate fixup!commits, essentially doing the same thing I was doing manually before.

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

If you find it useful, please feel free to improve and iterate on it and maybe one day we'll get such a feature in gitproper. I'd love to see a tool that can understand how a merge conflict should be resolved when it has been introduced by an interactive rebase.

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

回答by Alvaro

You can create a fixupfor a particular file by using this alias.

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

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

If you have made some changes in myfile.txtbut you don't want to put them in a new commit, git fixup-file myfile.txtwill create a fixup!for the commit where myfile.txtwas last modified, and then it will rebase --autosquash.

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

回答by joeytwiddle

I wrote a little shell function called gcfto perform the fixup commit and the rebase automatically:

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

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

For example, you can patch the second commit before the latest with: gcf HEAD~~

例如,您可以在最新提交之前修补第二次提交: gcf HEAD~~

Here is the function. You can paste it into your ~/.bashrc

这是函数。您可以将其粘贴到您的~/.bashrc

git_commit_immediate_fixup() {
  local commit_to_amend=""
  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'

It uses --autostashto stash and pop any uncommitted changes if necessary.

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

--autosquashrequires an --interactiverebase, but we avoid the interaction by using a dummy EDITOR.

--autosquash需要--interactiverebase,但我们通过使用 dummy 来避免交互EDITOR

--no-fork-pointprotects commits from being silently dropped in rare situations(when you have forked off a new branch, and someone has already rebased past commits).

--no-fork-point保护提交不会在极少数情况下被悄悄删除(当您分叉出一个新分支,并且有人已经重新调整了过去的提交时)。