Skip to main content
CodeOath
← All posts

Git95 min total · 17 parts

Git Internals and Workflows: Branching, Merging, and Rebasing

Part 3 of 17 · ~5 min

HEAD, the Working Tree, the Staging Area, and the Repository

Tuesday. The swap form half exists, the API endpoint does not, and you have been editing for two hours. Here is what git status says:

On branch feature/shift-swap
Changes to be committed:
        modified:   src/roster/availability.js

Changes not staged for commit:
        modified:   src/roster/swap.js
        modified:   CHANGELOG.md

Untracked files:
        src/roster/swap.test.js

Four files, three different states, and Git sorted them that way because it keeps your work in three distinct layers. Almost every "but I already saved that" confusion in Git traces back to mixing these up:

AreaWhat it holdsRight now it holdsHow to inspect it
Working treeThe actual files on disk, as you are editing themAll four files, as you last typed themOpen them
Staging area (index)Changes marked to go into the next commit, via git addOnly the availability.js changegit status, git diff --staged
Repository (.git history)Committed snapshots, permanent until deliberately rewrittenEverything up to a4f1c0e — and none of this morning's workgit log, git show

Think of git add and git commit as two ends of one pipe: add is what lifts an edit out of the working tree and drops it into the index, and commit is what seals whatever is currently sitting in that index into a new, permanent snapshot. If you committed right now, you would get a commit containing the availability.js change and nothing else — the half-written swap.js would stay exactly where it is, uncommitted, which is often precisely what you want.

The staging area is the part people skip, and it is the part that makes Git pleasant once you use it deliberately. Your two hours of work covers two unrelated things: a deadline check and a changelog entry. They should not be one commit. Staging lets you separate them after the fact:

git add src/roster/availability.js   # stage only the deadline check
git commit -m "Block swaps inside the deadline window"

git add CHANGELOG.md                 # now the other thing, separately
git commit -m "Note shift swapping in the changelog"

You can go finer than whole files. git add -p walks you through a file hunk by hunk and asks about each one, which is how you rescue a commit from a file where you fixed a bug and also renamed three variables while you were in there.

The two diff commands answer genuinely different questions, and this is the pair worth memorising:

git diff             # working tree vs. staging area — "what have I NOT staged yet?"
git diff --staged    # staging area vs. last commit — "what WILL this commit contain?"

Right now, git diff shows you swap.js and CHANGELOG.md. git diff --staged shows you availability.js. Neither shows you both, and reading the wrong one is exactly how you end up pushing a commit that is missing half of what you meant to include. (--cached is an older spelling of --staged; they do the same thing.)

Untracked files sit outside all of this. swap.test.js has never been added, so Git is not watching it, will not show its contents in any diff, and will not include it in a commit until you git add it once. This is also why a .gitignore written after a file has been committed does nothing — ignoring only applies to files Git is not already tracking.

Detached HEAD

Later on Tuesday you want to know when the availability calculation last worked the way you remember. So you check out an old commit to poke at it:

git checkout 6e2ac18

Git puts you in detached HEAD state, and the name describes exactly what happened: HEAD has let go of the branch label and is now hanging directly off one commit.

Normal:    HEAD → feature/shift-swap → 7c1f4ae
Detached:  HEAD → 6e2ac18

Everything works. You can read files, run the app, even commit. The problem is what commits mean here. You try a faster query, it works, you commit it — and then you switch back to carry on with the feature:

git switch feature/shift-swap
Warning: you are leaving 1 commit behind, not connected to
any of your branches:

  35f4f0c Try a faster availability query

If you want to keep it by creating a new branch, this may be a good time
to do so with:

 git branch <new-branch-name> 35f4f0c

That commit exists, and nothing points at it. No branch names it, HEAD has moved on, and it will not show up in git log on any branch. It is not deleted — Git holds unreferenced commits for weeks before collecting them, and the final chapter is about exactly how to get them back — but it is unreachable by every normal means.

The fix is to decide before you switch away, not after:

git switch --detach 6e2ac18         # same thing as the checkout above, but it says so out loud
git switch -c faster-availability   # give the work a real label before doing anything else

git switch --detach is worth preferring over git checkout <hash> for the same reason git restore is worth preferring over git checkout -- <file>: the command names the unusual thing it is about to do, instead of leaving you to infer it from whether the argument looked like a branch.

Git prints that warning for a reason. Detached HEAD is genuinely useful for looking around, running an old version, or bisecting a bug — and it is genuinely the easiest way in Git to lose work without noticing.