I am attempting to clean up the git history in a repo I'm working on. Given a git history that looks like this:
. -- .
/ \
... - S ... T - ... H
\ /
. -- .
That is:
- There is some arbitrary DAG behind point S.
- There is some arbitrary DAG after point T.
- There is some arbitrary DAG between point S and T.
- The graph before S is disjoint from the graph after T (i.e. removing T or S will disconnect the removed nodes predecessors from H).
I would like to rewrite the history between point S and T (e.g. squash or linearize), such that there is some new git history that ends with point T'.
... - S -- ... -- T'
The critical constraint is that the contents of the repo at point T and T' are exactly the same, even though the git commit is different and the way we got from S to T' might have changed.
This much I can do. What I would like to do after this (and i haven't had luck doing so yet) is to transplant the exact structure of the DAG inclusively between T and H to get:
... - S -- ... -- T' - ... H'
Of course the commit hashes will change, but what's important is that the graph structure, authors, and other meta data between T' and H' is the same.
I would have though I could do this with a cherry-pick:
git cherry-pick T^..H
but this seems to result in merge conflicts. I was looking for answers in this SO post: How to cherry-pick a range of commits and merge them into another branch? but either I'm invoking rebase --onto
incorrectly, or these answers aren't a solution to my question.
To make this more concrete I have a MWE. Consider the following code:
The following code constructs this example:
mkdir -p "$HOME/tmp/tmprepo"
rm -rf "$HOME/tmp/tmprepo"
mkdir -p "$HOME/tmp/tmprepo"
cd "$HOME"/tmp/tmprepo
git init
git checkout -b main
echo "state01" > state && git add state && git commit -m "Initial commit"
echo "state02" > state && git add state && git commit -m "Modify state"
git checkout -b branch1
echo "state03" > state && git add state && git commit -m "Modify state"
echo "state04" > state && git add state && git commit -m "Modify state"
echo "state05" > state && git add state && git commit -m "Modify state"
git checkout main
git checkout -b branch2
echo "state06" > state && git add state && git commit -m "Modify state"
echo "state07" > state && git add state && git commit -m "Modify state"
echo "state08" > state && git add state && git commit -m "Modify state"
git checkout main
git merge branch2 --no-ff -m "merge commit"
git merge branch1 -s ours --commit --no-edit --no-ff -m "merge commit"
git checkout -b branch3
echo "state09" > state && git add state && git commit -m "Modify state - WANT TO SQUASH"
git tag "Point1"
echo "state10" > state && git add state && git commit -m "Modify state - WANT TO SQUASH"
git checkout main
git merge --no-ff -m "merge commit - WANT TO SQUASH" branch3
git tag "Point2"
git checkout main
git checkout -b branch4
echo "state11" > state && git add state && git commit -m "Modify state"
echo "state12" > state && git add state && git commit -m "Modify state"
echo "state13" > state && git add state && git commit -m "Modify state"
git checkout main
git checkout -b branch5
echo "state14" > state && git add state && git commit -m "Modify state"
echo "state15" > state && git add state && git commit -m "Modify state"
echo "state16" > state && git add state && git commit -m "Modify state"
git checkout main
git checkout -b branch6
echo "state17" > state && git add state && git commit -m "Modify state"
echo "state18" > state && git add state && git commit -m "Modify state"
echo "state19" > state && git add state && git commit -m "Modify state"
git checkout branch5
git merge branch6 -s ours --commit --no-edit --no-ff -m "merge commit"
git checkout main
git merge branch5 -s ours --commit --no-edit --no-ff -m "merge commit"
git merge branch4 -s ours --commit --no-edit --no-ff -m "merge commit"
This creates this git history:
As an example I want to squash the commits between Point1 and Point2, and then apply the rest of the history after Point2.
I can do the squash like this:
# Squash all information between point1 and point2
git checkout Point1
git reset --hard Point2
git reset --soft Point1^
git commit -am "all changes between point1 and point2"
git tag "Point2_prime"
which gives us this:
But I can't figure out how to get the rest of the history on top of it. This is what I've tried so far:
# The state is now guarenteed to be the same as Point2, but the history has
# been modified to our liking. Now we need to replay all the other commits
# on top of this.
# Based on answers in this SO post:
# https://stackoverflow.com/questions/1994463/how-to-cherry-pick-a-range-of-commits-and-merge-them-into-another-branch
COMMIT_A=$(git rev-list -n 1 Point2)
COMMIT_B=$(git rev-list -n 1 main)
echo "COMMIT_A = $COMMIT_A"
echo "COMMIT_B = $COMMIT_B"
# I've tried the following, but they do not seem to work.
# Try with cherry pick
git cherry-pick "${COMMIT_A}..${COMMIT_B}"
# Try with rebase onto
git rebase "$COMMIT_A" "$COMMIT_B"~0 --onto HEAD
I would think because the state of the new commit is exactly the same as the state at Point2, there would be a way to do this non-interactively without merge errors. Is this possible?