Git & collaboration, section 1 of 2, 5h

Git as you actually use it

By the end: You can work on a team without losing work or producing unreviewable pull requests.

  • The mental model: three places a file can be
  • Branching and merging
  • Rebase, and when not to
  • Undoing things safely
  • Pull requests people can review

New to git & collaboration? Git records the history of a set of files: every change, who made it, and when. It lets several people edit the same project at once without overwriting each other, and lets you return to any earlier state. GitHub is a website that hosts those histories and wraps a review conversation around them. Why it matters

Git is the tool you touch on every working day, and most of the trouble people have with it comes from not knowing where a change currently lives. This section gives you that model first, then the handful of commands that follow from it: branching, merging, undoing safely, and finding the commit that broke something. Without it you lose work that was never recoverable, or produce a pull request nobody can review.

A repository is a graph of commits#

Almost every confusing thing git does stops being confusing once you hold one picture: a repository is a graph of commits, each one pointing back at the commit or commits it came from, and a branch is nothing but a name stuck to one node in that graph. Every command in this section either adds a node, moves a name, or both. Build a small repository and look at it rather than taking that on trust.

mkdir demo && cd demo && git init -b main
git config user.name Ada && git config user.email ada@example.com

for n in 1 2 3; do echo "line $n" >> app.py; git add app.py; git commit -m "commit $n"; done
git switch -c feature
echo "feature work" >> feature.py && git add -A && git commit -m "add feature.py"
echo "more feature" >> feature.py && git add -A && git commit -m "extend feature.py"
git switch main
echo "line 4" >> app.py && git add -A && git commit -m "commit 4 on main"

git log --graph --oneline --all
Four commits on main, two on a branch that left after the third. Every hash printed below will be different in your copy; the shape will not.
* 435ccff extend feature.py
* b2245a1 add feature.py
| * fdd7b5c commit 4 on main
|/
* 5c7464c commit 3
* 480943e commit 2
* 10a008f commit 1
The output of that last command. Read it bottom up: history flows upward and the two lines are the two branches.

Now look at what the pieces actually are, because both are smaller than people expect. A branch is a forty-character file. A commit is a snapshot plus a pointer to its parent.

cat .git/refs/heads/main
# fdd7b5c434ebec6f9e0859fabb8b54eb66d18ef7

cat .git/HEAD
# ref: refs/heads/main

git cat-file -p HEAD
# tree f74d34831129b127c5c4d7a4376604a2e499fea2
# parent 5c7464c29e79ff0dc498534a9a52465a45d3d87e
# author Ada <ada@example.com> 1789324096 +0530
# committer Ada <ada@example.com> 1789324096 +0530
#
# commit 4 on main
A branch is a file containing one hash. HEAD is a file saying which branch you are on. Creating a branch writes 41 bytes, which is why it is instant and why deleting one loses nothing.

Three consequences follow directly, and between them they answer most beginner questions. Creating and deleting branches is free and reversible, because the commits are not stored in the branch — the branch only names one of them. A commit is identified by a hash of its content, its parents and its metadata, so any command that changes a commit's contents or its parent produces a different commit with a different hash rather than editing the old one. And nothing in the graph is ever removed by an ordinary command: commits that no branch points at are still on disk, which is what makes the recovery moves in the next section work.

Three places#

Alongside the graph there is the staging area, and most day-to-day confusion comes from not knowing which of three places a change is in. The working directory is your files. The staging area is what you have marked for the next commit. The repository is committed history.

git status                # what is where, read this constantly
git add file.py           # working directory -> staging
git commit -m "message"   # staging -> repository
git push                  # repository -> remote

Branching#

git switch -c feature/retrieval-eval    # create and move to it
git switch main                        # move back
git branch -d feature/retrieval-eval   # delete when merged

Branch names are read by other people. A name describing the change beats a ticket number alone, and a ticket number plus a description beats both.

Merge or rebase#

