Git — the moves seniors use
📖 Walk me through it — plain English
Git is the tool that records the history of your code as a series of saved snapshots called commits. Each commit has a unique ID (a long string called a SHA, like a fingerprint). Everyone knows the basics — add stages a change, commit saves it, push uploads it to the shared server. This lesson is about the handful of commands beyond those basics that come up in interviews, usually as a casual question like "how would you find the commit that broke this?" Knowing them signals you've actually shipped real code on a team, not just done homework.
The mental picture: think of your project's history as a stack of save-points in a video game, and your branch as a bookmark (called HEAD) pointing at the latest one. Most of these commands are just different ways of moving that bookmark around, rewriting old save-points, or copying one save-point from a different playthrough into yours — safely, without losing your progress.
Here is what each command in the cards above is really for, in plain terms:
git rebase -i HEAD~5) — opens an editor listing your last 5 commits so you can squash (combine) them, reorder them, or reword their messages before sharing. Turns messy "wip / wip / fix typo" commits into one clean, logical commit.main and a separate release branch.--force-with-lease instead of plain --force: it first checks the remote hasn't changed since you last fetched, so you don't accidentally erase a teammate's push.git stash tucks your half-finished changes onto a shelf so your working folder is clean and you can switch branches; git stash pop takes them back off the shelf when you return.<<<, ===, >>>) showing both versions. You edit the file to the version you want, delete those markers, git add the file to mark it resolved, then continue the rebase or merge.Why this matters in an interview: nobody asks you to recite flags. They ask a story-shaped question, and reaching for bisect to hunt a regression, reflog to recover lost work, or force-with-lease to push safely shows judgment that only comes from real practice. Pick two or three of these and actually run them once — that's enough to answer confidently.
First, the on-ramp: git is snapshots over time
Before the senior moves, lock down the foundation in one sentence: git is a tool that takes snapshots of your project and remembers them in order, so you can always go back, compare, or branch off. Imagine pressing "save" on the entire folder and getting a labelled photo of it. Each photo is a commit. The chain of photos, newest pointing back to the one before it, is your history. Nothing more mysterious than that — every other command is about taking, naming, moving between, or combining these snapshots.
Here are the words you will see everywhere, each defined once so the rest of the page reads smoothly:
- Repository (repo) — the project folder plus its entire hidden history. Practically, it's any directory with a
.gitsubfolder; that subfolder holds every commit and is what makes the folder "a git repo." - Working tree — the actual files you see and edit on disk right now. Your live workspace. Editing a file changes the working tree but not yet the history.
- Staging area (also called the index) — a waiting room between your edits and a commit.
git addmoves a change into staging; the nextgit commitphotographs exactly what's in staging. It lets you commit some of your edits and leave others for later. - Commit — one saved snapshot, with a message, an author, a timestamp, and a unique SHA id. Commits are immutable: you don't change a commit, you make a new one.
- Branch — a movable label pointing at a commit, marking one line of work (e.g.
mainorfeature/login). Making a branch is cheap; it's just a pointer, not a copy of all the files. - HEAD — the "you are here" marker. It points at the branch (and thus the commit) you're currently sitting on. Switching branches moves HEAD.
- Remote — a copy of the repo living on a server (GitHub, GitLab) that your team shares. Its default nickname is
origin. Your local repo and the remote sync by sending commits back and forth. - .gitignore — a plain text file listing paths git should pretend don't exist (build output,
node_modules/, secrets, local config). Patterns in it never get staged or committed.
The three areas (and how a change flows through them)
Every local git action is really a change moving between three places. Picture them left to right, with the commands that carry a change from one to the next written on the arrows:
# A change's journey from "I typed it" to "it's in history":
WORKING TREE STAGING AREA REPOSITORY (.git)
(files on disk) (the index) (committed history)
──────────── ──────────── ─────────────────
you edit a file ──► git add file ──► git commit ──► snapshot saved
◄── git restore --staged git checkout/restore ◄── pull a file back
HEAD ──► main ──► (newest commit) ──► (older) ──► (older) ──► ... the chain of snapshots
Read it like a story. You edit a file — that lives only in the working tree. You git add it — now a copy of that change waits in staging. You git commit — git photographs staging and writes a permanent snapshot into the repository, and the main branch (with HEAD riding on it) advances to point at the new commit. The dotted return arrows are the undo directions: you can pull a staged change back out, or restore a file from a past commit. The whole rest of this lesson is just clever ways of working with that bottom row — the chain of snapshots.
A concrete sequence, line by line
Words only get you so far. Here is a complete, realistic session that starts a repo, makes a feature branch, commits, and shares it — with a comment on every line explaining exactly what it does to the three areas above:
git init # create a new repo (makes the hidden .git folder)
git clone <url> # OR: copy an existing remote repo down to your machine
git status # show what's edited, staged, or untracked — your map
git add app.py # move app.py's changes from working tree into staging
git add . # stage everything changed (mind your .gitignore)
git commit -m "Add login form" # snapshot staging into a new commit; advance the branch
git switch -c feature/login # create a new branch AND move HEAD onto it
git log --oneline # list the commit history, one line each (SHA + message)
git remote -v # show the remotes (usually "origin" -> the server URL)
git fetch # download new commits from the remote, but DON'T merge yet
git pull # fetch + merge: bring the remote's changes into your branch
git push -u origin feature/login # upload your branch's commits to the remote
A few definitions earned by that script. Clone copies a whole remote repo (history and all) to your machine. Fetch downloads new commits from the remote but leaves your branches untouched — a safe "let me see what's new." Pull is fetch plus an automatic merge into your current branch. Push sends your local commits up to the remote so teammates can see them. Merge joins two branches' histories together. Notice the order of trust: fetch never changes your work, pull changes it gently (merging), and push changes what everyone else sees — so the higher the blast radius, the more deliberate you should be.
Undoing things: reset vs revert (and friends)
Two commands sound the same and do opposite things; mixing them up is a classic mistake worth nailing down:
- reset moves the branch pointer backward, rewriting your local history as if recent commits never happened.
git reset --soft HEAD~1undoes the last commit but keeps its changes staged;--mixed(the default) keeps them in the working tree unstaged;--hardthrows the changes away entirely. Because it rewrites history, reset is for local, unshared work only. - revert leaves history intact and instead adds a new commit that is the exact inverse of an old one — it "un-does" the effect while keeping the record.
git revert <sha>is the safe choice for undoing something already pushed and shared, because it doesn't rewrite anyone's history. - stash (defined above) is the lightweight escape hatch: shelve uncommitted work so the working tree is clean, do something else, then
git stash popto bring it back. - cherry-pick copies one commit's changes onto your current branch as a brand-new commit — a surgical "I want just that fix here."
The mnemonic: reset rewrites, revert records. Reach for revert on shared branches and reset only when the commits live nowhere but your laptop.
Git questions are usually casual ("how would you find the commit that broke X?") not formal. Knowing 5 commands beyond add/commit/push signals you've shipped real code.
Merge preserves history (extra merge commit). Rebase replays your commits on top of main → linear history. Rule of thumb: rebase your local branch before pushing, never rebase shared branches.
Binary search for the commit that introduced a bug. git bisect start; git bisect bad; git bisect good <sha> — git checks out the midpoint, you mark good/bad, repeat. log n commits to find culprit.
Local log of every HEAD move. Saved you from a bad reset / lost commit. Even "deleted" commits live ~30 days here.
git rebase -i HEAD~5 lets you squash, reorder, or reword the last 5 commits before pushing. Clean up "wip wip fix typo" history into one logical commit.
Grab a single commit from another branch onto yours. Used for hot-fixes that need to land on main + a release branch.
--force-with-lease instead of --force — it refuses if remote moved since you fetched. Saves you from stomping a teammate's push.
git stash when you need to switch branches mid-change without committing. git stash pop brings it back.
Edit the file, remove the <<</===/>>> markers, git add the file, continue rebase/merge. git diff with no args shows what's left.
Pitfalls that bite real engineers
Committing secrets is forever.
If you git commit an API key, password, or .env file, deleting it in a later commit does not remove it — the old snapshot still contains it, and anyone with the repo (or a clone, or the remote's history) can read it. Treat any secret that touched a commit as compromised: rotate it immediately. Prevent it up front by listing sensitive paths in .gitignore before the first add, and skim git status before every commit so nothing unexpected sneaks in.
Force-push can erase a teammate's work.
A plain git push --force overwrites the remote branch with your version, silently throwing away any commits a teammate pushed since you last fetched. The habit that prevents disaster: always use git push --force-with-lease, which refuses the push if the remote moved underneath you. And never force-push a shared branch like main at all — rewrite history only on branches you alone own.
Two more sharp edges. Don't rebase a branch others have already pulled — you change every commit's SHA and their copies diverge. And git reset --hard discards uncommitted working-tree changes with no confirmation; if you ran it by mistake, your committed-but-orphaned work may still be recoverable via git reflog — but uncommitted edits are simply gone.
Takeaway: git records your project as ordered snapshots (commits). Edits live in the working tree, git add moves them to staging, git commit writes them to the repository; fetch/pull/push sync with the remote. Branches and HEAD are just pointers. Undo with revert on shared history, reset only locally. The senior moves — bisect to hunt a bug, reflog to recover, interactive rebase to tidy, cherry-pick to port a fix, and --force-with-lease to push safely — are all just careful ways of moving and combining those snapshots.
Go deeper (optional): the free Pro Git book covers the object model and internals end to end if you want to see what's inside .git.