Practical cheatsheet for day-to-day git. Favors the modern command surface (git switch / git restore over the overloaded git checkout) and the commands you actually need daily over exhaustive coverage. Scan the quick reference; jump to a section for details.
| Task | Command |
|---|---|
| Clone a repo (ssh) | git clone git@github.com:yourname/repo.git |
| See what's changed | git status --short / git diff |
| Stage a file | git add path/to/file |
| Stage everything | git add -A |
| Commit | git commit -m "message" |
| Push | git push |
| Pull with rebase | git pull --rebase |
| New branch + switch | git switch -c feature-x |
| Switch to existing branch | git switch main |
| Merge branch into current | git merge feature-x |
| Discard changes to file | git restore path/to/file |
| Unstage a file | git restore --staged path/to/file |
| Undo last commit, keep changes | git reset --soft HEAD~ |
| Revert a pushed commit safely | git revert <sha> |
| Stash current changes | git stash push -m "wip" |
| Get stashed changes back | git stash pop |
| See all commits (incl. lost ones) | git reflog |
| Show current branch | git branch --show-current |
| One-line log graph | git log --oneline --graph --decorate --all |
Git has three areas and files move between them:
.git/) — the object database and history.A tracked file is always in one of four states:
/etc/gitconfig — system-wide (--system)~/.gitconfig — per-user (--global).git/config — per-repo (default scope)git config --global user.name "Your Name"
git config --global user.email "you@example.com"
git config --global core.editor vim
git config --global merge.tool vimdiff
git config --global init.defaultBranch main
git config --global pull.rebase true # always rebase on pull
git config --list # all settings, with source
git config user.name # a single value
git config --show-origin -l # which file each setting comes from
# from scratch
git init
# clone (ssh preferred — no password prompts after SSH key setup)
git clone git@github.com:yourname/example-repo.git
# clone into a different directory name
git clone git@github.com:yourname/example-repo.git my-local-name
git status --short --ignored # ?? = untracked, !! = ignored, A/M/D = staged
git add path/to/file # stage a specific file
git add -A # stage everything (new, modified, deleted)
git add -p # stage hunks interactively — review before committing
git commit -m "short summary"
git commit # opens editor; first line = summary, blank line, body
git commit -v # show diff in the editor to help write the message
git commit -a -m "..." # stage all TRACKED modifications + commit in one step
git push # push current branch to its upstream
git pull --rebase # fetch + rebase (linear history) — safer than merge
git push -u origin feature-x # -u sets upstream so future `git push` just works
git log # full history, reverse chronological
git log --oneline -20 # compact, last 20 commits
git log --oneline --graph --decorate --all # branch-aware visual
git log --grep="bugfix" # search commit MESSAGES
git log -S "functionName" # search commit DIFFS (added/removed text — "pickaxe")
git log -p path/to/file # history + patches for a specific file
git log --stat # summary of files changed per commit
git diff # working dir vs staged
git diff --staged # staged vs last commit
git diff HEAD # working dir vs last commit (both combined)
git diff main..feature-x # differences between two branches
git show <sha> # full patch for a commit
git show <sha>:path/to/file # contents of a file at a past commit
git show <sha> --stat --name-only # files changed in a commit
# extract last committed version to a temp file for side-by-side compare
git show HEAD:path/to/file > /tmp/file.old
# diff against working copy, or use your editor's compare feature
diff /tmp/file.old path/to/file
git branch # list local branches (* = current)
git branch -a # include remote branches
git branch -v # show last commit on each
git branch --show-current # just the current branch name
git switch main # switch to an existing branch
git switch -c feature-x # create a new branch and switch to it
git switch -c feature-x <sha> # create branch starting at a specific commit
git switch - # switch back to previous branch
git merge feature-x # merge feature-x INTO current branch
git branch -d feature-x # delete a fully-merged branch
git branch -D feature-x # force-delete (even if unmerged) — use with care
git branch -m old-name new-name # rename a branch
Old pattern:
git checkout -b feature-x. Still works —git switch -c feature-xis the modern equivalent and doesn't overloadcheckoutwith file-restore semantics.
Git has a command for every flavor of "undo"; the right one depends on what you want to undo and where that thing lives (working dir, staged, committed, pushed).
git restore path/to/file # discard uncommitted changes to a file
git restore . # discard ALL uncommitted changes — destructive
git restore --staged path/to/file # unstage (keep changes in working dir)
git restore --staged . # unstage everything
git commit --amend # add more changes to, or reword, the last commit
git commit --amend --no-edit # amend without opening the editor (same message)
git reset --soft HEAD~ # undo last commit; keep changes staged
git reset --mixed HEAD~ # undo last commit; keep changes in working dir (default)
git reset --hard HEAD~ # undo last commit AND discard its changes — destructive
git reset HEAD~with no flag is--mixed. The destructive one is--hard.
revert, not reset)git revert <sha> # make a NEW commit that reverses <sha> — safe for shared history
git revert HEAD # revert the most recent commit
Rule of thumb:
resetrewrites history (safe only for commits nobody else has),revertappends new history (safe for anything, including pushed commits).
git reflog # every HEAD position over the last ~90 days
git reset --hard HEAD@{2} # jump back to where HEAD was 2 moves ago
git checkout HEAD@{2} -- path # restore a single file from a past HEAD state
Reflog is the safety net when you've run reset --hard, deleted a branch, or otherwise "lost" commits. As long as the commits existed in the last ~90 days on this machine, they're still in reflog.
| Situation | Fix |
|---|---|
| Committed to wrong branch | git reset --soft HEAD~ → git switch target-branch → git commit |
| Forgot to add a file to the last commit | git add forgotten-file && git commit --amend --no-edit |
| Need to change the last commit message | git commit --amend |
Accidentally ran reset --hard |
git reflog → git reset --hard HEAD@{N} |
| Deleted a branch that had unmerged work | git reflog → find the sha → git switch -c recovered-branch <sha> |
| Need to undo a commit already pushed | git revert <sha> && git push |
Never
--amendorreseta commit that's already been pushed and pulled by someone else — it rewrites history. Force-pushing over other people's work loses their commits.
Quick way to set aside uncommitted changes so you can switch context (pull, switch branches, etc.) without committing half-work.
git stash push -m "wip on login form" # save changes, clean working dir
git stash push -u -m "..." # include untracked files too (-u)
git stash push -k -m "..." # keep staged changes in place (-k / --keep-index)
git stash list # see all stashes
git stash show -p stash@{0} # preview a stash's contents
git stash pop # apply most recent stash + remove from list
git stash apply stash@{1} # apply a specific stash, KEEP it in the list
git stash drop stash@{0} # delete a stash without applying
git stash clear # delete all stashes — destructive
.gitignore — committed to the repo, visible to everyone. Use for generic, non-sensitive patterns: build output, dependency folders, OS junk, editor files..git/info/exclude — per-repo, local-only, never committed. Use for project-specific patterns you don't want in the shared .gitignore.~/.gitignore_global — applies across all repos, lives outside any project. Use for sensitive or workflow-specific patterns you never want leaking into any .gitignore.# one-time setup for global gitignore
git config --global core.excludesFile ~/.gitignore_global
.gitignoreis tracked, so any patterns and comments in it are visible in a public repo. Avoid putting private filenames or paths in.gitignore— use.git/info/excludeor the global gitignore instead.
# Build output
build/
dist/
target/
# Dependencies
node_modules/
.venv/
venv/
# Python
__pycache__/
*.pyc
# OS junk
.DS_Store
Thumbs.db
# Editors
*.swp
*.swo
*~
.vscode/
.idea/
# Secrets (still keep them out of repos — this is a defense in depth)
.env
.env.*
*.pem
.gitignore before the first commit# 1. create .gitignore, then init
git init
# 2. dry-run — shows what WOULD be staged (respects .gitignore)
git add -A --dry-run
# 3. confirm ignored files are actually ignored
git status --short --ignored
# 4. if the list looks right, commit for real
git add -A
git commit -m "initial commit"
Tags are named pointers to specific commits — typically used for releases.
git tag # list all tags
git tag -l 'v1.*' # list tags matching a pattern
git tag -n # list tags with their messages
git tag v1.0 # lightweight tag on current commit
git tag -a v1.0 -m "version 1.0" # annotated tag (has metadata — preferred for releases)
git tag -a v1.0 <sha> # tag an earlier commit
git show v1.0 # show a tag's details + the commit it points to
git push origin v1.0 # push a single tag (tags don't push by default)
git push origin --tags # push all tags
git tag -d v1.0 # delete a local tag
git push origin --delete v1.0 # delete a remote tag
Rebasing takes the commits from one branch and re-applies them on top of another, producing a linear history instead of a merge commit.
git switch feature-x
git rebase main # replay feature-x's commits on top of main
Use when: you want a clean, linear history on a feature branch before merging.
Don't rebase commits that are already pushed and that others have pulled — same reason as reset.
git rebase -i HEAD~5 # edit the last 5 commits
In the editor, change pick to:
squash (or s) — fold this commit into the previous onereword (or r) — keep the commit, change its messagedrop — discard the commitedit — pause for amendinggit remote -v # list all remotes with URLs
git remote add origin git@github.com:yourname/repo.git
git remote set-url origin <new-url> # change an existing remote's URL
git remote rename origin upstream # rename a remote
git remote remove origin # delete a remote
git fetch origin # download refs, don't merge
git fetch --prune # also delete refs to branches removed on the remote
git pull # fetch + merge (or rebase, if configured)
git pull --rebase # fetch + rebase, regardless of config
SSH keys live in ~/.ssh/ and are global to your machine, not per-repo.
# generate a key (one time, if you don't already have one)
ssh-keygen -t ed25519 -C "you@example.com"
# copy the PUBLIC key and paste into github.com → settings → SSH and GPG keys
cat ~/.ssh/id_ed25519.pub
# verify
ssh -T git@github.com
# expected: Hi yourname! You've successfully authenticated...
Once set up, cloning via an SSH URL "just works" — no tokens, no prompts. If you cloned with HTTPS and want to switch:
git remote set-url origin git@github.com:yourname/repo.git
# install (Debian/Ubuntu)
sudo apt install gh
# authenticate — this handles auth for git and gh without a PAT
gh auth login
# repos
gh repo create my-repo --public # create + init remote
gh repo create my-repo --private --source=. --push # create from current dir + push
gh repo list yourname --limit 100
gh repo clone yourname/some-repo
gh repo delete yourname/some-repo
# PRs
gh pr create
gh pr list
gh pr view 42
gh pr checkout 42
# issues
gh issue create
gh issue list
gh auth loginhas largely replaced manual PAT creation for CLI use. If you do need a PAT, GitHub now recommends fine-grained tokens (Settings → Developer settings → Personal access tokens → Fine-grained tokens) over classic tokens.
# local
git init
git branch -M main # ensure default branch is main
# create .gitignore, check what WOULD be staged before the first add
git status --short --ignored
git add -A --dry-run
# first commit
git add -A
git commit -m "initial commit"
# create remote + push (pick one)
gh repo create my-repo --public --source=. --push # with gh CLI
# OR, manually:
git remote add origin git@github.com:yourname/my-repo.git
git push -u origin main
# afterward: add a repo description and topics on github.com for discoverability
git clone git@github.com:yourname/template-repo.git my-project
cd my-project
git remote set-url origin git@github.com:yourname/my-project.git
git push -u origin main
# optional: keep a reference to the template for future syncs
git remote add upstream git@github.com:yourname/template-repo.git
git fetch upstream
git merge upstream/main # pull template updates (handle conflicts if needed)
git rebase -i HEAD~5
# change 'pick' to 'squash' on all but the first line
git push --force-with-lease # safer than --force: refuses if remote changed
git switch --orphan new-main # new branch with no parents
git add -A
git commit -m "initial commit"
git branch -D main # delete the old main
git branch -m main # rename new-main to main
git push -f origin main # force-push (you're replacing history)
Use sparingly. This nukes history for every collaborator. Only sane on personal repos or projects you fully control. Prefer
git filter-repoif you need to remove specific sensitive content while keeping the rest of history.
--force-with-lease over --forceWhen you do need to force-push, prefer --force-with-lease — it refuses if the remote has commits you don't know about, preventing you from silently clobbering someone else's work:
git push --force-with-lease
Useful when doing spring cleaning — scan a directory tree for repos with a GitHub remote:
find ~ -name .git -type d 2>/dev/null | while read d; do
git -C "${d%/.git}" remote -v 2>/dev/null | grep -q github.com && echo "${d%/.git}"
done
How it works: find locates .git directories; ${d%/.git} strips the suffix to get the repo root; git remote -v lists remotes; grep -q github.com checks for a GitHub remote; matching paths are printed.