Git for Users
Sources: * https://git-scm.com/docs
Git for Users
Git is a distributed version control system (DVCS). It helps you track and manage changes to a codebase, collaborate safely, and move between versions of your project (e.g., “what the repo looked like yesterday / last week / on a release tag”).
A practical way to use Git well is to separate two things:
- Snapshots of your project (commits) — the immutable records of versions
- Names/pointers (branches and HEAD) — the movable references you use to navigate and build on those versions
Most “hard” Git commands (checkout, reset, and their edge cases like detached HEAD) become straightforward once you keep that separation in mind.
This note is a Git user guide: it covers the common commands you’ll use day to day (status, add, commit, diff, log, branch, switch/checkout, merge/rebase basics, stash, restore, remote/push/pull), with extra emphasis on git checkout and git reset because they are frequently misunderstood.
1) Core concepts: commit, branch, HEAD
Commit = a snapshot node in history
Git records your project as a series of commits. Each commit is a snapshot of your tracked files (not “just the diff”). > When a file has been or was recorded in a commit before, then the file is "tracked". A tracked file can be edited in future, and the change should be staged and committed again, see git add and git commit in later sections. >
In essence, a commit has:
- a pointer to the snapshot (content state)
- metadata (author/committer/time/message)
- pointer(s) to parent commit(s)
Commits form a DAG:
1 | graph LR |
If you merge, the merge commit has two parents (we’ll come back to this later):
1 | graph LR |
Now that we know a commit is a snapshot node in the history DAG, we need a way to navigate that DAG and decide where new commits should attach. Git does this with pointers.
Branch = a pointer with a human name
A branch can be abstracted as a pointer to a commit.
Concretely, a branch is backed by a ref file whose name is the branch name (e.g., main, dev) and whose content is the hash of a commit.
So you can think of it like:
1 | main --> <hash-of-C3> |
When you create a new commit while you are on a branch, Git simply updates that ref file so the branch “moves forward” to the new commit.
1 | flowchart LR |
where branch "main" is a file named main and contains hash(C3).
After a new commit C4 on main, the file content changes from hash(C3) to hash(C4):
1 | flowchart LR |
HEAD = a pointer to where you are standing (usually a pointer-to-a-pointer)
HEAD is also a pointer-like thing, it points to where you're standing in the repository/DAG.
- Usually: HEAD points to a branch (not directly to a commit).
- Sometimes: HEAD points directly to a commit (detached HEAD). That case is special and we’ll handle it later.
So in the normal case, the chain is:
1 | HEAD -> main -> C3 |
- HEAD is telling Git “I’m currently on branch
main”. mainis telling Git “the current commit isC3”.
1 | flowchart TD |
And the special case (detached HEAD) is:
1 | HEAD -> C2 |
1 | flowchart TD |
2) The 3-layer mental model: working tree, index, repository
Now the model we’ll use for the rest of the note.
- Working tree = your working directory (the folder you edit; where
.git/lives). - Index (staging area) = the intermediate layer between working tree and repository. Despite the fact that it saves the changes to the files in working directories, not the complete files, i.e., snapshots. Without loss of generality, we can still think of it as a place to save the next commit (which is a snapshot) before it’s saved to the DAG — a “commit draft”.
- Repository = the DAG of commits (the saved snapshots).
1 | flowchart LR |
The 3-layer mental model, together with the abstractions treating commits, branches, HEADs as snapshots and pointers, are useful because almost every Git command either
- moves content across the 3 layers (
add,commit,restore), or - moves pointers that decide which commit you’re standing on (
switch/checkout,reset, merge/rebase). Usually, "hard commands" belong to this part.
3) An example of moving branches and HEAD
We’ll track one file: README.md. Versions: V1, V2, V3.
Start: you have one commit C1 with README.md V1. We refer it as "C1: V1" in the diagram.
1 | graph LR |
Clean state:
1 | Working tree: README = V1 |
Edit README to V2:
1 | Working tree: README = V2 (changed) |
Stage it:
git add README.md
1 | Working tree: README = V2 |
Commit it:
git commit -m "Update README"
Now the DAG grows:
1 | graph LR |
And you’re clean again:
1 | Working tree: README = V2 |
Repeat once more so we have C3 (V3):
1 | graph LR |
git checkout / git switch: move HEAD (to a commit or a branch)
checkout (and the newer replacement---switch) moves HEAD and updates your working tree (and usually your index) to match the target snapshot.
Usage:
1 | git checkout/switch <branch> |
So you can checkout to either
- a branch name (e.g.
dev,main) → HEAD attaches to that branch (symbolic ref). You are now “on branch dev”; or - a commit hash / tag (e.g.
C2,v1.2.3) → called detached HEAD at that commit. HEAD points directly to a commit, not to a branch.
Note: checkout only moves the HEAD, it won't move any branch, neither change any commit/snapshot.
Checking out to a branch is easy to understand, what frequently confuses people is checking out to a commit, resulting in "detatch HEAD".
Senario: Detarched HEAD
Suppose you’re at C3 on main (short for: “HEAD points to the main branch which points to C3 commit”). Now:
git checkout C2
The result is:
1 | Working tree: README = V2 |
Picture:
1 | graph LR |
From detached HEAD at C2, you edit and commit:
- edit README to
V2b git add README.mdgit commit -m "Experiment"
You create C4 on top of C2:
1 | graph LR |
Now you go back:
git switch main(orgit checkout main)
You’re back at C3, but C4 is still there — it’s just not pointed to by a branch name.
In this case, C4 is an unreferenced (often informally called “dangling”) commit: it’s reachable only by its hash (and temporarily via the reflog), so it’s easy to “lose track of”.
Eventually, Git may garbage-collect unreachable commits after they’ve been unreachable for long enough (often described as “around 60–90 days”, but the exact timing depends on your GC / reflog / repo settings).
Practical takeaway:
- If you want to keep work you did on detached HEAD, name it:
git switch -c experiment(create a branch at current commit), orgit branch experiment(then switch later).
git reset: move branches
reset moves the current branch name to another commit. It has mainly 3 modes. The mode decides whether index and working tree are also rewritten to match.
Now you are back on main at C3 (clean). Run:
git reset <mode> C2
We’ll say:
- HEAD stays attached to
main mainmoves fromC3toC2
1 | graph LR |
Now the modes:
--soft (move branch only)
git reset --soft C2
- branch moved back
- index and working tree keep what you had (
V3)
To recover from the resetting, the user should commit the staging area, which contains README.md V3, leading to a commit with README.md V3.
Intuition:
“Rewrite history pointer, keep my current work staged/ready.”
--mixed (move branch + reset index)
--mixed is the default mode of git reset.
git reset --mixed C2
- branch moved back
- index reset to
C2 - working tree still has
V3
To recover from the resetting, the user should add the working tree, which contains README.md V3, into the staging area, then commit the staging area, leading to a commit with README.md V3.
Intuition:
“Unstage everything, but keep the edits in my files.”
--hard (move branch + reset index + reset working tree)
git reset --hard C2
- branch moved back
- index =
C2 - working tree =
C2
Intuition:
“Throw away local edits and make my directory match that snapshot.”
Local repo vs remote repo
A local repository is your .git/ database on your machine: commits, branches, tags, reflog, etc.
A remote repository is another Git repository somewhere else (often a server, e.g., GitHub/GitLab). It’s not “the truth”; it’s just another copy that you can exchange commits with.
You connect them via remotes.
Common convention:
origin= the default name of the remote you cloned from (or added)
Note: since the repo repo is a copy of local repo, it should have exect the same commits. However, the local repo is unlikely to know where remote branches/pointers are, so it also saves the last seen (actually last fetched, see git fetch in later sections) remote fetch remote tracking branches, say origin/main, locally . User can't switch to that branch in local repo: 1
git switch origin/main # Throws error!!!
A useful picture:
1 | (local) main -> ... |
List remotes:
1 | git remote -v |
Add a remote repo (if you created the repo locally and you want to copy it to an empty remote repo):
1 | git remote add origin <url> |
Example URLs:
- SSH:
[email protected]:user/repo.git - HTTPS:
https://github.com/user/repo.git
Common Git Commands
This section is a practical “tour” of the Git commands you’ll use every day. The goal is not to memorize flags, but to know what each command changes (pointers vs layers) and when to reach for it.
git init: create a local repository
git init turns the current directory into a Git repository by creating a .git/ directory. After that, Git can track commits, branches, HEAD, and the index for this directory.
Typical flow:
1 | mkdir myproj && cd myproj |
What you get:
- A local repository (on your machine)
- An initial branch name (commonly
main, depending on your Git config) - No commits yet (until you commit)
git status: what changed, and where is it?
git status is your “flashlight”. It answers:
- What’s modified in the working tree but not staged?
- What’s staged in the index but not committed?
- What files are untracked?
Use it constantly.
git add: stage changes into the index
git add <path> copies the current content of files from the working tree into the index (staging area), i.e., into your “next commit draft”.
Common patterns:
1 | git add README.md |
git commit: write a snapshot into the repository (DAG)
git commit turns what’s in the index into a new commit in the repository DAG, and moves the current branch pointer forward.
1 | git commit -m "Explain reset modes" |
Common shortcut:
1 | git commit -am "msg" |
-a: also do git add for those already tracked (but may haven't been staged yet) files before committing.
git diff: inspect content differences (by layer)
Think “diff = compare two layers”:
git diff= working tree vs indexgit diff --staged(or--cached) = index vs HEADgit diff <commit1> <commit2>= compare two commits
Examples:
1 | git diff |
git log: read history (commits)
git log shows commits reachable from your current position (HEAD).
Useful variants:
1 | git log |
git push: publish your local commits to a remote
git push sends commits from your local repo to the remote, and updates the remote branch pointer (the real branch on the server, e.g., remote main).
First push of a branch typically sets upstream:
1 | git push -u origin main |
After upstream is set, you can often just do:
1 | git push |
Intuition:
- commits are copied to the remote repo (if the remote doesn’t already have them)
- the remote branch (on the server) moves forward
- your local
origin/maindoes not automatically move just because you pushed; it typically updates on the nextgit fetch(orgit pull)
git branch: list / create / delete branches
1 | git branch # list local branches |
Remember: a branch is a named pointer; creating/deleting a branch is usually just creating/deleting that name.
git fetch vs git pull
git fetch
git fetch downloads new objects (commits, etc.) that you don’t have locally, and updates your remote-tracking branches (like origin/main).
It does not:
- change your current local branch (
main) - change your working tree or index
1 | git fetch origin |
This is the safe “sync my view of the remote” command.
A typical outcome after someone pushed to the remote:
1 | Before fetch: |
git pull
git pull = git fetch + integrate into your current branch (via merge or rebase, depending on config).
1 | git pull |
Intuition:
- fetch updates
origin/main - then your current local branch is updated to include those remote commits (merge/rebase)
If you prefer more control, do fetch then merge/rebase explicitly.
git merge
Minimal mental model:
merge: combine two lines of history, often creating a merge commit
Tools for Git
To exclude files from being tracking, use .gitignore file.