Git · free · no signup

Learn git, with practice after every lesson

8 lessons, about 124 minutes of reading, and 24 multiple-choice questions. Each lesson names the mistake that most often costs people the interview, because that is where the hours actually go. Part of the free coding course.

Commits, and what they really are

Basics Git · 12 min · 10 XP

Git saves snapshots of your whole project, not edits to single files. Each snapshot is a commit, and every commit knows which one came before it. That chain is the entire model β€” once it clicks, most of Git stops feeling arbitrary and starts feeling obvious.

The part beginners miss is the staging area. A file you have edited is not automatically part of the next commit; you choose what goes in with git add. That indirection seems like a nuisance until the day you have fixed two unrelated things in one sitting and want them recorded as two separate commits, which is exactly when it becomes indispensable.

Three states describe every file: modified (edited), staged (chosen for the next commit), and committed (saved). git status names which one each file is in, and reading it before every commit is the habit that prevents the most common accident in this material β€” running git add . without looking and committing a secret, a 200 MB export, or a page of debug prints.

Syntax

git status                  # what has changed, and what is staged
git add report.py           # stage one file
git add .                   # stage everything changed
git commit -m "Fix the date filter losing the last day"

git log --oneline -5        # the last five commits
# a3f9c21 Fix the date filter losing the last day
# 88b1e04 Add weekly summary export
# 1c77aa9 Initial commit

git diff                    # unstaged changes
git diff --staged           # what the next commit will contain

Key points

  • Three states: modified (edited), staged (chosen for the next commit), committed (saved). git status names which one every file is in.
  • Write the message as what the change does, not what you did: 'Fix the date filter losing the last day' beats 'updates'. You are writing for whoever bisects this in a year.
  • Commit small and often. A commit containing one fix can be reverted; a commit containing four cannot be untangled.
The mistake that costs people the interview: Running git add . without checking git status first, and committing a secret, a 200 MB CSV, or debug prints. Once a secret is in history, deleting the file does not remove it β€” the commit still holds it.

Practice challenge

Stage one file, not everythingBasics
Task

You fixed a date bug in report.py and, separately, added debug prints to loader.py that you do not want committed. Write the commands to commit only the bug fix, with a message that says what the change does.

Expected output
git add report.py
git commit -m "Fix the date filter losing the last day"
Starter
$ ______
$ ______
Show a hint
  1. git add takes a path, not just a dot
  2. The message should describe the change, not the act of changing

Open this exercise in the app →

Check yourself

1. What does git add do?

  1. Saves the commit
  2. Stages a change for the next commit
  3. Uploads to GitHub
  4. Creates a branch
Show answer

B. Stages a change for the next commit

2. Why commit small and often?

  1. It runs faster
  2. A small commit can be reverted cleanly
  3. GitHub requires it
  4. It uses less disk
Show answer

B. A small commit can be reverted cleanly

3. You committed a password by mistake. Does deleting the file fix it?

  1. Yes, the file is gone
  2. No, the earlier commit still contains it
  3. Only on GitHub
  4. Yes, after a push
Show answer

B. No, the earlier commit still contains it

Back to the syllabus ↑

Branches and pull requests

Working level Git · 15 min · 15 XP

A branch is a movable label pointing at a commit. Making one costs nothing β€” no files are copied β€” which is why teams make one per piece of work rather than treating branches as a big decision. Understanding that a branch is a pointer rather than a copy explains most of Git's behaviour that otherwise looks strange.

This is the daily rhythm of almost every engineering job, and it is what a hiring manager is checking when they ask how you work: branch, commit, push, open a pull request, get review comments, address them, merge. Being able to describe that loop naturally matters more in an interview than knowing any individual command.

Two habits make it go smoothly. Pull main before you merge into it, so you are not merging onto a stale base and re-solving a conflict somebody already fixed. And treat the pull request as a conversation rather than a formality β€” reviewers are the cheapest bug detection a team has, and a candidate who describes review as something they value reads very differently from one who describes it as a hurdle.

Syntax

git switch -c fix/date-filter     # create and move onto a branch
# ...edit, add, commit...
git push -u origin fix/date-filter

# open a pull request on GitHub, get review, then:
git switch main
git pull                          # take other people's merged work first
git merge fix/date-filter

