1.4.11 · D4Python & Scientific Computing

Exercises — Git version control basics

2,537 words12 min readBack to topic

Before we start, a picture of the machine we are steering — the three places a file can live and the commands that move it between them.

Figure — Git version control basics

Keep this picture open. Almost every exercise is really the question "which arrow just fired, and where did the file land?"


Level 1 — Recognition

Recall Solution 1.1

State: Modified. Look at the leftmost box in the figure — the Working Directory. Editing a file changes it on disk, but you have not fired any arrow yet, so nothing moved. Git sees the file is different from the last snapshot but it is not marked for the next commit.

git status prints it under "Changes not staged for commit" and suggests git add train.py (the first arrow) to move it to the Staging Area.

Recall Solution 1.2
  • a → 4 (repository = tracked directory + .git)
  • b → 2 (commit = a snapshot with a hash, author, message, parent)
  • c → 3 (branch = a movable pointer to a commit)
  • d → 1 (HEAD = the pointer to where you are now; usually it points at a branch, and that branch points at a commit)

Mnemonic chain: HEAD → branch → commit → snapshot of the repository.


Level 2 — Application

Recall Solution 2.1
git status                       # 1. see both files as untracked
git add model.py                 # 2. fire arrow 1 for ONLY model.py
git commit -m "Add model skeleton"   # 3. fire arrow 2

Because you ran git add model.py (not git add .), notes.txt stays behind in the Working Directory — it is not in this commit. That is the whole point of the Staging Area: it lets you curate what goes into a snapshot.

Recall Solution 2.2
git checkout -b feature/scaler

The -b flag does two jobs at once: git branch feature/scaler (create a new pointer at the current commit) plus git checkout feature/scaler (move HEAD onto it). After this, any new commit lands on feature/scaler, and main stays frozen where it was.

Recall Solution 2.3
git remote add origin https://github.com/me/proj.git
git push -u origin main

The -u (short for --set-upstream) records that your local main tracks origin/main. From now on a bare git push knows where to go.


Level 3 — Analysis

Recall Solution 3.1

Fast-forward. Since main never moved past C, its history is entirely contained in feat's history. Git does not need to invent a merge commit — it just slides the main pointer forward from C to D.

After the merge, main points to D. See the top row of the figure below.

Figure — Git version control basics
Recall Solution 3.2

Three-way merge. The two branches have diverged — each has a commit the other lacks (E on main, D on feat). Git cannot just slide a pointer. It:

  1. finds the common ancestor = commit C (the last commit both branches share),
  2. compares C→D and C→E,
  3. combines both change sets into a brand-new merge commit M that has two parents: E and D.

See the bottom row of the figure. If C→D and C→E touched the same lines, Git stops and asks you to resolve a conflict by hand.

Recall Solution 3.3

No. git fetch only downloads the objects and moves the remote-tracking pointer origin/main. Your own main does not move — you can now inspect the incoming work with git log main..origin/main before touching your branch. To download and merge in one step: git pull origin main (which is literally fetch + merge).


Level 4 — Synthesis

Recall Solution 4.1
git checkout -b feature/prep          # branch + switch
# ...create prep.py...
git add prep.py
git commit -m "Add prep"              # commit 1 on feature
# ...edit prep.py again...
git add prep.py
git commit -m "Add scaling"           # commit 2 on feature
git checkout main
git merge feature/prep                # fast-forward: main gains both
git branch -d feature/prep            # safe delete (already merged)

Commit count on main: the original commit plus 2 = its previous count + 2. Because main had not moved, this is a fast-forward — no extra merge commit is created. git branch -d succeeds (lowercase -d refuses to delete unmerged branches; here it's fully merged, so it's safe).

Recall Solution 4.2
git fetch origin                 # (a) download, review — nothing merged yet
git log main..origin/main        #     inspect their new commits
git merge origin/main            # (b) integrate (may be three-way + conflicts)
#   ...resolve any conflicts, then git add + git commit...
git push origin main             # (c) publish combined history

The fetch-then-review-then-merge order is the whole safety story: you never let remote changes silently rewrite your working files, and you never overwrite the remote because your main already contains its commits before you push.


Level 5 — Mastery

Recall Solution 5.1
git restore --staged secrets.env    # unstage: reverse arrow 1
echo "secrets.env" >> .gitignore    # tell Git to never track it again
git add .gitignore
git commit -m "Ignore secrets.env"

git restore --staged is the exact inverse of the first arrow in our figure: it moves the file from the Staging Area back to Modified, leaving your file contents untouched. Because you never committed the secret, it never entered history — crisis averted. (Older Git uses git reset HEAD secrets.env for the same effect.)

Recall Solution 5.2

(a) Fix only the message:

git commit --amend -m "Add feature scaling"

--amend replaces the last commit with a new one (new hash) — safe only because it isn't pushed yet.

(b) Undo the commit, keep changes ready to re-stage:

git reset --soft HEAD~1

HEAD~1 means "the commit one step before HEAD." --soft moves the branch pointer back but leaves everything staged, so you can re-craft the commit. (--mixed, the default, would unstage them; --hard would delete your edits — avoid unless you truly want them gone.)

Recall Solution 5.3

A branch is not a copy of your files — it is a single line of text holding a commit's 40-character SHA-1 hash, stored in .git/refs/heads/. That file is about 41 bytes (40 hex chars + a newline). Creating a branch just writes those 41 bytes; the actual snapshots are shared objects in .git, reused across every branch. So 50 branches cost ~50 × 41 = 2050 bytes ≈ 2 KB of pointers, not 50 project copies. This is exactly why the Git workflow encourages one branch per feature.


Next steps: take these workflows into GitHub workflows and CI/CD pipelines, and see version control applied to data science in Jupyter notebooks, experiment tracking, and model versioning. Command-line fundamentals live in the CLI note.