Search code examples
gitgithubgit-commit

Edit a specific commit and reapply subsequent commits


I am currently submitting open-source code and the structure of commits are quite strict. I have three commits, let's call them A, B and C. It is important to note that all changes are within the same file.

Furthermore, I have received feedback on code within all three commits. Is it possible to drop into the staged code of, for example, commit B, edit a few lines of code, recommit and then reapply C without much hassle?

The best way I can think of right now is to undo all three commits and then go through the chunk staging again, which will take time to get perfect, since some code are in proximity.


Solution

  • However you achieve the result, when you are done, you will not use the original three commits. If you're allowed to leave A 100% alone, you can keep the original A, but if you must touch B, you won't have the original B any more, and must therefore make a new copy of C as well.

    So far, that's just a statement of fact, and not advice on how to achieve what you want. The way to get what you want is—usually—to use git rebase -i.

    Let's say you're on branch feature right now:

    ...--o--o--o--o   <-- main
                   \
                    A--B--C   <-- feature (HEAD)
    

    You simply run git rebase -i main and Git comes up with an instruction sheet that tells Git to keep the three commits as-as:

    # instructions
    pick <hash-of-A> <subject-of-A>
    pick <hash-of-B> <subject-of-B>
    pick <hash-of-C> <subject-of-C>
    

    Change the second pick to edit and write the instruction sheet back and exit your editor.1 Git will now start by trying to copy commit A directly in place, which will succeed. It will then continue by trying to copy commit B in place, which will also succeed, but now it will stop in the middle of this copying:

    ...--o--o--o--o   <-- main
                   \
                    A--B   <-- HEAD
                        \
                         C   <-- feature
    

    You will be in detached HEAD mode, with HEAD selecting commit B.

    You can now make changes to the file(s) you would like to change, git add, and run git commit --amend. The --amend will have Git make the new commit—let's call it B'—using commit A as its parent, instead of commit B. The result looks like this:

    ...--o--o--o--o   <-- main
                   \
                    A--B'  <-- HEAD
                     \
                      B--C   <-- feature
    

    You can now run git rebase --continue to make Git go on to the pick C command. This will cherry-pick commit C, making a new commit that we'll call C'. Some merge conflicts can occur here, because cherry-pick is actually a merge. If so, you'll need to fix them up and resume again before commit C' can be made. If no conflicts occur, though, we are now in this state:

    ...--o--o--o--o   <-- main
                   \
                    A--B'-C'  <-- HEAD
                     \
                      B--C   <-- feature
    

    This completes the set of operations that the interactive rebase is to perform, so it now does the last trick of any rebase, which is to yank the branch name to "here" (wherever HEAD is now) and re-attach your HEAD:

    ...--o--o--o--o   <-- main
                   \
                    A--B'-C'  <-- feature (HEAD)
                     \
                      B--C   [abandoned]
    

    Should you want to see the original commits, they still exist: you just have to find the hash ID of original commit C. This is available in:

    • ORIG_HEAD, for just a brief time (because ORIG_HEAD keeps getting overwritten);
    • the reflog for HEAD, for at least 30 more days by default; and
    • the reflog for branch feature, for at least 30 more days by default.

    The reflog entries have numeric-suffixed names: feature@{1}. You can also use time-relative names, such as HEAD@{yesterday}. Generally, if some name has had more than one change in any given time period, you'll want to run git reflog, though, instead of trying to guess something like "yesterday.10.am".


    1If you have a long-running editor (some variants of emacs, atom, etc), use whatever method it has to signal back to the waiting Git that this one file is done now and Git should resume.