programing

과거 커밋을 어떻게 쉽게 수정할 수 있습니까?

nasanasas 2020. 8. 20. 18:58
반응형

과거 커밋을 어떻게 쉽게 수정할 수 있습니까?


나는 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.

배관 도구 등을 사용하여 커밋을 다시 작성할 수있는 스마트 스크립트일까요?


업데이트 된 답변

얼마 전에 적합한 로그 메시지로 커밋을 구성하는 데 사용할 수 있는 새 --fixup인수가 추가되었습니다 . 따라서 과거 커밋을 수정하는 가장 간단한 방법은 다음과 같습니다.git commitgit 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

원래 답변

다음 git fixup은 내가 원래 질문에서 원했던 논리 를 구현하는 잠시 전에 작성한 작은 Python 스크립트 입니다. 스크립트는 일부 변경 사항을 준비한 다음 해당 변경 사항을 주어진 커밋에 적용한다고 가정합니다.

참고 :이 스크립트는 Windows 전용입니다. 사용하여 환경 변수를 찾고 git.exe설정합니다 . 다른 운영 체제에 대해 필요에 따라이를 조정하십시오.GIT_EDITORset

이 스크립트를 사용하여 내가 요청한 '손상된 소스 수정, 단계 수정, 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)

내가하는 일은 :

git add ... # 수정 사항을 추가합니다.
git commit # 커밋되었지만 잘못된 위치에 있습니다.
git rebase -i HEAD ~ 5 # 리베이스를 위해 마지막 5 개 커밋을 조사합니다.

편집 할 수있는 마지막 5 개의 커밋 목록과 함께 편집기가 열립니다. 변화:

08e833c 좋은 변화를 선택하십시오.
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...


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

Add this to your .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' -"

Example usage:

git add -p
git fixup HEAD~5

However if you have unstaged changes, you must stash them before the 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.


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.

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


UPDATE: A cleaner version of the script can now be found here: 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:

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

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

Now the bash function becomes quite simple:

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
}

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 --autosquash flag 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.

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.


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

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

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


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.

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

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

I use this a lot in 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:

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


commit --fixup and rebase --autosquash are great, but they don't do enough. When I have a sequence of commits A-B-C and 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 script which does just that.

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

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 git proper. 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.


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

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

Here is the function. You can paste it into your ~/.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 \
                         --preserve-merges "${commit_to_amend}~"
}

alias gcf='git_commit_immediate_fixup'

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

--autosquash requires an --interactive rebase, but we avoid the interaction by using a dummy EDITOR.


I'm not aware of an automated way, but here's a solution that might by easier to human-botize:

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

You can create a fixup for 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 $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' -"

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

참고URL : https://stackoverflow.com/questions/3103589/how-can-i-easily-fixup-a-past-commit

반응형