07. Rebasing - ivomac/GitBasics GitHub Wiki

๐Ÿ’ Cherry-picking: Copying commits

  • Cherry-picking is applying the changes in a specific commit on top of HEAD in the repository.
  • ๐ŸŸข If the changes apply with no conflicts, a new commit is created.
  • ๐ŸŸก If the changes do not apply, conflict markers are left in the workdir to resolve.
  • ๐Ÿ’ You can apply several commits in a sequence. Cherry-picking stops at each conflict.
def cherry_pick(commits: list[Commit]) -> list[Commit]:
  • Cherry-picking creates a new commit with the same changes but a different parent.

๐Ÿ’ Cherry-pick Scenario

A โ—„โ”€ B โ—„โ”€ C โ—„ main โ—„ HEAD
     โ–ฒ
     D โ—„โ”€ E โ—„โ”€ F โ—„ feature
  • We try to cherry-pick commits D, E, and F (the feature branch):
A โ—„โ”€ B โ—„โ”€ C โ—„โ”€ G โ—„ main โ—„ HEAD
     โ–ฒ
     D โ—„โ”€ E โ—„โ”€ F โ—„ feature
  • ๐ŸŸข D applied successfully. diff(C, G) is (mostly) the same as diff(B, D).
  • ๐ŸŸก diff(E, D) does not apply properly on G. Conflicts need to be resolved. Example:
<<<<<<< HEAD         # HEAD is G
(ours content)
=======
(theirs content)
>>>>>>> 674d107      # (part of) E's hash
  • โŒ We could decide to abort:
    • ๐Ÿงน G is removed and we go back to the repo state before cherry-picking.
  • โš”๏ธ If we resolve all conflicts and eventually complete cherry-picking, the state will be:
A โ—„โ”€ B โ—„โ”€ C โ—„โ”€ G โ—„โ”€ H โ—„โ”€ J โ—„ main โ—„ HEAD
     โ–ฒ
     D โ—„โ”€ E โ—„โ”€ F โ—„ feature
  • ๐Ÿ G, H, J, correspond to D, E, F in that order.
  • ๐Ÿง™ J contains changes from main and feature and effectively merged both branches.

โœ๏ธ Rebasing

def rebase(new_base: Commit | Branch):
  • Rebasing moves a series of commits forward so that the new_base commit is in their history.
  • Rebasing is effectively a cherry-pick wrapper and a common merge alternative.
  • It is a destructive operation (when successful), creating/deleting commits and rewriting history.

โœ๏ธ Rebase Scenario

  • ๐ŸŸข We replay the previous scenario, starting with HEAD on feature.
A โ—„โ”€ B โ—„โ”€ C โ—„ main
     โ–ฒ
     D โ—„โ”€ E โ—„โ”€ F โ—„ feature โ—„ HEAD
  • Currently, C/main is not in the history of feature.
  • If feature is rebased successfully onto main, the new state will be:
A โ—„โ”€ B โ—„โ”€ C โ—„ main
          โ–ฒ
          G โ—„โ”€ H โ—„โ”€ J โ—„ feature โ—„ HEAD
  • ๐Ÿ Again: G, H, J, correspond to D, E, F, the same commits as in the cherry-pick scenario.
  • ๐Ÿ”‘ The original commits D, E, F, still exist in the repository but are no longer referenced by any branch.

๐Ÿ—ž Rebase: Cherry-pick wrapper

  • โœ‚ Rebasing is a sort of wrapper for cherry-picking, with extra branch handling.
  • In the scenario above, rebasing feature onto main is the same as:
  1. โž• Creating a new feature-tmp branch in main and switching to it:
         main
          โ–ผ
A โ—„โ”€ B โ—„โ”€ C โ—„ feature-tmp โ—„ HEAD
     โ–ฒ
     D โ—„โ”€ E โ—„โ”€ F โ—„ feature
  1. ๐Ÿ”ญ Identify the commits of feature not in main's history: D, E, F.
  2. ๐Ÿ’ Cherry-picking the feature's commits on top of feature-tmp:
         main
          โ–ผ
A โ—„โ”€ B โ—„โ”€ C โ—„โ”€ G โ—„โ”€ H โ—„โ”€ J โ—„ feature-tmp โ—„ HEAD
     โ–ฒ
     D โ—„โ”€ E โ—„โ”€ F โ—„ feature
  1. ๐Ÿงฝ Deleting feature and renaming feature-tmp to feature:
         main
          โ–ผ
A โ—„โ”€ B โ—„โ”€ C โ—„โ”€ G โ—„โ”€ H โ—„โ”€ J โ—„ feature โ—„ HEAD

โš”๏ธ Rebase Conflict Markers

  • During rebasing, HEAD first changes to the new base branch.
  • HEAD then moves forward as you cherry-pick commits one-by-one on top of the base branch.
  • If there are conflicts during cherry-picking, conflict markers are added:
    • โš ๏ธ The top version in the conflict markers will refer to the moving HEAD version.
    • โš ๏ธ The bottom version will refer to the commits being cherry-picked.
<<<<<<< HEAD         # will go through C/G/H
(ours content)
=======
(theirs content)
>>>>>>> 674d107      # will go through D/E/F
  • โ‰๏ธ This can be unintuitive since HEAD will start and end in the branch being rebased (feature/F).

๐Ÿ”„ Rebase vs Merge

  • Merge: Creates a new merge commit that combines changes from both branches.
  • Rebase: Rewrites history by creating new commits on top of the target branch.

๐Ÿ“Š Comparison

Merge Rebase
Preserves full history Creates a linear history
Non-destructive Rewrites commit history
Shows when branches were integrated Makes it look like work happened sequentially
Creates merge commits No extra merge commits

๐Ÿ”ง Interactive Rebasing

  • Interactive rebasing allows you to modify commits as you rebase them:
  • During interactive rebasing, you can:
    • ๐Ÿ‘Œ Pick: Keep the commit as is
    • โœ๏ธ Reword: Change the commit message
    • ๐Ÿ’ฑ Edit: Pause to amend the commit in the workdir
    • ๐Ÿ—œ Squash/Fixup: Combine with previous commit, keeping/dropping its messages
    • ๐Ÿ”ฅ Drop: Remove the commit entirely
    • ๐Ÿ”„ Reorder: Change the order of commits
  • Interactive rebasing in-place (without changing base) let's you rewrite a branch's commit history.

๐Ÿ—œ Squash/Fixup

  • Squash/Fixup combines multiple commits into one:
A โ—„โ”€ B โ—„โ”€ C โ—„ main
     โ–ฒ
     D โ—„โ”€ E โ—„โ”€ F โ—„ feature โ—„ HEAD
  • After squashing commits E and F during in-place rebasing:
A โ—„โ”€ B โ—„โ”€ C โ—„ main
     โ–ฒ
     D โ—„โ”€ G โ—„ feature โ—„ HEAD
  • ๐Ÿค G is a new commit with the combined changes of E and F.
  • ๐Ÿ’ฌ G's message will be a combination of E and F's messages.
  • ๐Ÿงน Fixup would discard F's message.

๐ŸŒฒ Interactive Rebase in SourceTree

  1. Right-click on the parent of the first commit that you want to modify.
  2. Select "Rebase children of [commit] interactively..."
  3. In the dialog that appears:
    • Reorder commits by dragging
    • Double-click a commit to edit its message
    • Right-click for more options (squash, edit, etc.)
  4. Click "OK" to start the rebase