Post

Version Control and Git

Version Control and Git

Commands Cheat Sheet

Git Commands Cheat Sheet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# init local repo and push to GitHub
git init
git branch -M main
git add .
git commit -m "Initial commit"
git remote add origin <url>
git push -u origin main

# clone existing GitHub repo
git clone <url>

# status / history
git status
git log --all --graph --decorate --oneline
git graph
git show <commit>                 # show one commit
git reflog                        # recover lost commits / branch moves

# inspect changes
git diff                          # unstaged changes
git diff --staged                 # staged changes
git diff HEAD                     # all changes vs current commit

# staging / commit
git add file.cpp
git add .
git add -p                        # interactively stage chunks
git restore --staged file.cpp     # unstage, keep working directory changes
git commit -m "Message"
git commit -am "Message"          # add + commit tracked files only
git commit --amend                # replace last commit

# discard / restore changes
git restore file.cpp              # discard working directory changes in file
git restore .                     # discard all tracked working directory changes
git clean -fd                     # delete untracked files/directories; dangerous

# branch / switch
git branch                        # view local branches
git branch -vv                    # view branches with upstream info
git branch feature                # create branch, do not switch
git switch feature                # switch to branch
git switch -c feature             # create and switch
git branch -d feature             # delete merged branch
git branch -D feature             # force delete branch

# detached HEAD
git switch --detach <commit>        # detach HEAD at commit
git switch -c new-branch <commit>   # create new branch at commit and switch

# checkout legacy
git checkout main                 # switch to branch
git checkout -b feature           # create and switch
git checkout <commit>             # detached HEAD
git checkout -- file.cpp          # restore file

# objects
git cat-file -t <hash>            # type: blob, tree, commit, tag
git cat-file -p <hash>            # pretty-print content

# remotes
git remote -v                     # view remotes
git remote add origin <url>       # add remote named origin
git fetch origin                  # fetch updates from remote, don't touch files
git pull --ff-only                # pull only if fast-forward is possible
git push                          # push current branch to upstream
git push -u origin main           # push main and set upstream
git push -u origin HEAD           # push current branch and set upstream

# merge / rebase
git merge main                    # merge main into current branch
git rebase main                   # replay current branch on top of main

# stash
git stash push -u -m "wip"        # stash changes, including untracked files
git stash list                    # view stashes
git stash pop                     # apply and remove from stash list
git stash apply                   # apply but keep in stash list
git stash show -p stash@{0}       # inspect stash content

# reset
git reset --soft <commit>         # move branch, keep changes staged
git reset --mixed <commit>        # move branch, keep changes unstaged
git reset --hard <commit>         # move branch and discard changes; dangerous
git reset --hard origin/main      # force local branch/files to match remote

# ignore debugging
git status --ignored              # view ignored files
git check-ignore -v path/to/file  # debug why a file is ignored
git add -f path/to/file           # force add an ignored file

Quick Mental Models

git add          = put changes into staging area
git commit       = snapshot staging area
git branch       = create/view/delete branch pointers
git switch       = move HEAD to another branch
git fetch        = download remote refs, don't touch working files
git pull         = fetch + merge/rebase
git push         = upload local commits
git stash        = temporarily save current edits
git reset --hard = force branch + index + files to match a commit

Dangerous Commands

1
2
3
4
git reset --hard
git clean -fd
git branch -D
git push --force

Use safer force push:

1
git push --force-with-lease

Things I Might Forget

  • git commit commits the staging area, not necessarily all modified files.
  • git branch feature creates a branch but does not switch to it.
  • git switch feature switches to a branch.
  • git switch <commit> fails; use git switch --detach <commit>.
  • git checkout <commit> enters detached HEAD.
  • Detached HEAD still lets me edit files; the problem only happens if I commit without a branch.
  • git remote add origin URL does not change the current branch.
  • git push pushes committed changes, not uncommitted working directory changes.
  • git push usually pushes the current branch to its upstream, not all branches.
  • git fetch is safe: it downloads remote updates but does not change working files.
  • git pull --ff-only refuses non-fast-forward merges.
  • git reset --hard origin/main forces local branch and files to match the remote.
  • git stash push -u also saves untracked files.
  • If .gitignore behaves weirdly, use git check-ignore -v.

Why Version Control?

A version control system keeps project history so we can view old snapshots, know who changed what, work on branches, collaborate, and roll back when something breaks.

Key idea: do not learn Git as magic commands. Think of Git as a data structure:

1
Git = object database + commit graph + references

Git’s Data Model

Git stores history as snapshots, not mainly as line-by-line diffs.

1
history = series of snapshots

Core objects:

1
2
3
blob   = file content
tree   = directory mapping names to object hashes
commit = root tree hash + parent commit hash(es) + metadata

