Skip to content

Instantly share code, notes, and snippets.

@ErnWong
Last active January 18, 2022 03:35
Show Gist options
  • Save ErnWong/d66cbc7dae0dadbbcf183497c923c092 to your computer and use it in GitHub Desktop.
Save ErnWong/d66cbc7dae0dadbbcf183497c923c092 to your computer and use it in GitHub Desktop.
Git rebase and merge hybrid workflow: git remap

Shower thought: Rebase and merge each have their strengths and weaknesses.

  • Rebase results in a clean, linear history and allows the developer to resolve conflicts within the context of the commit being replayed. Git bisects are also easier Nevermind - apparently, git bisects work with merge-based workflows.
  • Merge never loses the old history, and commits the merging step as if it was a legitiment piece of work and effort that needs to be documented. Indeed, some merges can be non-trivial and resolving conflicts are often cases where bugs are introduced.

Then, why not have a different kind of "merge" command that merges each commit one by one, so that, if you only log the commits from the first parent of every merge commit, you'll get a very clean linear git history, and if you need to, you can traverse throught the merge commit's second parents to look back at the original commits. Here, the merge commits fully describe how the "old" commits map into the "new" commits.

Best of both worlds?

Of course, there are probably many corner cases that needs to be resolved first.

Cases

A1---A2---A3 master
      \
       B1----B2----B3 feature
git checkout feature
git remap master
A1---A2---A3 master
      \     \
       \      B1'---B2'---B3' feature
        \     /     /     /
         `--B1----B2----B3
git checkout master
git merge feature
> fast-forward merge
A1---A2---A3---B1'---B2'---B3' master
      \       /     /     /
       `-----B1----B2----B3

Rebase --onto may not work properly anymore: there are unwanted commits which we don't want merged in:

                                  .- Commit gets lost if merged automatically, since X1 is already an ancestor
                                  |
                                  v
A1---A2---A3---B1'---B2'---B3'---X1'---X2'---X3'---X4'---X5' master
      \       /     /     /     /     /     /     /     /
       \    B1----B2----B3     /     /     /     /     /
        \  /                  /     /     /     /     /
         X1------------------+-----X2----X3----X4----X5

Squashing = octopus merge:

A1---A2---A3-----------B13'---B4' master
      \    .-----+-----/      /
       \  /     /     /      /
        B1----B2----B3------B4

Dropping: Note: dangerous if commit is needed to be merged later on? What if it is re-mapped?

A1---A2---A3---B1'--------B3' master
      \       /           /
       `-----B1----B2----B3
A1---A2---A3---B1'--------B3' master
      \       /           /
       `-----B1----B2----B3
                     \
                      B2'---C1 feature
A1---A2---A3---B1'--------B3'---B2'---C1' master
      \       /           /    /     /
       `-----B1----B2----B3   /     /
                     \       /     /
                      B2'---+-----C1

Fixup: e.g. correcting commit message, or changing file contents in previous commit B1:

A1---A2---A3---A4---A5 master
A1---A2---A3'---A4'---A5' master
      \  /     /     /
       A3----A4----A5

Some possible solutions regarding the dropped commits:

  • Upon git remap, do a rebase of the original commits first to detach them from the original commits and ensure that any other references (e.g. branches) to those commits no longer reference the same commits. That way, when merging the dropped commits back into the master branch, it does not get silently removed.
  • Prevent git remap commands with drop instructions or the use of the --onto variant when there are other references to the dropped commits? This is iffy.

Handling of a mixture of squashes and splits

Suppose the following interactive rebase was done:

pick A
squash B
squash C
fixup D
fixup E
edit F

with the following linear graph:

A---B---C---D---E---F

Then rebase will stop at B, C and F to let the user write the commit message.

TODO: consider what should happen when the intention was to squash, but it was aborted and commit was split. Consider what should happen when the intention was to edit, but it was instead split into two commits, with the first commit squashed to the previous one.

The case when some of B2 should be in B1, some of B3 should be in B2, and some of B4 should be in B3:

A1---A2---A3---B12'--B23'--B34'--B4 master
      \      .~'/  .~'/  .~'/  .~'
       \  .~'  /.~'  /.~'  /.~'  
        B1----B2----B3----B4
for commit in $(git rev-list HEAD --first-parent)
do
git --no-pager log $commit --max-count=1
done
# Not tested - just an idea. Will this work?
function map-rebased-commit {
# But here, we need to figure out what the old commits were
done_list=$(grep -v "^drop" $(git rev-parse --git-path rebase-merge/done)) # Remove dropped commits
done_commands=($(echo "$done_list" | grep -o "^\(pick|reword|edit|squash|fixup\)"))
last_new_command=0
for i in "${!done_commands[@]}"
do
# Obviously do something better than this...
if [[ ${done_commands[$i]} = "pick" ]]
then
last_new_command=$i
fi
if [[ ${done_commands[$i]} = "reword" ]]
then
last_new_command=$i
fi
if [[ ${done_commands[$i]} = "edit" ]]
then
last_new_command=$i
fi
done
old_commits=($(echo "$done_list" | grep -o "\w\{40\}" | tail -n +${last_new_command}))
# and get the new tree
rebased_commit=($(git cat-file -p HEAD))
new_tree=${rebased_commit[1]}
parent=${rebased_commit[3]}
message=$(git show -s --format=%B)
new_commit=$(git commit-tree -p $parent "${old_commits[@]/#/-p }" -m $message $new_tree)
git reset --hard $new_commit
}
git rebase --exec "map-rebased-commit" $@
for commit in $(git rev-list ^HEAD $1 --first-parent)
do
git merge $commit
# TODO: detect merge conflict and pause command until resolved
# TODO: Using merge is not the correct approach, probably.
# Better to either use cherry-pick or use the existing rebase operation,
# then manually add the parent information to form the final commits.
# This is because merge by itself will try to merge even unwanted commits.
done
@ErnWong
Copy link
Author

ErnWong commented Jan 18, 2022

I know this gist is probably very outdated now compared with my current ideas, but I need somewhere to jot this down:

https://www.git-scm.com/docs/git-range-diff

The cost matrix and least cost assignment algorithm mentioned in the bottom might be useful for us to figure out how to reassociate the commits when we perform an undo operation.

@ErnWong
Copy link
Author

ErnWong commented Jan 18, 2022

git-range-diff can also be used to show the diff when resolving diverged remappings

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment