Everyday git: three places, pointers and upstreams
- Explain where a change lives — working tree, index, commit — and predict what
git status --short,git diffandgit diff --stagedshow - Explain a branch as a movable pointer and
origin/mainas your last-fetched copy, and read ahead/behind and[upstream]ingit statusandgit branch -vv - Start a branch whose push can't land on
main(--no-track,push -u,branch.autoSetupMerge simple,push.autoSetupRemote), and recognisefeature -> mainbefore it ships - Stop tracking a committed file with
git rm --cached, and explain why.gitignorealone changes nothing and what the commit does to teammates' copies
Read
Three places a change lives
FlagGit has three copies of your files, and every everyday command moves changes between them:
working tree ──git add──▶ index (staging area) ──git commit──▶ commits (history)
the files you edit the next commit, as you've built it snapshots, each with a parent
- The working tree is the folder you edit. Git doesn't record anything here by itself.
- The index (also called the staging area) is the commit you're about to make.
git addcopies a file's current content into it — the content at that moment, not a link to the file. - A commit is a snapshot of the whole index, plus a message and a pointer to its parent commit. Once made, it doesn't change.
The loop
git status # what differs between the three places
git diff # working tree vs index: changes you haven't staged
git add src/cart.ts # stage this file's current content
git diff --staged # index vs last commit: exactly what the commit will contain
git commit -m "Fix cart total rounding"
git log --oneline # the history, newest first
git diff with no arguments only shows unstaged changes. Once you git add everything,
it prints nothing — that doesn't mean nothing changed, it means the index caught up. To see what
you're about to commit, git diff --staged (--cached is the same flag).
Reading git status --short
Two columns: the left one compares the index with the last commit, the right one compares the working tree with the index.
M src/cart.ts staged, no further edits
M src/api.ts edited, not staged
MM src/order.ts staged, then edited again — the commit gets the staged version
A src/new.ts a new file, staged
?? notes.md untracked: git has never been told about it
MM is the one that surprises people: you staged a change, kept editing, and committed. The
commit contains what you staged, and the later edits are still sitting in the working tree.
git commit -a stages every change to files git already tracks, then commits. It skips
untracked files, so a new file still needs its own git add.
The branch that pushes to main
FlagThe habit "always branch from the freshest main" leads people to write:
git fetch
git switch -c feature origin/main
It does start the branch from the remote's latest main. It also does something you didn't
ask for — read the first line of the output:
branch 'feature' set up to track 'origin/main'.
Switched to a new branch 'feature'
branch.autoSetupMerge defaults to true, which means: when the start point is a
remote-tracking branch, make it the new branch's upstream. So feature's upstream is now
main on the remote:
% git config --get-regexp '^branch\.feature'
branch.feature.remote origin
branch.feature.merge refs/heads/main
% git status -sb # after one commit
## feature...origin/main [ahead 1]
What happens at push time depends on who's pushing
Plain git push on the command line refuses, because push.default is simple (the default
since Git 2.0) and simple only pushes to an upstream with the same name:
fatal: The upstream branch of your current branch does not match
the name of your current branch. To push to the upstream branch
on the remote, use
git push origin HEAD:main
Note the first suggestion: it's offering to push your feature work to main.
A tool that pushes "to the upstream" doesn't refuse. VS Code's Push and Sync, for example,
push <branch>:<its upstream> — here feature:main. So does any setup with
push.default=upstream:
% git -c push.default=upstream push
To github.com:acme/shop.git
84c2515..ea0a552 feature -> main
feature -> main: your unreviewed commits are on main — and if main deploys on push, they're
in production.
Starting branches that can't do that
git switch -c feature # from your local main (no upstream set)
git switch --no-track -c feature origin/main # from the remote's main, no upstream
git push -u origin feature # first push: upstream = origin/feature
Branching from a local branch sets no upstream (with the default setting), and --no-track
says so explicitly. Either way, the first push with -u gives the branch the upstream it should
have had — its own name on the remote. If a branch already has the wrong upstream, that same
git push -u origin feature replaces it.
Check any branch before trusting a push button: git branch -vv shows [origin/main] next to a
branch that will push to main.
.gitignore only ignores files git isn't tracking
FlagThe gitignore manual's first paragraph: a gitignore file "specifies intentionally untracked files that Git should ignore. Files already tracked by Git are not affected." So adding a pattern for a file that's already committed does nothing at all:
% git log --oneline
3a1f0c2 Ignore .env # added ".env" to .gitignore
9be41d7 Initial commit # ...but .env was committed here
% echo "SECRET=2" > .env
% git status --short
M .env # still tracked, still showing up, still committable
Ignore rules only answer the question "should this untracked file show up as ?? and be
picked up by git add .?". Once a file is in the index, git tracks it no matter what
.gitignore says. Even git check-ignore agrees, quietly:
% git check-ignore -v .env # prints nothing, exits 1: tracked files aren't checked
% git check-ignore --no-index -v .env # ignore the index: which rule *would* match?
.gitignore:1:.env .env
Untracking it
git rm --cached .env # remove from the index; the file stays on your disk
git commit -m "Stop tracking .env"
--cached is the important half: plain git rm deletes the file from your disk too. After the
commit, the .gitignore rule finally applies, and .env disappears from git status.
Two consequences people miss
Teammates lose the file. The commit records ".env was deleted". When a teammate pulls it,
git removes .env from their working tree — it was tracked there, and the new commit says
it's gone. (If they had local edits to it, the update refuses until they deal with them.) Tell
the team before you push, or have them back the file up first.
The old content is still in history. git rm --cached stops future commits from containing
the file; every earlier commit still does, on every clone and fork. If the file held a secret,
the secret is leaked: rotate it. Rewriting history doesn't un-leak a value that was already
pushed (the Web Security track's secrets unit covers that side).
Quiz
You edit three files, run git add ., then git diff — and it prints nothing. What does that mean?
git status --short shows MM src/order.ts. You run git commit -m "Fix order total". What goes into the commit?
You edited src/app.ts and created src/helper.ts, then ran git commit -am "Extract helper". What's in the commit?
A teammate pushed to main five minutes ago. Your git status still says Your branch is up to date with 'origin/main'. Why?
You ran git switch -c feature origin/main, made two commits, pressed VS Code's Sync button and clicked OK on its prompt. Where did the commits go?
git status -sb on your work branch prints ## fix-cart...origin/main [ahead 2]. What should you do before pushing?
You added .env to .gitignore and committed it, but after editing .env, git status still shows M .env. Why, and what's the fix?
You push the commit made by git rm --cached .env. What happens on a teammate's machine when they pull it?
Fill in the blank
Show exactly what the next commit will contain.
Flaggit diff Start feature from the remote's latest main without making origin/main its upstream, then publish it under its own name.
git fetch git switch -c feature origin/main # ...commits... git push origin feature
Make new branches track only a same-named remote branch, and make the first bare git push create one (Git 2.37+).
git config --global branch.autoSetupMerge git config --global push. true
Stop tracking .env but keep it on your disk.
git rm .env git commit -m "Stop tracking .env"
Predict the output
# notes.txt is committed, containing the line "one" printf 'two\n' >> notes.txt git add notes.txt printf 'three\n' >> notes.txt echo new > todo.txt git status --short
git switch -c feature origin/main # prints: branch 'feature' set up to track 'origin/main'. echo more >> README.md && git commit -qam "Add f" git push # default config git -c push.default=upstream push
# .env was committed; a later commit added ".env" to .gitignore echo "SECRET=2" > .env git status --short git check-ignore -v .env; echo "exit=$?"