A blob stores bytes, not the file name. The file name lives in a tree.

1
2
3
4
5
6
7
8
9
10
11
root/
|-- foo/
|   `-- bar.txt
`-- baz.txt

tree_root:
  foo     -> hash(tree_foo)
  baz.txt -> hash(blob_baz)

tree_foo:
  bar.txt -> hash(blob_bar)

A commit points to the root tree and its parent commit(s):

1
2
3
4
5
6
commit:
  tree = hash(root tree)
  parent(s) = hash(parent commits)
  author
  message
  timestamp

Commit Graph

Git history is a DAG: Directed Acyclic Graph. Each commit points back to its parent(s).

Straight history:

1
A <- B <- C <- D

Branch:

1
2
A <- B <- C <- D
                     E <- F

Merge:

1
2
3
A <- B <- C <- D <- M
          \        /
           E <- F

Merge commit M has two parents:

1
2
parent 1 = D
parent 2 = F

Immutability and Content Addressing

Commits are immutable. Editing history creates new commits and moves references.

1
git commit --amend
1
2
Before: A <- B <- C
After:  A <- B <- C'

Git stores objects by hash:

1
hash(content) -> content

If one byte changes, the hash changes. Since commits contain tree hashes and trees contain blob hashes:

1
blob changes -> tree changes -> commit changes

This is why commits are immutable.

Repository, References, and HEAD

A repository is mostly:

1
2
3
.git/
|-- objects/
`-- refs/

References are human-readable names pointing to commit hashes.

1
2
3
main    -> abc123
feature -> def456
HEAD    -> main

A branch is a mutable pointer to a commit. Commits are immutable; references move.

1
2
Before commit: A <- B      main -> B
After commit:  A <- B <- C main -> C

HEAD means where I currently am.

Normal HEAD:

1
HEAD -> main -> commit C

Detached HEAD:

1
HEAD -> commit

Detached HEAD is not read-only. We can edit files, but a new commit is not held by a branch unless we create one:

1
2
git switch --detach <commit>
git switch -c my-work

Staging Area

Git has three important areas:

1
2
3
HEAD              = current committed snapshot
staging area      = snapshot prepared for the next commit
working directory = real files currently being edited

Flow:

1
working directory --git add--> staging area --git commit--> commit

git commit commits the staging area, not necessarily the whole working directory.

1
2
3
git add main.cpp
git commit -m "Fix parser bug"
git add -p    # interactively stage chunks

The staging area helps create clean commits, especially when one file contains both real fixes and temporary debug prints.

Basic Commands

1
2
3
4
5
6
7
git init                         # create .git/
git status                       # branch, modified, staged, untracked files
git add file.cpp                 # stage file
git add .                        # stage many changes
git commit -m "Message"          # commit staged snapshot
git log                          # history
git log --all --graph --decorate --oneline

Default branch can be main or master, depending on config.

1
2
git branch -M main
git config --global init.defaultBranch main

Useful alias:

1
2
git config --global alias.graph "log --all --graph --decorate --oneline"
git graph

Inspecting Objects

git cat-file inspects Git’s object database.

1
2
git cat-file -t <object-id>   # type: blob, tree, commit, tag
git cat-file -p <object-id>   # pretty-print object content

Examples:

1
2
3
4
git cat-file -t HEAD
git cat-file -p HEAD
git cat-file -p <tree-hash>
git cat-file -p <blob-hash>

Commit output looks like:

1
2
3
4
5
6
tree abc123
parent def456
author ...
committer ...

Commit message

Tree output looks like:

1
2
100644 blob <hash> README.md
040000 tree <hash> src

Common modes:

1
2
3
100644 = normal file
100755 = executable file
040000 = directory/tree

Branch, Switch, Checkout

git branch manages branch pointers.

1
2
3
4
git branch              # view branches
git branch feature      # create branch, do not switch
git branch -d feature   # delete branch
git branch -M main      # rename current branch

git switch switches branches.

1
2
3
git switch main
git switch -c feature          # create and switch
git switch --detach <commit>   # detach HEAD at commit

git switch <commit> fails because switch expects a branch by default.

git checkout is the older command that does many things:

1
2
3
4
5
git checkout main
git checkout -b feature
git checkout <commit>          # detached HEAD
git checkout -- file.cpp       # restore file
git checkout <commit> -- file.cpp

Modern mapping:

1
2
3
4
5
git checkout main              -> git switch main
git checkout -b feature        -> git switch -c feature
git checkout -- file.cpp       -> git restore file.cpp
git checkout <commit> -- file  -> git restore --source <commit> file
git checkout <commit>          -> git switch --detach <commit>

Prefer switch and restore when possible because they are clearer.

Remotes, Push, and Upstream

A remote is another repository, usually GitHub.

1
2
git remote add origin <url>
git remote -v

origin is just a nickname for a remote URL. Adding it does not change the current branch or modify main.

1
2
origin https://github.com/user/repo.git (fetch)
origin https://github.com/user/repo.git (push)

git push sends committed changes to a remote. It does not push uncommitted working directory edits.

1
2
3
git add .
git commit -m "..."
git push

Usually git push pushes the current branch to its upstream, not all branches.

1
2
3
git push origin main              # push specific branch
git push -u origin fix-crawler    # push new branch and set upstream
git push --all origin             # push all branches; do not use casually

An upstream is the remote branch tracked by a local branch.

1
local main tracks origin/main
1
2
git push -u origin main   # -u = --set-upstream
git branch -vv            # show upstreams

After upstream is set, Git knows what git push and git pull mean for that branch.

Fetch, Pull, and Reset

git fetch downloads new commits/references and updates remote-tracking branches, but does not change local files.

1
git fetch origin
1
2
3
4
5
6
7
8
9
Before:
local main:   A <- B
origin/main:  A <- B

Remote has C.

After fetch:
local main:   A <- B
origin/main:  A <- B <- C

git pull is roughly:

1
2
git fetch
git merge

git pull --ff-only only updates if a fast-forward is possible.

1
git pull --ff-only

Fast-forward:

1
2
3
4
local main:   A <- B
origin/main:  A <- B <- C

After: main: A <- B <- C

Diverged history:

1
2
local main:   A <- B <- D
origin/main:  A <- B <- C

In this case, --ff-only fails instead of creating an unexpected merge commit.

git reset --hard forces the current branch, staging area, and working directory to match a commit.

1
git reset --hard origin/main
1
2
3
current branch -> origin/main
staging area   -> origin/main
working dir    -> origin/main

Danger: unstashed/uncommitted work is lost. Unique local commits can become unreachable from the branch.

Stash

git stash temporarily saves current changes and makes the working directory clean.

1
2
3
4
git stash push -u -m "wip"   # -u includes untracked files
git stash list
git stash pop                 # apply and remove from stash list
git stash apply               # apply but keep in stash list

Mental model:

1
stash = temporary hidden commit in a drawer

Jump to Latest Main but Keep Current Edits

Goal:

1
2
jump to the latest main
but keep my current file changes

Force local main to match GitHub:

1
2
3
4
5
git stash push -u -m "wip before updating main"
git fetch origin
git switch main
git reset --hard origin/main
git stash pop

Safer version that does not delete local commits on main:

1
2
3
4
git stash push -u -m "wip before updating main"
git switch main
git pull --ff-only
git stash pop

Use pull --ff-only for safety. Use reset --hard origin/main only when local main should exactly match GitHub.

.gitignore

.gitignore ignores intentionally untracked files.

*.csv
__pycache__/
.env

Debug ignored files:

1
2
3
4
git check-ignore -v path/to/file
git status --ignored
git config --get core.excludesfile
git add -f path/to/file

Ignore rules can come from:

1
2
3
4
.gitignore
folder/.gitignore
.git/info/exclude
global gitignore

Usually run git check-ignore -v before force-adding an ignored file.

Common Workflows

Create a new repo and push to GitHub:

1
2
3
4
5
6
git init
git branch -M main
git add .
git commit -m "Initial commit"
git remote add origin <url>
git push -u origin main

Create a new branch and work there:

1
2
3
4
5
git switch -c fix-crawler
# edit files
git add .
git commit -m "Fix crawler"
git push -u origin fix-crawler

Switch branch but keep modifications:

1
git switch main

If Git refuses because changes would be overwritten:

1
2
3
git stash push -u
git switch main
git stash pop

Discard uncommitted changes:

1
2
3
4
5
git restore file.cpp       # one file
git restore .              # all tracked changes
git clean -fd              # untracked files too; deletes files
git restore --staged file.cpp
git reset file.cpp         # older style unstage

Mental Models

1
2
3
4
5
6
7
8
9
10
11
12
13
14
repo = objects + references
commit = root tree hash + parent hashes + metadata
branch = mutable pointer to commit
HEAD = where I currently am
normal HEAD = HEAD -> branch -> commit
detached HEAD = HEAD -> commit
commit creation = working directory -> staging area -> commit
origin = nickname for remote repository URL
origin/main = remote-tracking branch
upstream = remote branch tracked by local branch
fetch = download remote updates, do not touch my files
pull = fetch + merge/rebase into current branch
reset --hard H = make current branch + index + working directory exactly H
stash = temporarily save current edits somewhere else
This post is licensed under CC BY 4.0 by the author.