Skip to main content

Git cheatsheet

Learn Git step by step, from your first commit to rescuing "lost" work. Each section has three parts:

  • In short โ€” the idea in one sentence.
  • Example โ€” commands with a comment saying what each one does or prints.
  • Try it โ€” a tiny exercise to check you understood.

Want the longer story behind a topic? The complete guide walks through it in more depth.

๐Ÿ“– Full guide: Git โ†’ Beginner
How to use this page

First create the practice repo โ€” every example runs inside it, and nothing you do there can hurt real work. Then go through Part 1 in order. Move to Part 2 once you're comfortable, and Part 3 when you need to rescue or investigate something. Type the commands yourself.

Contentsโ€‹

Practice repo

Part 1 โ€” Beginner: What Git is ยท Setting up ยท Saving changes ยท Seeing history ยท Branches ยท Merging ยท Remotes ยท Ignoring files

Part 2 โ€” Core: Resolving conflicts ยท Undoing changes ยท Stash ยท Rebasing ยท Cleaning up commits ยท Tags ยท The pull-request flow ยท Common mistakes

Part 3 โ€” Advanced: Reflog: finding lost work ยท Cherry-pick ยท Bisect: finding the bad commit ยท Force-pushing safely ยท Hooks ยท How Git stores data ยท Big repositories ยท Words you'll meet

Practice repoโ€‹

In short: a tiny project with two commits and its own local "remote" (a second repository in the next folder), so you can practise pushing and pulling without GitHub.

# Practice repo: run once, in any empty folder
mkdir git-practice && cd git-practice
git init --bare -b main origin.git # plays the part of GitHub
git init -b main shop && cd shop # your working copy
git config user.name "Ada"
git config user.email "ada@example.com"
echo "# Shop" > README.md
git add README.md
git commit -m "Add README"
echo "print('hello')" > app.py
git add app.py
git commit -m "Add app"
git remote add origin ../origin.git
git push -u origin main

You're now inside git-practice/shop, on branch main, with two commits that are also on the remote. To start over, delete the git-practice folder and run this again.

Part 1 โ€” Beginnerโ€‹

1. What Git isโ€‹

In short: Git records snapshots of your project over time, so you can see what changed, go back, and work on several things at once without them getting in each other's way.

WordMeaning
Repository (repo)A project folder whose history Git tracks (the history lives in .git/)
CommitOne saved snapshot, with a message, an author, and an id like 3f2a9c1
BranchA movable name pointing at a commit โ€” a separate line of work
RemoteAnother copy of the repo, usually on GitHub or GitLab, called origin
HEAD"Where you are now" โ€” usually the current branch

Every change moves through three places:

working folder  --git add-->  staging area  --git commit-->  history
(files you edit) (what goes in (saved snapshots)
the next commit)
git status          # which branch you're on, and which files changed

Try it: in the practice repo, run git status and git log --oneline. How many commits are there?

2. Setting upโ€‹

In short: tell Git who you are once per computer, then either create a new repo or copy ("clone") an existing one.

git config --global user.name "Ada Lovelace"       # stored in ~/.gitconfig
git config --global user.email "ada@example.com"
git config --global init.defaultBranch main # new repos start on "main"
git config --global pull.rebase true # see section 13 for why

git init my-project # new, empty repo in my-project/
git clone ../origin.git shop-copy # copy an existing repo (a URL works the same way)
git config --list --show-origin # every setting, and which file it came from

--global settings apply to every repo on your computer; without --global, a setting applies to the current repo only.

Try it: clone the practice remote into a folder called second-copy and check git log --oneline shows the same two commits.

3. Saving changesโ€‹

In short: git add chooses what goes into the next snapshot; git commit saves it with a message.

echo "print('bye')" >> app.py          # change a tracked file
echo "flask" > requirements.txt # create a new file

git status # app.py modified, requirements.txt untracked
git add app.py requirements.txt # stage both (git add . stages everything here)
git status # both listed under "Changes to be committed"
git commit -m "Say goodbye and add requirements"
git status # nothing to commit, working tree clean

Write the message as a short summary of why, in the imperative: "Add login rate limit", not "changes" or "fixed stuff". Commit small, related changes together โ€” one idea per commit.

Try it: change two files, but stage and commit only one of them. Check git status shows the other is still modified.

4. Seeing history & changesโ€‹

In short: git log lists commits; git diff shows exactly what changed, line by line.

git log --oneline                     # one line per commit: id + message
git log --oneline --graph --all # with branches drawn as a graph
git log -p -1 # the latest commit, with its changes
git log --author="Ada" --since="2 weeks ago"

