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:

  1. a pointer to the snapshot (content state)
  2. metadata (author/committer/time/message)
  3. pointer(s) to parent commit(s)

Commits form a DAG:

1
2
graph LR
C1((C1)) --> C2((C2)) --> C3((C3))

If you merge, the merge commit has two parents (we’ll come back to this later):

1
2
3
graph LR
A((A)) --> M((Merge))
B((B)) --> M

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
2
main  -->  <hash-of-C3>
dev --> <hash-of-C2>

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
2
3
flowchart LR
C1((C1)) --> C2((C2)) --> C3((C3))
main -.-> C3

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
2
3
flowchart LR
C1((C1)) --> C2((C2)) --> C3((C3)) --> C4((C4))
main -.-> C4

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”.
  • main is telling Git “the current commit is C3”.
1
2
3
flowchart TD
HEAD -.-> main
main -.-> C3((C3))

And the special case (detached HEAD) is:

1
2
HEAD -> C2
main -> C3
1
2
3
flowchart TD
HEAD -.-> C2((C2))
main -.-> C3((C3))

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
2
3
flowchart LR
W["Working tree<br/>your working directory"] -->|git add| I["Index (staging area)<br/>a not-yet-saved commit draft"]
I -->|git commit| R["Repository<br/>the DAG of commits"]

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

  1. moves content across the 3 layers (add, commit, restore), or
  2. 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
2
3
4
graph LR
C1(("C1: V1"))
main[main] --> C1
HEAD[HEAD] --> main

Clean state:

1
2
3
Working tree: README = V1
Index: README = V1
Repo/HEAD: C1 (V1)

Edit README to V2:

1
2
3
Working tree: README = V2   (changed)
Index: README = V1
Repo/HEAD: C1 (V1)

Stage it:

  • git add README.md
1
2
3
Working tree: README = V2
Index: README = V2 (this is the “commit draft”)
Repo/HEAD: C1 (V1)

Commit it:

  • git commit -m "Update README"

Now the DAG grows:

1
2
3
4
graph LR
C1(("C1: V1")) --> C2(("C2: V2"))
main[main] --> C2
HEAD[HEAD] --> main

And you’re clean again:

1
2
3
Working tree: README = V2
Index: README = V2
Repo/HEAD: C2 (V2)

Repeat once more so we have C3 (V3):

1
2
3
4
graph LR
C1(("C1: V1")) --> C2(("C2: V2")) --> C3(("C3: V3"))
main[main] --> C3
HEAD[HEAD] --> main

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
2
git checkout/switch <branch>
git checkout/switch <hash of a commit>

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
2
3
4
Working tree: README = V2
Index: README = V2
HEAD: detached at C2
main: still points to C3

Picture:

1
2
3
4
graph LR
C1((C1)) --> C2((C2)) --> C3((C3))
main[main] --> C3
HEAD[HEAD] --> C2

From detached HEAD at C2, you edit and commit:

  1. edit README to V2b
  2. git add README.md
  3. git commit -m "Experiment"

You create C4 on top of C2:

1
2
3
4
5
graph LR
C1((C1)) --> C2((C2)) --> C3((C3))
C2 --> C4((C4))
main[main] --> C3
HEAD[HEAD] --> C4

Now you go back:

  • git switch main (or git 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), or
    • git 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
  • main moves from C3 to C2
1
2
3
4
graph LR
C1((C1)) --> C2((C2)) --> C3((C3))
main[main] --> C2
HEAD[HEAD] --> main

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
2
3
(local)   main        ->  ...
(local) origin/main -> ... (your last known remote state)
(remote) main -> ... (the actual branch in the remote repo)

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:


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
2
mkdir myproj && cd myproj
git init

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
2
3
git add README.md
git add .
git add -p # stage interactively (hunks)

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 index
  • git diff --staged (or --cached) = index vs HEAD
  • git diff <commit1> <commit2> = compare two commits

Examples:

1
2
3
git diff
git diff --staged
git diff HEAD~1 HEAD

git log: read history (commits)

git log shows commits reachable from your current position (HEAD).

Useful variants:

1
2
3
4
git log
git log --oneline --graph --decorate
git log -p # include patch (diff) per commit
git log -- <path> # history for a file

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
2
3
git push -u origin main
# or for a feature branch:
git push -u origin feature-x

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/main does not automatically move just because you pushed; it typically updates on the next git fetch (or git pull)

git branch: list / create / delete branches

1
2
3
git branch            # list local branches
git branch feature-x # create branch pointing to current commit
git branch -d feature-x

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
2
3
4
5
6
7
8
Before fetch:
main -> C3
origin/main -> C3
(remote main is actually at C5)

After fetch:
main -> C3 (unchanged)
origin/main -> C5 (updated)

git pull

git pull = git fetch + integrate into your current branch (via merge or rebase, depending on config).

1
2
3
git pull
# or explicitly:
git pull --rebase

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.