git branch -d fix/date-filter     # tidy up after merging

# if main moved on while you worked, replay your work on top:
git switch fix/date-filter
git rebase main

Key points

  • Branch names that say what and why β€” fix/date-filter, add/csv-export β€” make a busy repository readable at a glance.
  • Pull main before you merge into it. Merging stale work is how a conflict you already solved comes back.
  • A pull request is a conversation, not a formality. Reviewers are the cheapest bug detection a team has.
The mistake that costs people the interview: Committing straight onto main because a branch feels like ceremony for a small change. It removes review, makes reverting risky, and on most teams it is the fastest way to be told you do not work well with others.

Practice challenge

Branch, push, mergeWorking level
Task

Write the sequence to create a branch for a CSV export feature, push it so a pull request can be opened, then after review merge it into main β€” including the step that stops you merging onto a stale base.

Expected output
git switch -c add/csv-export
git push -u origin add/csv-export
git switch main
git pull
git merge add/csv-export
Starter
$ ______
(work, commit)
$ ______
$ ______
$ ______
$ ______
Show a hint
  1. The stale-base step happens after switching to main and before merging
  2. -u sets the upstream so later pushes need no arguments

Open this exercise in the app →

Check yourself

1. What does a branch actually cost to create?

  1. A full copy of the project
  2. Almost nothing β€” it is a label on a commit
  3. One commit
  4. It depends on repository size
Show answer

B. Almost nothing β€” it is a label on a commit

2. Why pull main before merging into it?

  1. To free disk space
  2. So you are not merging onto a stale base
  3. GitHub requires it
  4. To trigger CI
Show answer

B. So you are not merging onto a stale base

3. What does git rebase main do to your branch?

  1. Deletes it
  2. Replays your commits on top of the current main
  3. Merges main into it
  4. Pushes it
Show answer

B. Replays your commits on top of the current main

Back to the syllabus ↑

Undoing things without panic

Advanced Git · 16 min · 20 XP

Most Git fear is fear of losing work. Almost nothing committed is ever truly lost: Git keeps a log of every position each branch has held, so a bad reset is usually recoverable in one command. Knowing that changes how confidently you work, because the cost of experimenting drops to nearly nothing.

The distinction that matters is whether the commit has been pushed. Rewriting history you have kept to yourself is free β€” amend, rebase, reorder as you like. Rewriting history other people have already pulled forces them to repair their own copies, which is why force-pushing a shared branch is treated as a serious mistake rather than a preference.

That gives a simple rule. On a shared branch use revert, which adds a new commit undoing an old one and leaves history intact. Keep reset for local work. And know that reflog exists before you need it: it is the undo button for the undo button, and the only thing it cannot recover is uncommitted changes destroyed by reset --hard, which is the one genuine way to lose work in Git.

Syntax

git restore report.py            # throw away uncommitted edits to a file
git restore --staged report.py   # unstage, keep the edits

git commit --amend -m "Better message"   # fix the LAST commit (unpushed only)

git revert a3f9c21               # new commit that undoes an old one β€” safe when shared
git reset --soft HEAD~1          # undo last commit, keep changes staged
git reset --hard HEAD~1          # undo last commit AND discard changes

# the safety net β€” every position HEAD has held:
git reflog
# a3f9c21 HEAD@{0}: reset: moving to HEAD~1
# 88b1e04 HEAD@{1}: commit: Add weekly summary export
git reset --hard 88b1e04         # go back to before the mistake

Key points

  • revert is safe on shared branches because it adds a commit. reset rewrites history and should stay local.
  • reflog is the undo button for the undo button. Before assuming work is gone, look there.
  • --hard is the only one of these that destroys uncommitted work. Everything else is recoverable.
The mistake that costs people the interview: Reaching for git reset --hard to 'clean things up' with uncommitted changes present. Those changes were never committed, so reflog cannot bring them back β€” this is the one genuine way to lose work in Git.

Practice challenge

Undo safelyAdvanced
Task

Commit a3f9c21 broke production. It was pushed three days ago and four colleagues have pulled it. Give the command you would use, and say in one line why the other obvious option is wrong here.