echo "# TODO" >> app.py
git diff # unstaged changes (working folder vs staging area)
git add app.py
git diff --staged # staged changes (staging area vs last commit)
git show HEAD # the latest commit: message + changes
git blame README.md # who last changed each line, and in which commit

In a diff, lines starting with + were added and lines with - were removed. HEAD~1 means "one commit before HEAD", HEAD~2 two before, and so on.

Try it: show only the commits that touched app.py (git log --oneline -- app.py).

5. Branchesโ€‹

In short: a branch lets you work on something without changing main. Creating one is instant โ€” it's just a new name pointing at a commit.

git switch -c feature/login           # create a branch and move onto it
echo "def login(): pass" > login.py
git add login.py
git commit -m "Add login stub"

git branch # list branches; * marks the current one
git switch main # go back โ€” login.py disappears from the folder
git switch feature/login # ...and comes back
git branch -m feature/login feature/sign-in # rename

Name branches after the work: feature/login, fix/crash-on-empty-cart. git switch is the modern command; older guides use git checkout for the same thing.

Try it: create a branch docs/readme, add a line to README.md, commit it, then switch back to main and confirm the line isn't there.

6. Mergingโ€‹

In short: git merge brings another branch's commits into the branch you're on.

git switch -c feature/login
echo "def login(): pass" > login.py
git add login.py && git commit -m "Add login stub"

git switch main
git merge feature/login # "Fast-forward": main simply moves up to feature/login
git branch -d feature/login # delete the branch โ€” its commits are now on main
git log --oneline --graph
KindWhenResult
Fast-forwardmain has no new commits since the branch startedmain just moves forward; no extra commit
Merge commitBoth branches have new commitsA new commit with two parents joins them

git merge --no-ff feature/login always makes a merge commit, which keeps a visible record that a branch existed.

Try it: make a commit on main and on a new branch, then merge. Look at git log --oneline --graph โ€” you'll see the two lines join.

7. Remotesโ€‹

In short: push sends your commits to the remote; fetch downloads theirs; pull downloads and merges them into your branch.

git remote -v                          # origin  ../origin.git (fetch/push)

echo "v2" >> README.md
git commit -am "Update README" # -a stages every tracked file that changed
git push # send main's new commit to origin

git switch -c feature/search
echo "def search(): pass" > search.py
git add search.py && git commit -m "Add search stub"
git push -u origin feature/search # -u: remember origin/feature/search for next time

git fetch # download new commits; your files don't change
git status # "Your branch is up to date with 'origin/feature/search'"
git pull # fetch + merge (or rebase) into the current branch

origin/main is your copy of "where main was on the remote at the last fetch". git log --oneline main..origin/main lists commits the remote has that you don't yet.

Try it: clone the remote into a second folder, commit and push from there, then run git fetch and git log --oneline main..origin/main in the first.

8. Ignoring filesโ€‹

In short: a .gitignore file lists files Git should never track โ€” build output, logs, secrets, editor settings.

cat > .gitignore <<'EOF'
# comments start with #
__pycache__/
*.log
.env
build/
!build/keep.txt
EOF
# above: a folder anywhere ยท every .log file ยท one file ยท a folder ยท except this one (!)

echo "SECRET=1" > .env
git status --short # .env is not listed
git check-ignore -v .env # .gitignore:4:.env .env โ€” which rule matched
git add .gitignore && git commit -m "Ignore build output and secrets"

.gitignore only affects files Git isn't tracking yet. To stop tracking a file you already committed: git rm --cached secrets.txt, then add it to .gitignore โ€” and if it held a real secret, rotate the secret, because it's still in the history.

Try it: create debug.log and notes/todo.txt; ignore all .log files and the whole notes/ folder, then check with git status.

Part 2 โ€” Coreโ€‹

9. Resolving conflictsโ€‹

In short: a conflict happens when two branches change the same lines. Git marks both versions in the file; you choose the result and commit.

git switch -c feature/greeting
echo "print('hi from feature')" > app.py
git commit -am "Feature greeting"
git switch main
echo "print('hi from main')" > app.py
git commit -am "Main greeting"

git merge feature/greeting # โœ— CONFLICT (content): Merge conflict in app.py
git status # app.py listed under "Unmerged paths"
cat app.py
# <<<<<<< HEAD
# print('hi from main')
# =======
# print('hi from feature')
# >>>>>>> feature/greeting

echo "print('hi from both')" > app.py # edit to the result you want; delete the markers
git add app.py # mark it resolved
git commit --no-edit # finish the merge

git merge --abort gives up and puts everything back as it was. Many editors (VS Code, IntelliJ) show "Accept current / incoming / both" buttons for each conflict.

Try it: create a conflict in README.md, then resolve it by keeping both lines.

10. Undoing changesโ€‹

In short: pick the undo that matches where the change is โ€” only in your folder, staged, committed locally, or already pushed.

echo "oops" >> app.py
git restore app.py # throw away unstaged changes to a file

echo "oops" >> app.py && git add app.py
git restore --staged app.py # unstage (the change stays in the file)
git restore app.py

git commit --allow-empty -m "Too early"
git reset --soft HEAD~1 # undo the commit, keep its changes staged

echo "bad" > bad.txt && git add bad.txt && git commit -m "Bad commit"
git revert --no-edit HEAD # a NEW commit that undoes it โ€” safe on pushed history
git log --oneline -3 # Revert "Bad commit" ยท Bad commit ยท โ€ฆ
SituationCommand
Unstaged edits to a filegit restore <file>
Staged by mistakegit restore --staged <file>
Fix the last commit's message or add a forgotten file (not pushed)git commit --amend
Undo local commits, keep the changesgit reset --soft HEAD~1
Undo local commits and their changesgit reset --hard HEAD~1 โ€” destroys work
Undo a commit that's already pushedgit revert <commit>

Try it: commit a file by mistake, then use git reset --soft HEAD~1 and git restore --staged to get back to a clean state with the file still on disk.

11. Stashโ€‹

In short: git stash puts unfinished changes on a shelf so you can switch branches, then brings them back later.

echo "half-finished" >> app.py
git stash push -m "wip: new greeting" # the folder is clean again
git stash list # stash@{0}: On main: wip: new greeting

git switch -c hotfix # do something elseโ€ฆ
git switch main

git stash pop # bring the changes back and remove them from the shelf
git diff --stat # app.py | 1 +

git stash apply brings changes back but keeps them on the shelf; git stash -u also stashes new, untracked files. Don't leave things in the stash for long โ€” it's easy to forget what's there.

Try it: stash two separate changes, list them, then apply the older one with git stash apply stash@{1}.

12. Rebasingโ€‹

In short: git rebase main replays your branch's commits on top of the latest main, giving a straight history instead of a merge commit.

git switch -c feature/cart
echo "cart = []" > cart.py
git add cart.py && git commit -m "Add cart"

git switch main
echo "## Shop docs" >> README.md
git commit -am "Improve README" # main has moved on

git switch feature/cart
git rebase main # feature/cart now starts from the new main
git log --oneline --graph --all # one straight line, no merge commit
Before:        A---B---D  main              After:   A---B---D  main
\ \
C feature/cart C' feature/cart

