How can I easily fixup a past commit?

前端 未结 12 2224
执笔经年
执笔经年 2020-12-02 03:48

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

相关标签:
12条回答
  • 2020-12-02 04:32

    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.

    0 讨论(0)
  • 2020-12-02 04:34

    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
    
    0 讨论(0)
  • 2020-12-02 04:36

    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

    0 讨论(0)
  • 2020-12-02 04:41

    UPDATED ANSWER

    A while ago, a new --fixup argument was added to git commit which 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:

    $ 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 fixup logic I hoped for in my original question. The script assumes that you staged some changes and then applies those changes to the given commit.

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

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

    #!/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)
    
    0 讨论(0)
  • 2020-12-02 04:49

    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.

    0 讨论(0)
  • 2020-12-02 04:49

    I'd recommend https://github.com/tummychow/git-absorb:

    Elevator Pitch

    You have a feature branch with a few commits. Your teammate reviewed the branch and pointed out a few bugs. You have fixes for the bugs, but you don't want to shove them all into an opaque commit that says fixes, because you believe in atomic commits. Instead of manually finding commit SHAs for git commit --fixup, or running a manual interactive rebase, do this:

    • git add $FILES_YOU_FIXED

    • git absorb --and-rebase

    • or: git rebase -i --autosquash master

    git absorb will automatically identify which commits are safe to modify, and which indexed changes belong to each of those commits. It will then write fixup! commits for each of those changes. You can check its output manually if you don't trust it, and then fold the fixups into your feature branch with git's built-in autosquash functionality.

    0 讨论(0)
提交回复
热议问题