Merge keeps both histories and adds a merge commit. Rebase replays your commits on top of the target, producing a straight line. Rebase makes history readable, and it rewrites commits, which is why it has a rule attached.

In the graph both are one sentence. Merge adds a node with two parents and moves the branch name onto it. Rebase makes new copies of your commits with a different parent, then moves the branch name onto the last copy. Run each on a copy of the demo repository and compare.

git switch main && git merge feature          git switch feature && git rebase main

*   d983e7b Merge branch 'feature'               * 24e98d7 extend feature.py
|\                                               * e5ebea2 add feature.py
| * 435ccff extend feature.py                    * fdd7b5c commit 4 on main
| * b2245a1 add feature.py                       * 5c7464c commit 3
* | fdd7b5c commit 4 on main                     * 480943e commit 2
|/                                               * 10a008f commit 1
* 5c7464c commit 3
* 480943e commit 2
* 10a008f commit 1
Same two commits of work, two different graphs. On the right, b2245a1 and 435ccff have become e5ebea2 and 24e98d7 - identical changes, different commits, because the parent changed and the parent is part of the hash.

That right-hand column is the whole reason for the rule below. The old commits b2245a1 and 435ccff still exist on your disk, but nothing points at them any more, and anyone else who had fetched them now holds commits that your branch has never heard of. Their git will offer to merge the two versions of the same work back together, which produces every commit twice.

Never rebase anything you have pushed and others have pulled

Rebasing rewrites commits, so their copy and yours diverge. Rebase your own unpushed work freely. Once it is shared, merge instead.

The merge commit on the left is also worth one more look, because it is the only kind of commit with two parents and that shape matters later. It is what lets git work out, for any commit, whether it is an ancestor of main, which is how a pull request knows it has been merged. It is also why reverting a merge needs telling which of the two parents to keep, covered in the recovery section. And if main had not moved since the branch left, git would not have created a node at all: it would simply have slid the name main forward onto the branch tip, which is what the phrase fast-forward means.

What reset actually resets#

Reset is the command people are most afraid of, and the fear is aimed at the wrong part of it. Reset always moves the current branch name to the commit you give it. The flag decides which of the other two places follow it there. Here is the same repository, reset back one commit three times, with the resulting status and the contents of the file.

git reset --soft  HEAD~1   HEAD=5c7464c   status: M_ app.py   app.py has 4 lines
git reset --mixed HEAD~1   HEAD=5c7464c   status: _M app.py   app.py has 4 lines
git reset --hard  HEAD~1   HEAD=5c7464c   status: (clean)     app.py has 3 lines

M_  staged: the change is in the index, ready to commit again
_M  not staged: the change is in your files only
--hard  the fourth line is gone from the file, and was never committed elsewhere
All three move the branch to the same commit. Only the third one touches your files. --mixed is the default, so a bare git reset is the middle row.

Read from that what each is for. Soft reset is how you squash the last few commits into one: move back three, everything is staged, commit once. Mixed reset is how you unstage and rethink what belongs in the commit. Hard reset is how you throw the work away, and it is the only one of the three that can lose anything, because the commits it leaves behind are still recoverable through the reflog while uncommitted file changes are not.

Undoing#

git restore file.py              # discard uncommitted changes to a file
git restore --staged file.py     # unstage, keep the changes
git commit --amend               # fix the last commit (before pushing)
git revert <sha>                 # undo a commit with a new commit, safe
git reset --hard <sha>           # move the branch, DESTROYS uncommitted work

git reflog                       # the safety net: every state you have been in
reflog is the command that gets work back when something has gone badly wrong. Learn it before you need it.

Prefer revert to reset on anything shared. Revert adds a commit that undoes the change, which is honest history. Reset pretends the change never happened, which breaks everyone who already has it.

Finding when something broke#

git bisect start
git bisect bad                   # current state is broken
git bisect good v1.4             # this version was fine
# git checks out midpoints; mark each good or bad
git bisect reset
Binary search through history. Finds the breaking commit in about log2(n) steps.