The golden rule: never rebase commits that other people already have. Rebasing creates new commits (C' has a new id), so anyone who built on the old ones gets a confusing, duplicated history. Rebase your own local work; merge shared branches.

Try it: rebase a branch that conflicts with main. Fix the file, then git add it and run git rebase --continue (or git rebase --abort).

13. Cleaning up commitsโ€‹

In short: before sharing a branch, tidy its commits โ€” squash "fix typo" commits into the commits they fix, reword messages, drop mistakes.

git switch -c feature/tidy
echo "a" > a.txt && git add a.txt && git commit -m "Add a"
echo "b" > b.txt && git add b.txt && git commit -m "Add b"
echo "a2" >> a.txt && git add a.txt
git commit --fixup HEAD~1 # "fixup! Add a" โ€” marks this as a fix for "Add a"

git rebase -i --autosquash main # opens the editor with the fixup already placed
git log --oneline main.. # Add b ยท Add a โ€” the fix is folded in

In the editor that git rebase -i opens, change the word at the start of each line: pick keeps a commit, reword edits its message, squash / fixup merges it into the one above, drop deletes it. Save and close to run it.

Setting pull.rebase true (section 2) makes git pull rebase your local commits on top of the remote's, instead of adding a merge commit each time.

Try it: make three commits, then squash them into one with git rebase -i HEAD~3.

14. Tagsโ€‹

In short: a tag is a permanent name for one commit โ€” usually a release like v1.2.0.

git tag -a v1.0.0 -m "First release"    # annotated tag: has author, date, message
git tag # v1.0.0
git show v1.0.0 --stat # the tag's message, then the commit
git push origin v1.0.0 # tags aren't pushed unless you ask
git switch --detach v1.0.0 # look at the code as it was at the release
git switch main

Use annotated tags (-a) for releases. Plain git tag v1.0.0 makes a "lightweight" tag โ€” just a name, with no author or message. Version numbers usually follow SemVer (Semantic Versioning): MAJOR.MINOR.PATCH, where a MAJOR change breaks compatibility.

Try it: tag the current commit v0.1.0, make another commit, then list the commits since the tag with git log --oneline v0.1.0...

15. The pull-request flowโ€‹

In short: most teams never commit to main directly. You push a branch, open a pull request (PR, called a "merge request" on GitLab), and teammates review it before it's merged.

git switch main && git pull                  # 1. start from the latest main
git switch -c fix/empty-cart # 2. one branch per change
echo "# handle empty cart" >> app.py
git commit -am "Handle empty cart" # 3. small, focused commits
git fetch && git rebase origin/main # 4. catch up with main before asking for review
git push -u origin fix/empty-cart # 5. push, then open the PR on GitHub/GitLab
  1. Reviewers comment; you push more commits to the same branch and the PR updates itself.
  2. After approval, the PR is merged (often "squash and merge" โ€” one tidy commit on main), and the branch is deleted.

Keep PRs small โ€” a few hundred lines at most. Small PRs get reviewed faster and more carefully, and are easier to revert.

Try it: follow steps 1โ€“5 in the practice repo, then merge the branch into main yourself and push.

16. Common mistakesโ€‹

In short: the classic Git traps, and how to get out of each one.

MistakeWay out
Committed to main instead of a branch (not pushed)git branch fix/x (keeps the commit), then git reset --hard origin/main on main, then git switch fix/x
Wrong commit message (not pushed)git commit --amend -m "Better message"
Forgot a file in the last commit (not pushed)git add file then git commit --amend --no-edit
Committed a secretRotate the secret first; then remove it from history (git filter-repo) โ€” deleting the file in a new commit isn't enough
"detached HEAD"You're on a commit, not a branch. git switch -c new-branch to keep work, or git switch main to leave
git pull made a surprise merge commitUse git pull --rebase (or set pull.rebase true)
Push rejected: "fetch first"Someone pushed before you: git pull --rebase, then git push
reset --hard lost commitsThey're still in the reflog for about 90 days

Try it: make a commit on main by mistake, then move it onto a new branch using the first row of the table.

Part 3 โ€” Advancedโ€‹

17. Reflog: finding lost workโ€‹

In short: the reflog records every place HEAD has been, so commits "lost" by a reset, rebase, or deleted branch can almost always be found.

echo "important" > work.txt
git add work.txt && git commit -m "Important work"
git reset --hard HEAD~1 # oops โ€” the commit seems gone
git log --oneline -1 # it's not in the logโ€ฆ

git reflog -3 # โ€ฆbut the reflog remembers:
# <id> HEAD@{0}: reset: moving to HEAD~1
# <id> HEAD@{1}: commit: Important work
git reset --hard HEAD@{1} # jump back to it
cat work.txt # important

The reflog is local to your computer and entries expire after about 90 days. Work that was never committed (or stashed) isn't in it โ€” commit often.

Try it: delete a branch with unmerged commits (git branch -D), then recreate it from the reflog: git branch restored <id>.

18. Cherry-pickโ€‹

In short: git cherry-pick copies one specific commit from another branch onto the current one.

git switch -c release/1.0               # the release branch starts here
git switch main
echo "new feature" >> README.md
git commit -am "Add new feature" # NOT for the release
echo "fix: guard empty cart" >> app.py
git commit -am "Fix empty cart crash"
FIX=$(git rev-parse HEAD) # the fix's commit id

git switch release/1.0
git cherry-pick "$FIX" # only the fix, as a new commit on release/1.0
git log --oneline -2 # Fix empty cart crash ยท Add app โ€” no "new feature"

Typical use: a bug fix made on main that must also go into a release branch. git cherry-pick -x adds "(cherry picked from commit โ€ฆ)" to the message, so you can trace where it came from.

Try it: cherry-pick two commits at once with git cherry-pick A B.

19. Bisect: finding the bad commitโ€‹

In short: git bisect finds the commit that broke something by binary search โ€” halving the range each step โ€” so even 1,000 commits take about 10 checks.

for i in 1 2 3 4 5 6 7 8; do            # make 8 commits; the 6th breaks the "test"
if [ $i -ge 6 ]; then echo "fail $i" > status.txt; else echo "pass $i" > status.txt; fi
git add status.txt && git commit -qm "Change $i"
done

git bisect start
git bisect bad HEAD # it's broken now
git bisect good HEAD~7 # it worked at "Change 1"
git bisect run grep -q pass status.txt # exit 0 = good, 1 = bad; Git does the rest
# <id> is the first bad commit โ€ฆ Change 6
git bisect reset # go back to where you started

Without run, Git checks out a middle commit and you test it yourself, typing git bisect good or git bisect bad each time.

Try it: bisect by hand โ€” no run โ€” and count how many steps it takes for 8 commits.

20. Force-pushing safelyโ€‹

In short: after rewriting history you've already pushed (a rebase or amend), a normal push is rejected. --force-with-lease overwrites the remote only if nobody else pushed in the meantime.

git switch -c feature/rewrite
echo "v1" > f.txt && git add f.txt && git commit -m "Draft"
git push -u origin feature/rewrite

git commit --amend -m "Final" # rewrite the pushed commit
git push # โœ— rejected (non-fast-forward)
git push --force-with-lease # accepted โ€” the remote was where you last saw it

Plain --force overwrites whatever is there, including a teammate's commits. Only ever force-push your own branches โ€” protect main in your Git host's settings so nobody can force-push to it.

21. Hooksโ€‹

In short: hooks are scripts Git runs at certain moments โ€” before a commit, before a push โ€” to block mistakes automatically.

cat > .git/hooks/pre-commit <<'EOF'
#!/bin/sh
# Refuse commits that contain an obvious secret
if git diff --cached | grep -qE 'AKIA[0-9A-Z]{16}|BEGIN PRIVATE KEY'; then
echo "pre-commit: possible secret in staged changes" >&2
exit 1
fi
EOF
chmod +x .git/hooks/pre-commit

echo "key=AKIAABCDEFGHIJKLMNOP" > config.txt
git add config.txt
git commit -m "Add config" # โœ— pre-commit: possible secret in staged changes
git restore --staged config.txt
HookRunsTypical use
pre-commitBefore a commit is madeFormatting, linting, secret scanning
commit-msgAfter you write the messageEnforce a message format
pre-pushBefore a pushRun the fast tests

.git/hooks isn't shared when someone clones the repo. Teams share hooks with a tool like pre-commit (the framework) or Husky, and repeat the important checks in CI, since hooks can be skipped with --no-verify.

22. How Git stores dataโ€‹

In short: Git is a database of objects, each named by a hash of its contents. A commit points to a tree (a folder listing), which points to blobs (file contents).

git cat-file -t HEAD                    # commit
git cat-file -p HEAD # tree <id> ยท parent <id> ยท author โ€ฆ ยท message
git cat-file -p 'HEAD^{tree}' # the folder listing: blob <id> README.md, blob <id> app.py
git cat-file -p HEAD:README.md # # Shop โ€” the file's contents
git rev-parse HEAD # the full 40-character commit id
ObjectHolds
blobA file's contents (not its name)
treeNames and modes of files and subfolders, pointing at blobs and trees
commitA tree, parent commit(s), author, date, message
tag (annotated)A commit, a name, a tagger, a message

This is why a branch is cheap (a file holding one commit id), why identical files are stored once, and why changing any old commit changes the id of every commit after it.

23. Big repositoriesโ€‹

In short: for huge repos, download less โ€” fewer commits, fewer files, or big files stored elsewhere.

REPO="file://$(cd .. && pwd)/origin.git"          # a real URL works the same way
git -C ../origin.git config uploadpack.allowFilter true # GitHub and GitLab already allow this
git clone --depth 1 "$REPO" shallow # only the latest commit ("shallow clone")
git clone --filter=blob:none "$REPO" lazy # all commits, file contents fetched when needed
git -C shallow log --oneline | wc -l # 1
ToolHelps when
--depth 1 (shallow clone)CI builds that only need the latest code
--filter=blob:none (partial clone)Long history, but you want full git log
git sparse-checkout set src/apiA monorepo where you only work in one folder
Git LFS (Large File Storage)Big binary files โ€” images, models, videos
git maintenance startKeeps a big local repo fast in the background

24. Words you'll meetโ€‹

In short: the jargon, in one line each.

WordMeaning
SHA / commit idThe 40-character hash that names a commit; the first 7 characters are usually enough
originThe default name for the remote you cloned from
upstreamThe remote branch your local branch tracks; also the original repo of a fork
ForkYour own server-side copy of someone else's repo
Fast-forwardMoving a branch forward without a merge commit
Detached HEADBeing on a commit directly instead of on a branch
SquashCombining several commits into one
Trunk-based developmentEveryone merges small changes into main often; branches live hours or days
Git flowAn older model with long-lived develop and release branches
MonorepoMany projects in one repository
CODEOWNERSA file naming who must review changes to which folders

For the object model, workflows, and interview questions, see the complete guide.