Search code examples
gitgit-commitgit-amend

Git: how to modify the content (not commit message) for the HEAD^^ commit


Suppose I worked on:

  • feature 1: file1 + file3
  • feature 2: file2

and I have the following commits

  • Commit 2: feature 2 completed.
  • Commit 1: Feature 1 completed.

The wrong thing I made is I forget to add the file3 to commit 1. How can add the changes of file3 to commit 1?

One way of this is:

git reset --soft Commit 1
git add file3
git commit --amend 
git add file2
git commit -m "feature 2 completed"

I don't want this way is because in fact I may not only have commit 2 but also have commit 3,4,5,6...

is there other easy way to do this?

Thanks


Solution

  • Technically, you cannot modify any commit. Even git commit --amend does not do that. Remember that each commit is uniquely identified by its hash ID.

    What you can do is take some existing commit, extract it, and use it to build a new, different commit that is almost the same as the original commit, but with whatever it is that you consider flawed, fixed. You must then convince everyone—yourself, and anyone who has cloned the commit—to stop using the old, flawed commit, using the shiny new one instead. The new commit has a new, different hash ID.

    If the shiny new commit is not at the tip of a branch—if it's two steps back, as in this case, for instance—then you have an additional problem. Remember, branch names like master and develop identify one particular commit, which Git calls the tip commit of the branch. Each commit also contains the hash ID of its parent (previous) commit, and this is how Git finds the earlier commits from the latest:

    ...--D--E--F--G   <-- master
             \
              H--I--J--K   <-- develop
    

    Here, the uppercase letters stand in for the big ugly hash IDs. The name master identifies commit G: the tip of master. The name develop identifies commit K, the tip of develop. From commit G, Git can work backwards to F, then E, and so on. From K, Git can work backwards to J, and eventually to E and so on.

    If there is a problem in commit I, what you can do is have Git extract commit I, make some change to the index-and-work-tree pair (just like you would do for any other commit), and then make a new commit—let's call it I' to indicate that it's the replacement for I—whose parent is the same as I's parent:

    ...--D--E--F--G   <-- master
             \
              H--I--J--K   <-- develop
               \
                I'  <-- temporary
    

    Having done that, we now just need to have Git copy J to a shiny new J' whose parent is I' and whose effect on I' is the same as J's effect on I:

    ...--D--E--F--G   <-- master
             \
              H--I--J--K   <-- develop
               \
                I'-J'  <-- temporary
    

    Now we repeat the trick for K to make K':

    ...--D--E--F--G   <-- master
             \
              H--I--J--K   <-- develop
               \
                I'-J'-K'   <-- temporary
    

    Finally, we tell Git: Drop the temporary name. Make the name develop identify shiny new commit K' instead of icky old K. This gives us:

    ...--D--E--F--G   <-- master
             \
              H--I--J--K   [abandoned]
               \
                I'-J'-K'   <-- develop
    

    and if we stop drawing the abandoned branch it looks as though we have somehow changed three commits. But we have not: the new commits have new, different hash IDs.

    ...--D--E--F--G   <-- master
             \
              H--I'-J'-K'  <-- develop
    

    This is what git rebase -i does. It sounds like a lot of work, and in some cases it can be, but usually git rebase -i makes it easy and painless for you.

    Note that if you have pushed develop elsewhere, you must now force-push this new develop, to tell the other Git that received commits I, J, and K that it should forget those commits (and any later ones as well!) and instead use the shiny new replacements.

    If other users have picked up the original I-J-K sequence, you must convince them to discard their I-J-K copies too. If they have built their own commits atop K, you have to get them to copy their old commits atop your shiny new K'. This can be noticeable amounts of work for all the other users, so be sure you really want to do this to them.