Foundations: Python, Git and toolsLoading progress…

Rebase and undo

Trace commits and branches, choose an appropriate undo operation, and resolve a small merge conflict.

Rebase: replay your commits somewhere else

git rebase main takes the commits that exist only on your branch and re-creates them, one at a time, on top of main. The result is a straight line. The copies are new commits: they have a different parent, so they get different ids. The originals still exist but nothing names them.

Before (on feature)

c354b72 M1c354b72M10fc7878 F10fc7878F13bb8440 F23bb8440F2737e9cf M2737e9cfM2HEAD → featuremain

git merge main

c354b72 M1c354b72M10fc7878 F10fc7878F13bb8440 F23bb8440F2737e9cf M2737e9cfM2b259c23 Merge branch 'main' into featureb259c23Merge bran…HEAD → featuremain

git rebase main

c354b72 M1c354b72M10fc7878 F10fc7878F13bb8440 F23bb8440F2737e9cf M2737e9cfM2ca456f0 F1ca456f0F19ce5ab6 F29ce5ab6F2HEAD → featuremain

Merge keeps F1 and F2 and adds one commit that joins the lines. Rebase builds new copies of F1 and F2 on top of M2: look at their ids, they changed. The dashed originals are what feature used to be.

Same starting history, two ways to combine it.

Undoing: move a pointer, or add a commit

git reset moves the current branch label to another commit. Its three modes differ in how far the change reaches: --soft moves only the label, --mixed (the default) also resets the staging area, --hard also overwrites your files. git revert is different: it leaves history alone and adds a commit that does the opposite.

Start: last commit C3, plus one staged edit and one newer unstaged edit of a.txt

Working treefiles on disk
Staging areathe next snapshot
Repositorythe last commit
git add →
git commit →
← restore --staged
a.txt
a4e5893 · edited
a5 (unstaged)
0fd7eb8 · staged
a4 (staged)
d616f73
a3
b.txt
e6bfff5
b2
e6bfff5
b2
e6bfff5
b2

new or untracked differs from the area to its right in sync

Pick an undo and compare the graph and the three areas before and after.

Predict first. A bad commit is already pushed to a shared branch that colleagues pulled. Which undo is safer?

Chapter context

Git records snapshots in a graph of commits. Branch names are movable references to commits. Treat the working tree, staging area and recorded history as distinct states, and many apparently mysterious commands become predictable.

Chapter reference: math, worked example & code

This section belongs to the whole chapter, so it is the same on every lesson in it and may cover a different lesson's topic. The code for the experiment you just ran is inside the experiment itself.

Math and notation

Model history as a directed acyclic graph. A commit is reachable from a branch when following parent edges reaches it. A merge preserves both ancestry paths; rebasing creates a different path with new commit identities.

Worked example

Let B have parent A, C have parent B, and D have parent B. A merge M with parents C and D makes both C and D reachable. Moving a branch from C to M is different from copying D's files over C: the graph also records why those histories meet.

Reference code

The following example uses the Python standard library and does not call a remote service.

parents = {'A': [], 'B': ['A'], 'C': ['B'], 'D': ['B'], 'M': ['C', 'D']}
def ancestors(commit):
    seen, todo = set(), [commit]
    while todo:
        node = todo.pop()
        if node not in seen:
            seen.add(node)
            todo.extend(parents[node])
    return seen
assert ancestors('M') == {'A', 'B', 'C', 'D', 'M'}

Python examples are provided for study and local execution; this page does not execute Python.

Common mistakes

Git commit does not automatically upload data. Rebase changes commit identities. Reset modes have different effects on uncommitted work.