Matthieu Vergne's Homepage

Last update: 06/01/2022 17:50:20

How to Fake a Git Commit in an Existing Commits History?

Context

Your commits history misses some changes. You don't necessarily want to change the final result, but you want to introduce a change that appears nowhere in your history. It might be some lost intermediary changes that you want to recover, or some changes made in another branch that you want to reproduce.

If the changes appears in a commit, you can split it instead.

Question

How to fake a Git commit in an existing commits history?

Method

Rewriting a Git history can be done in plenty of ways. Unfortunately, messing it up can be done as well. If you feel unsafe about rewriting the history, give a look here first. Better feel safe than sorry.

Faking a commit means introducing a commit that, at the end, does not change the state of the code. There is only two ways to do so: create an empty commit, or create a commit that we revert. An empty commit provides no change, so it is not what we want. We want here to create a commit with some content that we revert in another commit. Although the combination of both allows to maintain the state of the code, we still create a new commit with some new changes. We can then play with this commit (and its revert) depending on the result to achieve.

Introduce a Commit Manually

This is rather simple:


COMMIT_LAST=$(git rev-parse HEAD)              # Identify the last commit
COMMIT_BASE=<commit SHA-1>                     # Identify the base commit
MESSAGE_INSERT="<message>"                     # Message of the commit to insert
git reset --hard ${COMMIT_BASE}                # Place the branch on the base commit
<editing>                                      # Insert the change
git add .                                      # Stage the change
git commit -m "${MESSAGE_INSERT}"              # Commit the change
git revert --no-edit HEAD                      # Revert the change
git cherry-pick ${COMMIT_BASE}..${COMMIT_LAST} # Reproduce the remaining commits

At this point, you have inserted one commit with the change you want, and another which reverts it. You can now move them where you need and squash them with existing commits if required. If you need to preserve the change, just delete the revert commit.

Pick an Existing Commit

Picking an existing commit requires first to place yourself in a compatible state. Not necessarily the same state, but a state on which you can reproduce the commit with no conflict. You can do so in two ways:

  • select a base commit which is compatible
  • fake a compatible state by applying the manual procedure to create a compatible base commit and its revert

Once you have ensured that you have a compatible base commit, the procedure is highly similar:


COMMIT_LAST=$(git rev-parse HEAD)              # Identify the last commit
COMMIT_BASE=<commit SHA-1>                     # Identify the base commit
COMMIT=<commit SHA-1>                          # Identify the commit to insert
git reset --hard ${COMMIT_BASE}                # Place the branch on the base commit
git cherry-pick ${COMMIT}                      # Insert the commit
git revert --no-edit HEAD                      # Revert the change
git cherry-pick ${COMMIT_BASE}..${COMMIT_LAST} # Reproduce the remaining commits

Pick a Sequence of Commits

It is fundamentally the same thing than picking a single commit. The main difference is that all the sequence must be considered for the compatibility of the base commit and the operations to do. Once you have a commit compatible with the whole sequence:


COMMIT_LAST=$(git rev-parse HEAD)                # Identify the last commit
COMMIT_BASE=<commit SHA-1>                       # Identify the base commit
COMMIT=<commit SHA-1>                            # Identify the last commit to insert
LENGTH=<length>                                  # Identify the length of the sequence to insert
git reset --hard ${COMMIT_BASE}                  # Place the branch on the base commit
git cherry-pick ${COMMIT}~$((LENGTH))..${COMMIT} # Insert the commits sequence
git revert --no-edit HEAD~$((LENGTH))..HEAD      # Revert the commits sequence
git cherry-pick ${COMMIT_BASE}..${COMMIT_LAST}   # Reproduce the remaining commits

At this point, you have each commit of the sequence reproduced, but also a revert for each of them to pay with.

Pick Commits from Different Places

For this last case, there is no dedicated procedure. The important point is to understand that each (sequence of) commit(s) builds on a different state. Just consider one of them and execute the corresponding procedure, then repeat.

Answer

Faking a Git commit is a rather simple process: just start from a base commit with git reset --hard, create your new commit here, revert it with git revert, and reproduce the remaining commits of the branch with git cherry-pick. Although the idea itself is simple, a particular care should be taken when the new commit is taken from elsewhere with git cherry-pick. Indeed, the base commit must provide a state on which we can reproduce the commit with no conflict. Pay even more attention with an increasing number of commits to reproduce.

Bibliography