Expected output
git revert a3f9c21
reset rewrites history others already have, forcing them to repair their own copies; revert adds a new commit and leaves history intact
Starter
Command: ______
Why not the other: ______
Show a hint
  1. The deciding fact is that other people already pulled it
  2. One of these two is safe on shared branches and one is not

Open this exercise in the app →

Check yourself

1. Which is safe to use on a branch others have pulled?

  1. git reset --hard
  2. git revert
  3. git commit --amend
  4. git rebase
Show answer

B. git revert

2. What is git reflog for?

  1. Viewing remote branches
  2. Seeing every position HEAD has held, to recover from a bad reset
  3. Listing contributors
  4. Showing file history
Show answer

B. Seeing every position HEAD has held, to recover from a bad reset

3. What does git reset --hard destroy that reflog cannot recover?

  1. Pushed commits
  2. Uncommitted changes
  3. Branch names
  4. Tags
Show answer

B. Uncommitted changes

Back to the syllabus ↑

Work on a team without breaking main

Job-ready Git · 17 min · 25 XP

Everything you have learned so far assumed you were alone. On a team the commands barely change and the discipline around them is the whole job: main must always be deployable, your branch must be small enough to review, and a conflict is a conversation rather than an emergency.

Small branches are the single biggest lever on how fast your work gets merged. A 40-line pull request gets a careful review in ten minutes; an 800-line one gets "looks good to me" three days later, which is not a review at all. If a change is genuinely large, split it into a series where each piece is safe on its own β€” a refactor with no behaviour change, then the behaviour change.

Conflicts are not a sign anything went wrong. They mean two people edited nearby lines, and Git is correctly refusing to guess. Pull main into your branch often so you meet conflicts in ones and twos rather than in a wall at the end. When you resolve one, read both sides and understand why the other person made their change before you keep yours.

Syntax

# Start from a current main, always
git switch main && git pull
git switch -c fix/duplicate-invoice-rows

# ... small, focused commits ...
git add -p                       # stage hunks, not whole files
git commit -m "Dedupe invoice rows on (client_id, period)"

# Keep up with main WHILE you work, not at the end
git fetch origin
git merge origin/main             # conflicts arrive in ones, not in a wall

git push -u origin fix/duplicate-invoice-rows
# open the PR, describe WHAT CHANGED and WHY, link the ticket

# After review, if history is messy, tidy before merge:
git rebase -i origin/main         # squash "fix typo" into its parent
git push --force-with-lease       # never plain --force on a shared branch

Key points

  • Keep the branch small. Review quality falls off a cliff somewhere around 200 changed lines, and an unreviewed merge is the thing branches exist to prevent.
  • Merge main into your branch regularly. Conflicts met early are minutes; conflicts met at the end are hours.
  • --force-with-lease, never --force. It refuses if someone else pushed in the meantime, which is exactly the case where forcing destroys their work.
The mistake that costs people the interview: Working for two weeks on a branch without pulling main, then hitting forty conflicts and resolving them by taking your own side everywhere. You have just silently reverted two weeks of somebody else's work, and the tests may well still pass.

Practice challenge

Two weeks of conflictsJob-ready
Task

You branched from main two weeks ago and never pulled. Merging now produces 40 conflicts. Say what you should have done differently, how you resolve this safely now, and why 'take mine everywhere' is dangerous specifically here.

Expected output
Should have: merged origin/main into the branch regularly, so conflicts arrived in ones and twos
Resolve now: merge main in, work through conflicts file by file, and for each read BOTH sides to understand the other change before choosing; run the tests after
Why not take-mine: it silently reverts two weeks of colleagues' work. The tests may still pass, because reverting a change usually compiles fine - which is what makes it dangerous
Starter
Should have: ______
Resolve now by: ______
Why not take-mine: ______
Show a hint
  1. The danger is not that it breaks the build - it is that it does not
  2. Frequency is the fix; forty conflicts is twenty conflicts you postponed twice

Open this exercise in the app →

Check yourself

1. Why keep pull requests small?

  1. Git is slower with large diffs
  2. Large PRs get rubber-stamped rather than reviewed
  3. GitHub charges by diff size
  4. It uses less disk
Show answer

B. Large PRs get rubber-stamped rather than reviewed

2. What does --force-with-lease add over --force?

  1. It is faster
  2. It refuses if someone else has pushed since you fetched
  3. It creates a backup branch
  4. It skips the hooks