Pull requests#

  • Keep them small. A 2000-line pull request gets approved without being read, which is worse than not being reviewed.
  • One concern per pull request. Refactoring and behaviour change together makes both unreviewable.
  • Write the description for someone who was not in the conversation: what, why, and what you considered.
  • Review your own diff before requesting review. You will find something, every time.

Data and model weights do not belong in git. Git stores a full copy of every version of a binary file, so a repository with model checkpoints in it becomes unusable quickly, and because history is immutable the size never comes back down once it has gone in — removing the file in a later commit leaves every earlier copy in the history that everyone clones. Use object storage with a versioned path, or a dedicated data versioning tool, and commit the reference.

What to skip#

Git has a very large surface and most of it is never used by working engineers. The commands above, plus the recovery moves in the next section, cover essentially everything that comes up on a normal team. A few things are worth deliberately not learning until something forces you to.

  • Submodules. They solve a real problem badly and every team that adopts them spends time on them forever. If you are given a repository that uses them, learn git submodule update --init --recursive and no more.
  • Branching models with names. Gitflow and the rest are process documents rather than git features, and the branch commands underneath are the ones above. Learn the model your team uses when you join it.
  • Interactive rebase beyond squashing. Reordering, splitting and editing old commits is worth knowing exists and is not worth practising. Squashing before review covers almost every real need.
  • Custom merge drivers, git hooks written by hand, and anything involving git filter-branch. Modern equivalents exist, and the cases that need them are rare enough that you should read the documentation at the time rather than carrying it around.
  • Memorising flags. The four worth having in your fingers are git status, git log --oneline --graph, git diff and git reflog. Everything else can be looked up without the lookup costing you anything.

Where people go wrong

git reset --hard with uncommitted work in the directory. It is unrecoverable through normal means, because the work was never committed and reflog only tracks commits.

If this comes up in an interview

What is a branch in git?

A file containing one commit hash, plus an entry in HEAD saying which branch you are on. The commits are not stored in the branch, so creating and deleting branches is instant and costs nothing, and deleting one does not delete any commits.

Then they push further

Then why does rebasing change the commit hashes?

A commit's id is a hash of its contents, its metadata and its parent. Rebase gives the commits a different parent, so they hash differently. The originals are still on disk until they are garbage collected, which is why the reflog can get them back.

What gets marked up. Strong answers describe the graph and the pointer. Describing a branch as a copy of the code is the answer that predicts trouble later.

What is the difference between git reset --soft, --mixed and --hard?

All three move the current branch to the commit you name. Soft leaves your files and the staging area alone, so the changes from the undone commits are staged and ready to recommit. Mixed, the default, leaves your files alone but unstages. Hard overwrites your files from the commit, and is the only one of the three that can destroy work that was never committed.

What is the difference between merge and rebase?

Merge combines two histories and adds a merge commit, preserving what actually happened. Rebase replays your commits on top of the target, producing a linear history but rewriting the commits. Never rebase anything already pushed and pulled by others.

How do you undo a commit that is already on the shared branch?

git revert, which adds a new commit undoing the change. Reset rewrites history and breaks everyone who already pulled it.

How would you find which commit introduced a bug?

git bisect. Mark a known good commit and a known bad one, and it binary searches the range, so a thousand commits takes about ten checks.

Why not commit model weights to git?

Git stores a full copy of every version of a binary, so the repository grows enormously and clones become impractical. Store weights in object storage with a versioned path and commit the reference instead.

Build this: Create a repository, make ten commits with a bug in the middle, then find it with bisect. Then practise recovering a branch you deleted using reflog.

This is one section of Git & collaboration, 8 hours across 2 sections. Read it in full to get the whole topic in order.

Stop 6 of 50 on the AI Engineer route

0 of 50 topics done · 0h behind you, 603h to go

Next on this routeTyping & validationChecking that model output is what you claimed.11h

What you have marked done is stored in this browser only. It does not reach a server, and clearing site data will clear it.