Git, Properly: The Model Behind the Commands You Use Every Day
Most people learn Git as a list of commands and get stuck the first time one does something unexpected. The model underneath is small: three places your code lives, commits as snapshots, branches as pointers. Learn that, and the everyday commands, the traps in stash and pull, and the safe ways to undo a mistake all make sense.
Tauseef Fayyaz

Why memorising commands is not enough
Most people learn Git as a list of incantations. add, commit, push, and when something goes wrong, a search for the exact error message and a copied command from someone who seemed confident.
That works until the day it does not: a pull that refuses to run, a stash that did not save the file you needed, a force push that wiped a colleague's afternoon. At that point the list of commands is no help, because the problem is not which command to type. It is not knowing what Git is actually doing.
The good news is that the model underneath is small. Once you have it, almost every command becomes obvious, and the dangerous ones announce themselves.
What Git actually gives you
Git is a distributed version control system. Every clone holds the full history of the project, which is why you can commit, branch and read the log on a plane.
It gives you three things worth naming precisely.
History you can go back to. Every commit is a complete, recoverable state of the project. You can see what changed, when, by whom and, if the message is any good, why.
Parallel work without collisions. Branches let several people change the same codebase at the same time, and merging brings that work back together with conflicts surfaced rather than silently overwritten.
Integrity, which is not the same as security. Every object Git stores is named by a cryptographic hash of its contents. Change a single byte in an old file and the hashes stop matching, so tampering with history is detectable. Commits and tags can also be signed, so you can prove who made them.
What Git does not give you is access control or a backup. Who can push to a repository is decided by GitHub, GitLab or your server, not by Git. A repository that only exists on your laptop is exactly as safe as your laptop. And some commands, git reset --hard and git clean among them, will destroy uncommitted work with no way back. Git protects what you have committed and pushed. It does not protect you from yourself before that.
The model in four ideas
1. Three places your code lives
At any moment, a file can differ across three areas:
- The working tree: the files on disk that you edit.
- The staging area (also called the index): a draft of your next commit.
- The repository: the committed history, stored in the
.gitdirectory.
git add copies changes from the working tree into the staging area. git commit turns the staging area into a permanent commit. That middle step is what lets you commit half the changes in a messy working tree and leave the rest for later.
2. A commit is a snapshot, not a diff
It is natural to think of a commit as a list of changes. Git does not store it that way. Each commit points to a full snapshot of the project, plus its parent commit, the author and the message. Files that did not change are not copied; the snapshot simply points at the identical file from before.
The diffs you see in git log -p or a pull request are calculated on the fly by comparing two snapshots. This is why switching branches is fast and why any commit can be checked out on its own.
3. A branch is just a pointer
A branch is not a copy of your code. It is a small file containing the hash of one commit. When you commit on a branch, the pointer moves forward to the new commit. Creating a branch costs almost nothing, which is why you should create them freely.
4. HEAD is where you are
HEAD points at the branch you currently have checked out. Commit, and the branch HEAD points at moves forward. If HEAD points directly at a commit instead of a branch, you are in "detached HEAD" state: you can look around and even commit, but those commits belong to no branch and are easy to lose unless you create one.
With those four ideas, the rest of this post is mostly vocabulary.
Starting and inspecting
git clone https://github.com/owner/repo.git
Copies the entire repository and its history, sets up a remote called origin, and checks out the default branch.
git status
git status -sb # short format, with branch and ahead/behind info
Shows what is modified, what is staged, what is untracked, and how far your branch is ahead of or behind its remote counterpart. Run it constantly. It is free, and it answers most "what just happened" questions.
git log --oneline --graph --all
Plain git log shows commit history for the current branch. The flags above turn it into a compact picture of every branch, which is the fastest way to understand a repository you have just cloned. Add -p to see the changes in each commit, or -- path/to/file to see the history of one file.
Making changes
git add src/app.ts # stage one file
git add -p # stage chosen parts of files, interactively
git add -A # stage everything, including new and deleted files
git add stages content exactly as it is at that moment. If you edit the file again afterwards, the new edits are not staged until you add again. Files matched by .gitignore are skipped.
git add -p is the one worth building a habit around. It walks you through each change and asks whether to stage it, which is both how you make focused commits and how you catch the debugging line you forgot to delete.
git diff # working tree vs staging area: what you have not staged
git diff --staged # staging area vs last commit: what you are about to commit
git diff HEAD # everything since the last commit
git diff --name-only # just the file names
The difference between the first two is the source of a lot of confusion. If git diff shows nothing but you know you changed something, it is probably already staged.
git commit -m "Add retry with backoff to payment client"
Creates a commit from whatever is staged. Nothing more, nothing less.
Branches
git switch -c feature/retry # create a branch and switch to it
git switch main # switch to an existing branch
git branch # list local branches
git branch --all # include remote-tracking branches
git switch arrived in Git 2.23 to split the overloaded git checkout into clearer commands, and it is no longer marked experimental. git checkout still works and is not going away, but switch does one thing and is harder to misuse.
A note on git branch --all: the remotes/origin/... entries are your local copy of what the remote looked like the last time you fetched. They are not a live view of the server.
Working with remotes
git remote -v
Lists each remote with its fetch and push URLs. Useful when you are not sure where git push is about to send your work.
git fetch
Downloads new commits from the remote and updates your remote-tracking branches such as origin/main. It never changes your own branches or your working tree, which makes it completely safe to run at any time.
git pull
A fetch followed by integrating the remote branch into yours. By default that integration is a merge; with --rebase it replays your local commits on top instead. If your branch and the remote have both moved and you have not told Git which you prefer, modern versions refuse to guess and ask you to choose. Set a default once (see the configuration section below) and the question goes away.
A useful habit: git fetch then git log --oneline --graph --all to see what came in, then decide how to integrate. pull is convenient, but it does two things at once.
git push
git push -u origin HEAD
Sends your commits to the remote. git push origin HEAD means "push the branch I am on to a branch of the same name on origin", which saves typing the branch name. -u records that relationship, so future git push and git pull on that branch need no arguments.
Setting work aside
git stash # save tracked changes and clean the working tree
git stash -u # also include untracked files
git stash list
git stash pop # re-apply the latest stash and remove it
git stash apply # re-apply but keep it in the list
The trap here is the default. Plain git stash saves changes to files Git already tracks, staged or not, but leaves brand new untracked files behind. If the file you care about has never been committed, you need -u.
Stashes are also easy to forget. For anything you will not come back to within the hour, a throwaway branch with a "wip" commit is more honest and much harder to lose.
Undoing things, safely
This is where understanding the model pays for itself.
Discard changes to a file you have not staged.
git restore src/app.ts
Permanent. The edits were never committed, so Git has no copy of them.
Unstage a file but keep your edits.
git restore --staged src/app.ts
Fix the last commit before pushing it.
git commit --amend
Replaces the last commit with a new one, including anything you have staged since. The new commit has a different hash, so only do this to commits you have not pushed, or be ready to force push a branch nobody else uses.
Undo a commit that is already shared.
git revert <commit>
Creates a new commit that reverses the old one. History stays intact and nobody else's clone is disturbed. On a shared branch, this is almost always the right answer.
Move a branch back to an earlier commit.
git reset --soft HEAD~1 # undo the commit, keep changes staged
git reset HEAD~1 # undo the commit, keep changes unstaged (the default, --mixed)
git reset --hard HEAD~1 # undo the commit and throw the changes away
reset rewrites where your branch points. It is fine on local work and trouble on anything others have pulled.
Recover from almost anything.
git reflog
The reflog is a local diary of every position HEAD has been in: every commit, checkout, reset and rebase. If a reset or rebase went wrong, find the entry from before it and point a branch back there with git branch rescue <hash>. Entries are kept for around 90 days by default. Most "I lost my commits" moments are a two minute fix for someone who knows the reflog exists.
If you must force push, do it with a lease.
git push --force-with-lease
Plain --force overwrites whatever is on the remote. --force-with-lease refuses if someone has pushed since you last fetched, which turns a silent loss of their work into an error message.
Four commands that feel advanced and are not
git bisect finds the commit that introduced a bug by binary search. Mark a known bad commit and a known good one, test the commit Git checks out, and repeat. Across a thousand commits it takes about ten steps. git bisect run ./test.sh automates the whole thing.
git blame -L 40,60 src/app.ts shows the last commit to touch each line. Its real use is not assigning blame; it is finding the commit message and pull request that explain why a strange line exists.
git cherry-pick <commit> copies one commit onto your current branch as a new commit. Handy for pulling a single fix into a release branch.
git log -S "functionName" finds commits that added or removed a given string. It is the fastest way to discover when something was deleted.
A .gitignore and three settings worth having
A .gitignore at the root of the repository keeps build output, dependencies and secrets out of commits:
node_modules/
dist/
.env
*.log
It only affects untracked files. If a file is already committed, adding it to .gitignore does nothing until you run git rm --cached <file>. And if a secret was ever committed and pushed, ignoring it now is not enough: rotate the secret, because it is in the history.
Three settings remove most day to day friction:
git config --global init.defaultBranch main # new repositories start on main
git config --global pull.rebase true # pull rebases instead of merging
git config --global push.autoSetupRemote true # first push sets up tracking automatically
If your team prefers merge commits, use git config --global pull.ff only instead of pull.rebase, which makes pull refuse anything other than a clean fast forward so you decide how to integrate.
Commit messages people can use
A commit message is a note to the next person who runs git blame on your line, and that person is often you.
- A short summary line, around 50 characters, written as an instruction: "Add retry to payment client", not "Added retries" or "fixes".
- A blank line, then a body wrapped at about 72 characters when the why is not obvious.
- Say why the change was made. The diff already says what.
Many teams also use Conventional Commits, with prefixes such as feat:, fix: and refactor:, which makes history easy to scan and lets tools generate changelogs.
What is changing
Git is preparing for a 3.0 release that will make SHA-256 the default hash for new repositories, use main as the default branch name, and switch to a faster storage format for references. There is no release date yet, and existing repositories and commands will keep working. None of it changes the model above.
The short version
Your code lives in three places: the working tree, the staging area and the repository. A commit is a snapshot, a branch is a pointer, and HEAD is where you are. add stages, commit records, fetch looks, pull integrates, push shares. Stash with -u if the file is new. Use restore for uncommitted mistakes, revert for shared ones, and reset only on work nobody else has. Force push with a lease. And when something goes badly wrong, before you search for a scary command, run git reflog.
The best way to make this stick is to break things on purpose. Make a scratch repository, commit a few files, then reset, rebase, stash and recover until none of it feels dangerous. Ten minutes of that is worth more than any cheat sheet, including this one.
Comments (0)
Comments are closed for now.
No comments yet.
Stuck on something specific?
Writing only gets you so far. If you want an answer to your situation rather than the general case, book a session and we will work through it together. Sessions are free for approved Sefism members, and a few slots open each week.
Follow along
New writing, resources and project ideas land here first.
Hand-picked courses, roadmaps, guides and tools.
Realistically scoped final-year project ideas.
Career questions people ask, answered in full.
Who hires in Pakistan, how they hire, and what it pays.
Where to study computing: admissions, tests and programmes.