Show answer

B. It refuses if someone else has pushed since you fetched

3. A merge conflict means:

  1. Something is broken
  2. Two people changed nearby lines and Git will not guess
  3. Your branch is corrupted
  4. You must start the branch again
Show answer

B. Two people changed nearby lines and Git will not guess

Back to the syllabus ↑

What must never go in a repository

Basics Git · 13 min · 10 XP

A repository is public history even when the repo is private, because private repos get shared, forked, made public by an admin who did not think, or handed to a contractor for a week. So the question is never 'is this repo private', it is 'am I willing for this to exist forever in a place I do not control'. Three things fail that test: secrets, generated files, and large binaries.

Secrets are the one that ends careers. An API key committed and then deleted in the next commit is still in the history, still readable by anyone who clones, and still valid until somebody rotates it. GitHub scans public pushes and bots scrape them within seconds β€” the measured time from pushing an AWS key to it being used for crypto mining is often under five minutes. The fix is not deleting the file, it is rotating the key, because you must assume it is already gone.

Generated files and large binaries are less dramatic but they are what makes a repo miserable to work in. node_modules, build output, a 200 MB CSV export, a compiled .jar: none of them are source, all of them can be produced from source, and every one of them permanently inflates the repo for every person who ever clones it. .gitignore is how you say so once, at the start, before the first commit rather than after.

Syntax

# .gitignore β€” write this BEFORE the first commit
.env                    # secrets: never, under any circumstances
.env.local
*.pem
*.key

node_modules/           # generated: reinstallable from package.json
dist/
build/
__pycache__/
*.pyc

