Terminal, Git & GitHub
Terminal, Git & GitHub

Everyday git: three places, pointers and upstreams

Lesson · ~30 min
After this unit you can
  • Explain where a change lives — working tree, index, commit — and predict what git status --short, git diff and git diff --staged show
  • Explain a branch as a movable pointer and origin/main as your last-fetched copy, and read ahead/behind and [upstream] in git status and git branch -vv
  • Start a branch whose push can't land on main (--no-track, push -u, branch.autoSetupMerge simple, push.autoSetupRemote), and recognise feature -> main before it ships
  • Stop tracking a committed file with git rm --cached, and explain why .gitignore alone changes nothing and what the commit does to teammates' copies

Read

Simple
1 / 2

Three places a change lives

Flag

Git 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 add copies 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.

Medium
1 / 2

The branch that pushes to main

Flag

The 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.

Complex
1 / 2

.gitignore only ignores files git isn't tracking

Flag

The 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?

Flag

git status --short shows MM src/order.ts. You run git commit -m "Fix order total". What goes into the commit?

Flag

You edited src/app.ts and created src/helper.ts, then ran git commit -am "Extract helper". What's in the commit?

Flag

A teammate pushed to main five minutes ago. Your git status still says Your branch is up to date with 'origin/main'. Why?

Flag

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?

Flag

git status -sb on your work branch prints ## fix-cart...origin/main [ahead 2]. What should you do before pushing?

Flag

You added .env to .gitignore and committed it, but after editing .env, git status still shows M .env. Why, and what's the fix?

Flag

You push the commit made by git rm --cached .env. What happens on a teammate's machine when they pull it?

Flag

Fill in the blank

Show exactly what the next commit will contain.

Flag
git diff 

Start feature from the remote's latest main without making origin/main its upstream, then publish it under its own name.

Flag
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+).

Flag
git config --global branch.autoSetupMerge 
git config --global push. true

Stop tracking .env but keep it on your disk.

Flag
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
Flag
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
Flag
# .env was committed; a later commit added ".env" to .gitignore
echo "SECRET=2" > .env
git status --short
git check-ignore -v .env; echo "exit=$?"
Flag

Further reading