data/*.csv              # large: keep the loader, not the export
*.xlsx

.DS_Store               # noise from your machine, not the project
.vscode/

# Already tracked before you ignored it? .gitignore will NOT help.
git rm --cached .env    # stop tracking, keep your local copy
git commit -m "Stop tracking .env"

# Check what you are about to commit, every time:
git status
git diff --staged

Key points

  • A secret committed once is compromised forever. Deleting the file does not help β€” the old commit still contains it. Rotate the key; that is the only real fix.
  • .gitignore only stops files that are not already tracked. If you committed it first you need git rm --cached before the ignore takes effect.
  • Commit source, not things built from source. If a file can be regenerated by a command, ignore it and write the command down instead.
The mistake that costs people the interview: Believing that because a repository is private, a committed secret is safe. Private repos get made public, forked, and shared with contractors, and history is copied on every clone. Treat a committed key as leaked from the moment it is pushed, and rotate it the same day.

Practice challenge

Stop tracking a committed secretBasics
Task

You committed and pushed .env two weeks ago, then added it to .gitignore, and it is still showing as tracked. Write the commands to stop tracking it, and say what you must do about the key inside it.

Expected output
$ git rm --cached .env
$ git commit -m "Stop tracking .env"
Why: .gitignore only affects files that are NOT already tracked; .env was tracked, so it kept being tracked
The key: rotate it. It is in the pushed history and in every clone. Deleting the file does not remove it from the earlier commits.
Starter
$ ______
$ ______
Why .gitignore did not help: ______
What must happen to the key: ______
Show a hint
  1. --cached removes it from the index but keeps your local copy
  2. Ask what a person who cloned the repo last week already has

Open this exercise in the app →

Check yourself

1. You committed an API key, then deleted the file and committed again. Is the key safe?

  1. Yes, the file is gone
  2. No β€” the earlier commit still contains it, so rotate the key
  3. Yes, if the repo is private
  4. Only if you force-push
Show answer

B. No β€” the earlier commit still contains it, so rotate the key

2. You added node_modules/ to .gitignore but it is still being tracked. Why?

  1. .gitignore needs a restart
  2. .gitignore only affects untracked files β€” use git rm --cached
  3. The path needs a leading slash
  4. Ignore files do not work on folders
Show answer

B. .gitignore only affects untracked files β€” use git rm --cached

3. Why keep build output out of the repository?

  1. It is against the licence
  2. It is regenerable from source and permanently inflates every clone
  3. Git cannot store binaries
  4. It breaks git log
Show answer

B. It is regenerable from source and permanently inflates every clone

Back to the syllabus ↑

Merge, rebase, and reading a conflict

Working level Git · 16 min · 15 XP

Merge and rebase both answer 'my branch is behind main, now what', and they differ only in the history they leave. Merge takes the two lines of work and ties them together with a merge commit; your branch keeps the commits you actually made, in the order you made them, and the history shows that two things happened in parallel, which is true. Rebase instead replays your commits on top of the current main, one at a time, as if you had started this morning.

Rebase produces a straight, readable history and is the reason many teams prefer it. The rule that comes with it is absolute: never rebase a branch other people have pulled. Rebasing rewrites commits β€” same changes, new identities β€” so anyone who already has the old ones now has a branch that has silently diverged from yours, and the recovery is unpleasant. Rebase your own unpushed work freely; leave shared branches alone.

A conflict is not an error, it is Git declining to guess. It happens when two branches changed the same lines, and Git marks both versions in the file for you to choose between. The markers are mechanical: everything between <<<<<<< and ======= is what is on the branch you are merging into, everything between ======= and >>>>>>> is what is coming in. You edit the file until it is what you want, remove every marker, then git add it to say you have resolved it.

Syntax

# bring your branch up to date with main
git checkout my-feature
git fetch origin
git merge origin/main          # safe on shared branches
# or
git rebase origin/main         # only if nobody else has this branch

# a conflict looks like this inside the file:
<<<<<<< HEAD
timeout = 30
=======
timeout = 60
>>>>>>> origin/main

# decide, delete all three markers, leave the line you want:
timeout = 60

git add config.py              # 'I have resolved this file'
git commit                     # for a merge
git rebase --continue          # for a rebase

git merge --abort              # back out, nothing lost
git rebase --abort

Key points

  • Merge keeps true parallel history and is always safe. Rebase makes history linear but rewrites commits, so never rebase anything someone else has pulled.
  • HEAD is your side, the part below ======= is what is coming in. Resolving means editing to what you actually want, not mechanically keeping one side.
  • You can always back out with git merge --abort or git rebase --abort. Nothing is lost, so a conflict is never a reason to panic or to re-clone.
The mistake that costs people the interview: Resolving a conflict by accepting one whole side because the markers are annoying. In a config or a shared function that quietly deletes the other person's work, the tests still pass, and it surfaces a week later as a bug nobody can explain. Read both sides and decide what the file should say.

Practice challenge

Resolve a conflict correctlyWorking level
Task

Merging main into your branch produced the conflict below in config.py. Your branch raised the timeout to 60 because of a slow supplier API; main lowered it to 15 for an unrelated health check. Write what the resolved file should contain and explain your reasoning in one line.

Expected output
Resolved: neither value blindly. The two changes serve different callers, so one shared constant is the actual bug -
supplier_timeout = 60
health_check_timeout = 15
Reasoning: accepting either side silently breaks the other team's change; the conflict revealed that one setting was doing two jobs
Command: git add config.py
Starter
<<<<<<< HEAD
timeout = 60
=======
timeout = 15
>>>>>>> origin/main

Resolved file: ______
Reasoning: ______
Command after editing: ______
Show a hint
  1. Both sides are correct for their own caller - that is why it conflicted
  2. Picking a winner here loses somebody's work and the tests will still pass

Open this exercise in the app →

Check yourself

1. When must you not rebase?

  1. When the branch is more than a day old
  2. When other people have already pulled the branch
  3. When there are merge conflicts
  4. When you are on main
Show answer

B. When other people have already pulled the branch

2. In a conflict, what is between <<<<<<< HEAD and =======?

  1. The incoming change
  2. The version on the branch you are currently on
  3. The common ancestor
  4. The most recent commit
Show answer

B. The version on the branch you are currently on

3. A merge conflict has appeared and you are not ready to deal with it. What can you do?

  1. Delete the branch
  2. git merge --abort, which puts everything back
  3. Force push
  4. Re-clone the repository
Show answer

B. git merge --abort, which puts everything back

Back to the syllabus ↑

Finding the commit that broke it

Advanced Git · 17 min · 20 XP

'It worked last month' is a solvable problem, and solving it from history is faster than reading code. Three tools cover almost every case. git log -S searches history for when a string appeared or disappeared, which finds the commit that removed a check or changed a constant. git blame annotates each line of a file with the commit that last touched it, which turns 'why is this here' into a commit message and an author you can ask.

blame has a bad name it does not deserve. You are not looking for a culprit, you are looking for context: the commit that introduced a line usually has a message and often a pull request explaining why, and that is frequently the whole answer. The trap is that a reformat or a rename shows up as the last change to every line, hiding the real one β€” git blame -w ignores whitespace, and following the commit before the reformat gets you past it.

When the change is not obvious from any single file, git bisect finds it by binary search. You tell Git one commit where the bug exists and one where it does not; Git checks out the midpoint and asks you whether it is broken; you answer good or bad, and it halves the remaining range each time. Across a thousand commits that is about ten tests to find the exact one, and if you can express the test as a script, git bisect run does it without you.

Syntax

# when did this string arrive or leave?
git log -S "retry_limit" --oneline
# a3f9c21 Drop retry limit for the batch job

# who last touched each line, ignoring reformatting
git blame -w app/loader.py
git blame -w -L 40,60 app/loader.py    # just lines 40-60

# what exactly did that commit change?
git show a3f9c21

# binary search for the breaking commit
git bisect start
git bisect bad                  # now is broken
git bisect good v1.4.0          # this tag was fine
# Git checks out the midpoint; you test and answer:
git bisect good                 # ...or: git bisect bad
# repeat ~10 times for 1000 commits
git bisect reset                # back to where you started

# automate it: exit 0 = good, non-zero = bad
git bisect run pytest tests/test_dates.py

Key points

  • git log -S finds the commit where a string appeared or disappeared. It is the fastest way to answer 'when did this constant change'.
  • git blame -w ignores whitespace, so a reformat does not mask the commit you are looking for. Without -w the last reformat blames every line.
  • git bisect is binary search over history: about ten steps for a thousand commits, and fully automatic with git bisect run plus a test that exits non-zero on failure.
The mistake that costs people the interview: Reading the diff of a large merge commit to find a regression. A merge commit shows the combined result, not the change that caused it. Bisect finds the individual commit, and git show on that commit is the diff you actually wanted to read.

Practice challenge

Find the commit that changed a constantAdvanced
Task

A retry limit used to be 5 and is now 1, and nobody remembers changing it. Write the command that finds the commit where the string retry_limit changed, the command to see exactly what that commit did, and the command to find who last touched those lines without a whitespace-only reformat masking it.

Expected output
Find the commit: git log -S "retry_limit" --oneline
See the change: git show <hash>
Blame the lines: git blame -w app/client.py
(-w ignores whitespace so a reformat does not blame every line)
Starter
Find the commit: ______
See the change: ______
Blame the lines: ______
Show a hint
  1. -S searches history for when a string appeared or disappeared
  2. One flag on blame ignores whitespace-only changes

Open this exercise in the app →

Check yourself

1. Which command finds the commit where the string 'retry_limit' was removed?

  1. git grep retry_limit
  2. git log -S "retry_limit"
  3. git blame retry_limit
  4. git diff retry_limit
Show answer

B. git log -S "retry_limit"

2. Why use git blame -w?

  1. It is faster
  2. It ignores whitespace, so a reformat does not hide the real change
  3. It shows only your commits
  4. It blames the whole file at once
Show answer

B. It ignores whitespace, so a reformat does not hide the real change

3. About how many steps does bisect need across 1,000 commits?

  1. About 1,000
  2. About 500
  3. About 10
  4. About 100
Show answer

C. About 10

Back to the syllabus ↑

Reviewing someone else's pull request

Job-ready Git · 18 min · 25 XP

Reviewing is the part of Git work that is judged most and taught least, and it is the fastest way for a new joiner to build a reputation. A review is not a search for style violations β€” a formatter should own those β€” it is you taking partial responsibility for this code working. Start by reading the description and asking what the change is meant to do, then read the tests, because the tests tell you what the author believes the change does. A change with no test for its main claim is the single most useful thing to point out.

Read for the things a machine cannot see. Does this handle the empty case, the duplicate, the second call? Is the error swallowed? Does the name say what the function does? Is there a migration that must run first, and does the deploy order matter? Those are review questions. Whether the brace is on the same line is not, and spending your review on that trains people to ignore your reviews.

How you write the comment decides whether it lands. Separate what blocks merge from what is a preference and say which is which, because a reviewer who marks everything as blocking gets routed around. Ask rather than instruct when you are not sure β€” 'what happens if items is empty here?' is both easier to receive and genuinely more likely to be right than 'this crashes on empty'. And approve when it is good enough: a review that never ends is a review that stops being requested.

Syntax

# review locally when the diff is not enough
git fetch origin pull/482/head:review-482
git checkout review-482
git diff main...review-482        # three dots: only their changes

# does it actually pass?
pytest -q

# read the commits, not just the combined diff
git log main..review-482 --oneline

# --- comments that land ---
# BLOCKING: parse_dates() returns None when the file is empty, and
#   line 88 calls .strftime() on it. tests/test_loader.py has no
#   empty-file case; can we add one?
#
# Non-blocking: `d2` reads as a temp name β€” maybe `parsed_dates`?
#
# Question: if this deploys before the migration, does the old
#   column still exist? Wondering about deploy order, not the code.

Key points

  • Read the tests first. They state what the author thinks the change does, and a missing test for the main claim is the highest-value comment you can leave.
  • Label blocking versus preference explicitly. A reviewer who blocks on taste gets worked around, and then nobody catches the real bugs.
  • Ask instead of assert when unsure. A question is easier to receive, and it is more often correct than a confident claim about code you have read once.
The mistake that costs people the interview: Approving a large pull request quickly because the author is senior or the change is urgent. Your approval is a claim that you checked. If you have not, say so β€” 'I read the API changes but not the migration' is useful and honest, and a silent rubber stamp is what lets a bad deploy through.

Practice challenge

Write three review commentsJob-ready
Task

A pull request adds parse_dates(), which returns None for an empty file, and line 88 calls .strftime() on the result. There is no test for the empty case. A variable is named d2. The change also needs a database migration. Write one blocking comment, one non-blocking comment and one question, each labelled.

Expected output
BLOCKING: parse_dates() returns None on an empty file and line 88 calls .strftime() on it, which will raise. There is no empty-file case in the tests - can we add one?
Non-blocking: d2 reads as a temporary name; parsed_dates would be clearer. Not a merge blocker.
Question: if this deploys before the migration runs, does the old column still exist? Asking about deploy order rather than the code itself.
Starter
BLOCKING: ______
Non-blocking: ______
Question: ______
Show a hint
  1. The blocking one should be the thing that breaks in production, not the naming
  2. A question is easier to receive than an assertion, and more often correct

Open this exercise in the app →

Check yourself

1. What should you read first in a pull request?

  1. The longest file
  2. The description and the tests
  3. The commit hashes
  4. The line count
Show answer

B. The description and the tests

2. Why label a comment as non-blocking?

  1. It is required by GitHub
  2. So the author knows what must change to merge and what is preference
  3. It closes the thread
  4. It notifies the team lead
Show answer

B. So the author knows what must change to merge and what is preference

3. You have only reviewed part of a large PR but it is urgent. Best action?

  1. Approve it, someone else will check
  2. Say exactly which parts you reviewed and which you did not
  3. Reject it
  4. Say nothing and let it merge
Show answer

B. Say exactly which parts you reviewed and which you did not

Back to the syllabus ↑

Common questions

Do I need any background to start Git?

No. This track begins at its own beginning and assumes nothing, and the first lesson explains what the thing is before showing you any syntax.

How long does the Git track take?

About 124 minutes of reading across 8 lessons, plus the practice challenges and 24 multiple-choice questions, which is where the time actually goes.

Is it free?

Yes, and there is no account. Everything runs in your browser.

More: all 15 tracks · what employers actually ask for · the full syllabus

Keep reading

The STAR method, properly: how to build answers that hold up
A working guide to STAR interview answers: how to weight each part, how to build five stories that cover most…
Returning to work after a career break: rebuilding confidence and explaining the gap
How to present a career break on your CV, close the confidence gap, and answer interview questions about time…
Free AI interview coach
Free AI interview coach: voice mock interviews that talk back, role-specific questions, coding practice and…
Interview countdown, prediction & mock practice
Free interview prep: a live countdown to your interview date, then the 15 most common questions as flip-cards…