Configuration & Initialization
Global & Local Config
Git stores config at three levels: system (/etc/gitconfig), global (~/.gitconfig for the user), and local (.git/config per repo). Lower levels override higher ones. Always set user.name and user.email before committing, otherwise commits will use an unhelpful default identity. Setting init.defaultBranch to main avoids the deprecated master default and aligns with modern conventions.
# set user identity (required for commits)
git config --global user.name "Alice Lee"
git config --global user.email "[email protected]"
# set default editor and branch name
git config --global core.editor "code --wait"
git config --global init.defaultBranch main
# view all settings and their origin
git config --list --show-origin
# edit config files directly
git config --global --edit # ~/.gitconfig
git config --edit # repo .git/configCreate & Clone Repositories
git init creates an empty repository by adding a hidden .git directory that stores all version data. git clone copies a remote repo including its full history. Use --depth 1 for a shallow clone when you only need the latest snapshot (e.g. CI builds) — it dramatically reduces download size. --single-branch avoids fetching unrelated branches.
# create a new repo from scratch in current dir
mkdir my-app && cd my-app
git init
# clone a remote repository
git clone https://github.com/user/repo.git
# clone into a specific folder name
git clone https://github.com/user/repo.git my-folder
# clone a single branch (saves bandwidth)
git clone -b dev --single-branch https://github.com/user/repo.git
# shallow clone: only the latest commit
git clone --depth 1 https://github.com/user/repo.gitAliases & Shortcuts
Aliases let you define shorter names for frequently used commands or command sequences. They are stored in the [alias] section of your gitconfig. An alias starting with ! runs as a shell command, enabling complex workflows. The lg alias above produces a compact visual history graph that is extremely useful for understanding branch structure.
# create shortcuts for common commands
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.ci commit
git config --global alias.st status
git config --global alias.lg "log --oneline --graph --all"
# now use them
git co main
git lg
# alias that runs an external command (starts with !)
git config --global alias.unstage "reset HEAD --"
git unstage file.txtHelp & Documentation
Git ships with comprehensive built-in documentation. git help <command> opens the man page in your pager. The -h flag gives a quick one-screen summary of options. The guides (git help -g) include a tutorial, a glossary of terms, and an everyday workflow reference — excellent for beginners learning the terminology.
# open the full manual for a command
git help commit
git commit --help
# show a concise synopsis
git commit -h
# list all git commands
git help -a
# list all guides (tutorial, glossary, etc.)
git help -g
git help glossary.gitignore Patterns
.gitignore tells Git which files to exclude from version control — essential for build artifacts, dependencies, and secrets. Patterns use glob syntax; a trailing slash matches directories. Prefixing with ! negates a pattern, forcing a file to be tracked. .gitignore itself should be committed so the team shares ignore rules. Already-tracked files are not affected by new ignore patterns — remove them with git rm --cached first.
# .gitignore file — patterns for files to skip
node_modules/
*.log
.env
.env.local
dist/
build/
# but track this specific file even if it matches
!important.log
# ignore all .txt files in build/ but not subdirs
build/*.txt
# ignore everything in a folder except one file
secrets/*
!secrets/template.envStaging & Committing
Status & Staging
Git uses a two-step model: changes go to the staging area (index) before being committed. git status shows what is modified, staged, and untracked. git add -p lets you stage individual hunks of a patch — invaluable for splitting a messy working tree into focused, logical commits. Use git restore --staged (the modern command) to unstage without losing your edits.
# see current state of working tree
git status
git status -s # short format
git status -sb # short + branch info
# stage changes
git add file.txt # one file
git add src/ # a directory
git add . # all changes in repo
git add -p # stage interactively by hunk
# unstage a file (keep changes in working dir)
git restore --staged file.txt
git reset HEAD file.txt # older syntaxCommitting Changes
A commit records a snapshot of the staged changes. Write messages in the imperative mood ('add feature' not 'added feature'). The -a flag only stages already-tracked files — new files still need git add. --amend rewrites the last commit; use it to fix a typo or add a forgotten file, but never amend commits you have already pushed to a shared branch.
# commit staged changes with a message
git commit -m "feat: add login form"
# multi-line commit message
git commit -m "feat: add login form" -m "Closes #42"
# stage tracked files AND commit in one step
git commit -am "fix: correct typo"
# amend the previous commit (keep message)
git commit --amend --no-edit
# amend and edit the message
git commit --amend -m "feat: add login form (v2)"Commit Message Conventions
Conventional Commits is a widely adopted specification for structured commit messages. The type prefix (feat, fix, docs, etc.) enables automated changelog generation and semantic versioning. The ! marker indicates a breaking change. A blank line separates the subject (<=50 chars) from the body, and another blank line separates footers like 'Closes #123' which auto-close issues on GitHub.
# Conventional Commits format
feat: add user registration endpoint
fix: resolve crash on empty cart
docs: update API reference
style: format login component
refactor: extract validation logic
test: add unit tests for parser
chore: upgrade dependencies
# with scope and breaking change marker
feat(api): add pagination to list endpoint
fix!: change default port (BREAKING CHANGE)
# with body and footer
feat: add dark mode
Implement theme toggle using CSS variables.
Closes #128Viewing History with log
git log is the primary tool for exploring history. --oneline --graph --all is the most useful combination for understanding branch structure at a glance. You can filter by author, date range, or message content with --grep. --stat shows which files changed and how many lines, while --patch shows the full diff of every commit.
# full history with details
git log
git log --oneline # one line per commit
git log --oneline --graph --all # visual branch graph
git log -n 5 # last 5 commits
# filter by author, date, or message
git log --author="Alice"
git log --since="2 weeks ago"
git log --grep="fix"
# show files changed in each commit
git log --stat
git log --patch # full diffsDiff & Show
git diff compares snapshots — with no arguments it shows unstaged changes against the index. --staged compares the index against HEAD. git show displays a single commit's metadata and patch. The HEAD:path syntax lets you view any file's contents at any commit without checking it out, which is handy for recovering old versions or inspecting history.
# compare working tree, index, and commits
git diff # unstaged changes
git diff --staged # staged but uncommitted
git diff HEAD # all changes vs last commit
git diff main feature # compare two branches
git diff abc1234 def5678 # compare two commits
# inspect a single commit
git show HEAD # latest commit diff + metadata
git show abc1234
git show HEAD:file.txt # view a file at a given commitBranching & Merging
Creating & Switching Branches
Branches in Git are lightweight pointers to a commit — creating one is nearly instant. git switch (Git 2.23+) is the modern, safer alternative to checkout for changing branches, reserving checkout for restoring files. -d refuses to delete an unmerged branch (protecting your work); -D forces deletion. Always delete branches after merging to keep the branch list clean.
# list branches
git branch # local branches
git branch -a # local + remote
git branch -vv # with tracking info
# create and switch
git branch feature # create only
git checkout feature # switch to it
git checkout -b feature # create + switch (classic)
git switch -c feature # create + switch (modern)
# delete branches
git branch -d feature # safe delete (merged only)
git branch -D feature # force deleteMerging Branches
A fast-forward merge simply moves the branch pointer forward when the target has no new commits — producing linear history. --no-ff forces a merge commit, preserving the fact that a branch existed (useful for feature tracking). --squash combines all branch commits into a single staged change you then commit once — great for tidying noisy feature history before integrating into main.
# merge feature into main
git checkout main
git merge feature
# fast-forward merge (default when possible)
# main just moves forward to feature's commit
# create a merge commit (preserves branch history)
git merge --no-ff feature -m "Merge feature branch"
# squash all feature commits into one
git merge --squash feature
git commit -m "feat: add feature (squashed)"
# abort a merge with conflicts
git merge --abortResolving Merge Conflicts
Conflicts occur when the same lines are changed differently on two branches. Git inserts conflict markers (<<<<<<<, =======, >>>>>>>) showing both sides. Resolve by editing the file to the desired final state, then git add to mark it resolved. Commit to complete the merge. git mergetool launches a visual diff tool. If you are overwhelmed, git merge --abort returns to the pre-merge state.
# a conflict produces markers in the file
# <<<<<<< HEAD
# my changes (current branch)
# =======
# their changes (incoming branch)
# >>>>>>> feature
# edit the file to resolve, then:
git add resolved-file.txt
git commit # finalize the merge
# use a merge tool
git mergetool
# see which files conflict
git status
# abandon the merge
git merge --abortRebasing
Rebase replays your branch's commits on top of another branch, producing linear history without merge commits. Interactive rebase (-i) is a power tool: squash merges commits, reword edits messages, drop removes commits, edit pauses to let you modify a commit. NEVER rebase commits that have been pushed and shared — it rewrites history and breaks teammates' repositories. Use rebase only on your own local branches.
# rebase feature onto main (replay feature commits)
git checkout feature
git rebase main
# interactive rebase — rewrite last 5 commits
git rebase -i HEAD~5
# options: pick, reword, squash, fixup, drop, edit
# continue, skip, or abort during a rebase
git rebase --continue
git rebase --skip
git rebase --abort
# rebase while pulling
git pull --rebase origin mainCherry-Picking & Reflog
Cherry-pick applies an individual commit from another branch onto your current branch — useful for backporting a bugfix without merging the whole branch. The reflog is a local log of every HEAD movement (commits, checkouts, resets) kept for ~90 days. It is your safety net: even after a destructive reset, you can find the old commit hash in the reflog and recover it. Reflog data is local only and never pushed.
# apply a specific commit onto current branch
git cherry-pick abc1234
git cherry-pick abc1234 def5678 # multiple commits
git cherry-pick abc1234..def5678 # range (exclusive start)
# reflog: record of where HEAD has been
git reflog
git reflog show feature
# recover a 'lost' commit
git reset --hard HEAD@{2}
# view a branch's history of moves
git reflog show mainRemote Repositories
Managing Remotes
A remote is a named reference to another repository, typically origin for your fork and upstream for the original project. git remote -v shows the fetch and push URLs. Forks on GitHub use the upstream remote to sync with the original: fetch from upstream, merge or rebase, then push to origin. set-url is handy for switching between HTTPS and SSH authentication.
# list configured remotes
git remote -v
git remote show origin
# add a remote
git remote add origin https://github.com/user/repo.git
# add an upstream remote (for forks)
git remote add upstream https://github.com/original/repo.git
# rename or remove
git remote rename origin upstream
git remote remove origin
# change a remote URL (e.g. HTTPS to SSH)
git remote set-url origin [email protected]:user/repo.gitFetch, Pull & Push
fetch downloads remote data but leaves your working tree untouched — safe to inspect before merging. pull = fetch + merge (or rebase with --rebase). The first push of a new branch needs -u to set up tracking so future git push/pull work without arguments. --force-with-lease is the safe alternative to --force: it only overwrites the remote if no one else has pushed in the meantime, preventing accidental clobbering of teammates' work.
# download remote changes without merging
git fetch origin
git fetch --all --prune # all remotes, drop deleted branches
# fetch + merge in one step
git pull
git pull --rebase # rebase instead of merge
# push local commits to remote
git push origin main
git push -u origin feature # push + set upstream tracking
git push # subsequent pushes (uses tracking)
# force push (rewrites remote history — DANGER)
git push --force-with-lease # safer than --forceTracking & Syncing Branches
Tracking branches link a local branch to a remote one so git pull and git push know where to fetch/push without specifying. -u (short for --set-upstream-to) sets this on the first push. When teammates delete remote branches, your local remote-tracking refs become stale — git remote prune origin cleans them up. git fetch --prune does this automatically during fetch.
# set upstream for current branch
git branch -u origin/main
git branch --set-upstream-to=origin/main
# create a local branch tracking a remote one
git checkout -b feature origin/feature
git switch feature # auto-detects tracking branch
# delete a remote branch
git push origin --delete feature
# list all remote-tracking branches
git branch -r
git remote prune origin # remove stale remote refsPull Requests Workflow
The standard GitHub flow: create a feature branch, push it, open a Pull Request for review, and delete the branch after merge. For forks, the upstream remote lets you pull changes from the original repository. Keeping your fork's main in sync with upstream regularly prevents large, painful merges later. Many teams enable 'auto-delete branch on merge' to keep the branch list tidy.
# typical feature workflow
git checkout -b feature/login
# ... make changes, commit ...
git push -u origin feature/login
# create PR on GitHub, then after merge:
git checkout main
git pull origin main
git branch -d feature/login
# sync a fork with its upstream
git fetch upstream
git checkout main
git merge upstream/main
git push origin mainBare Repositories & Mirrors
A bare repository has no working tree — it only stores the .git data. Bare repos are used on servers (like self-hosted Git) as the central remote that multiple people push to and pull from. --mirror clones everything including remote-tracking refs and is used for backups or migrating a repo between hosts. --all pushes all branches; --tags pushes all tags.
# create a bare repo (no working tree) — for servers
git init --bare project.git
# mirror clone (full copy including all refs)
git clone --mirror https://github.com/user/repo.git
# push all branches and tags
git push --all
git push --tags
# push a specific branch to a specific remote
git push origin local-name:remote-nameUndoing Changes
Reset: Soft, Mixed, Hard
reset moves the current branch pointer. --soft keeps your changes staged (only the commit is undone) — ideal for re-committing. --mixed (default) unstages but keeps working tree changes. --hard discards everything permanently — your only recovery is the reflog. Never use --hard on commits you have pushed, as it rewrites shared history and causes divergence.
# reset moves HEAD and optionally the index/worktree
git reset --soft HEAD~1 # undo commit, keep staged
git reset --mixed HEAD~1 # undo commit + unstage (DEFAULT)
git reset --hard HEAD~1 # undo commit + discard changes
# reset to a specific commit
git reset --hard abc1234
# reset a single file to HEAD
git reset HEAD file.txt # unstage
git checkout -- file.txt # discard working changesRevert (Safe Undo)
Unlike reset (which rewrites history), revert adds a new commit that inverses the target commit — safe for shared branches because history is preserved. This is the correct way to undo a change that has already been pushed. Reverting a merge commit requires -m 1 to specify which parent line to keep (1 = the branch you merged into).
# revert creates a NEW commit that undoes another
git revert abc1234
git revert HEAD # undo last commit
# revert a range
git revert HEAD~3..HEAD
# revert without committing (stage the inverse)
git revert --no-commit abc1234
# revert a merge commit
git revert -m 1 abc1234 # -m 1 = keep main branch parentRestore & Clean
git restore (Git 2.23+) is the modern, focused command for working-tree operations, separating concerns from checkout. --staged unstages without touching working changes. git clean removes untracked files — always run with -n (dry run) first to preview what will be deleted. -x is aggressive: it also deletes gitignored files, which is useful for a clean build but can wipe secrets or build outputs.
# discard unstaged changes in a file
git restore file.txt
git checkout -- file.txt # older syntax
# unstage a file (keep working changes)
git restore --staged file.txt
# restore a file from a specific commit
git restore --source=abc1234 file.txt
# remove untracked files
git clean -n # dry run (preview)
git clean -fd # remove untracked files + dirs
git clean -fdx # also remove ignored filesAmend & Fixup
--amend rewrites the last commit — handy for fixing a typo or adding a forgotten file, but never amend pushed commits. The fixup workflow is elegant: create fixup commits as you notice small issues, then git rebase -i --autosquash automatically reorders and squashes them into their target commits. This keeps history clean while letting you commit small fixes incrementally.
# amend the last commit (message + content)
git add forgotten-file.txt
git commit --amend --no-edit
# change only the commit message
git commit --amend -m "better message"
# create a fixup commit (for later autosquash)
git commit --fixup=abc1234
# squash fixups during interactive rebase
git rebase -i --autosquash abc1234~1Reflog Recovery
The reflog is your safety net. It records every commit, checkout, reset, and rebase — even operations that 'destroy' commits. Entries persist for ~90 days. If you accidentally reset --hard or delete a branch, find the orphaned commit hash in the reflog and reset to it or create a new branch pointing at it. The reflog is local only, so this works even offline.
# reflog records every HEAD movement
git reflog
# abc1234 HEAD@{0}: reset: moving to abc1234
# def5678 HEAD@{1}: commit: feat: add x
# ghi9012 HEAD@{2}: checkout: moving to feature
# recover a 'lost' commit
git reset --hard def5678
# recover a deleted branch
git branch recovered-feature def5678
# reflog for a specific branch
git reflog show featureStashing & Workflows
Stashing Changes
Stash shelves uncommitted changes so you can switch branches or pull updates with a clean tree. apply keeps the stash in the list (useful if you want to apply it to multiple branches); pop applies and removes it. Stashes are a LIFO stack referenced by stash@{N}. Use clear with caution — it permanently discards all stashes.
# save uncommitted changes (reverts working tree to HEAD)
git stash
git stash push -m "wip: login form" # with a message
# list, show, apply
git stash list
git stash show -p stash@{0} # show diff
git stash apply # apply latest, keep stash
git stash pop # apply latest, drop stash
# apply a specific stash
git stash apply stash@{2}
# drop a stash
git stash drop stash@{0}
git stash clear # drop ALL stashesPartial & Selective Stash
These flags give fine-grained control over what gets stashed. --keep-index is useful when you have staged a logical commit but want to test only those changes — stash the rest, run tests, then pop. -u includes untracked files (otherwise they are left in your working tree). -p lets you pick specific hunks, mirroring git add -p.
# stash only staged changes
git stash --staged
# stash only unstaged changes (keep staged)
git stash --keep-index
# stash interactively by hunk
git stash -p
# stash including untracked files
git stash -u
git stash --include-untracked
# stash everything (even ignored)
git stash -aStash Branches & Create
git stash branch creates a new branch at the stash's original parent commit and applies the stash there — perfect when a stash no longer applies cleanly to the current branch due to conflicts. You can also export a stash as a patch file with show -p for archival or sharing. Stashes are local and never pushed, so they should not be used for long-term storage.
# create a branch from a stash (great for conflicts)
git stash branch feature/wip stash@{0}
# create a commit from a stash without applying
git stash store -m "saved wip" stash@{0}
# apply a stash to a different branch
git checkout other-branch
git stash apply stash@{0}
# inspect stash contents
git stash show stash@{0} --stat
git stash show -p stash@{0} > patch.diffGit Workflow Models
GitHub Flow is the simplest: one main branch, feature branches with PRs, deploy on merge — ideal for continuous deployment. Git Flow (Vincent Driessen's model) adds develop, release, and hotfix branches for structured release management — suited for versioned products. Trunk-Based Development uses very short-lived branches and is favored by high-performance DevOps teams for maximum integration speed.
# GitHub Flow (simple, popular)
# main is always deployable; feature branches + PRs
git checkout -b feature/x
git push -u origin feature/x
# open PR, review, merge to main, deploy
# Git Flow (structured, with release branches)
# main: production releases
# develop: integration branch
# feature/*: features off develop
# release/*: release prep
# hotfix/*: urgent fixes off main
# Trunk-Based Development
# short-lived branches (1-2 days), frequent rebase on mainSubmodules
Submodules embed one Git repository inside another — useful for sharing a library across projects while keeping it versioned independently. The parent repo stores a pointer to a specific commit of the submodule. Clones do not fetch submodule contents by default; --recurse-submodules does it in one step. Submodules can be fiddly; for simpler dependency management, consider Git subtrees or a package manager.
# add another repo as a submodule
git submodule add https://github.com/user/lib.git libs/lib
# clone a repo with its submodules
git clone --recurse-submodules https://github.com/user/repo.git
# initialize submodules in an existing clone
git submodule update --init --recursive
# update submodules to their latest remote commit
git submodule update --remote
# record a submodule pointer change
git add libs/lib
git commit -m "chore: bump lib submodule"Inspection & Debugging
Blame & Annotate
git blame (also called annotate) shows the commit and author for every line of a file — essential for understanding why code looks the way it does. -L restricts to a line range, useful for large files. -w ignores pure whitespace changes, and -C detects code moved or copied from another file, giving you a more accurate history of where lines truly originated.
# show who last changed each line
git blame file.txt
git blame -L 10,20 file.txt # only lines 10-20
git blame -e file.txt # show author email
git blame -w file.txt # ignore whitespace changes
git blame -C file.txt # detect moved lines across files
# GUI annotation in some editors
git gui blame file.txtBisect (Binary Search Bugs)
bisect performs a binary search through history to pinpoint the exact commit that introduced a bug. You mark a known-good and known-bad commit; Git checks out the midpoint, you test, then mark good or bad, halving the range each time. With a script, the whole process is fully automated — a massive time-saver for regressions in large histories.
# find the commit that introduced a bug
git bisect start
git bisect bad # current commit is broken
git bisect good v1.0.0 # v1.0.0 was working
# Git checks out a midpoint; test it, then:
git bisect good # or git bisect bad
# automate with a script that exits non-zero on bug
git bisect start HEAD v1.0.0 -- npm test
# finish and return to original branch
git bisect reset
# view the bisect log
git bisect logSearching Code & History
git grep searches tracked files in the working tree — faster than grep -r because it uses the index. The -S 'pickaxe' option finds commits that added or removed a specific string, invaluable for tracing when a function or bug was introduced. -G is similar but matches a regex anywhere in the diff. Combined with --author and --since filters, you can pinpoint any change in history.
# search working tree for a string
git grep "TODO"
git grep -n "TODO" -- "*.js"
git grep -i "error" src/
# search across all commits (history)
git log -S "functionName" --oneline # pickaxe: when added/removed
git log -G "functionName.*\(" --oneline # regex match in diffs
# search commit messages
git log --grep="fix.*login" --oneline
git log --author="Alice" --since="1 week ago"FSCK & Dangling Objects
git fsck (file system check) verifies the integrity of the object database and can find dangling commits — useful for recovery when the reflog is insufficient. git gc reorganizes and compresses objects to save disk space; --prune=now removes unreachable objects immediately. Git auto-gcs periodically, but running it manually can shrink a large repo. --aggressive recomputes deltas for maximum compression.
# check repository integrity
git fsck --full
git fsck --unreachable
# find dangling (unreachable) commits & blobs
git fsck --lost-found
# garbage collect and prune old objects
git gc
git gc --prune=now
git gc --aggressive
# count objects and disk usage
git count-objects -vArchive & Bundle
git archive exports a clean snapshot of a commit without the .git directory — ideal for distributing releases or sending source to someone who does not need history. git bundle packages a repo (or a range of commits) into a single file that can be cloned or fetched from — perfect for transferring repos across air-gapped networks or via email when you cannot use a remote server.
# export a snapshot as a tar/zip (no .git history)
git archive --format=zip --output=app.zip HEAD
git archive --format=tar HEAD | gzip > app.tar.gz
git archive -o release.zip v1.0.0
# bundle a repo (with history) into a single file
git bundle create repo.bundle --all
git bundle create patch.bundle main..feature
# clone from a bundle (offline transfer)
git clone repo.bundle new-repo
git fetch patch.bundle featureAdvanced Techniques
Hooks
Hooks are scripts that run automatically at specific points. Client-side hooks (pre-commit, commit-msg, pre-push) enforce local policy like linting or tests. Server-side hooks (pre-receive, post-receive) run on the remote and can enforce branch protection or trigger CI/CD. The .git/hooks directory contains sample scripts ending in .sample — rename to activate. Tools like Husky or pre-commit framework manage hooks in the repo itself for team consistency.
# client-side hooks live in .git/hooks/
# pre-commit runs before a commit is created
# commit-msg validates the commit message
# pre-push runs before pushing to remote
# sample pre-commit hook (.git/hooks/pre-commit)
#!/bin/sh
npm run lint || exit 1
npm test || exit 1
# server-side hooks (on the remote)
# pre-receive, update, post-receive
# make a hook executable
chmod +x .git/hooks/pre-commitWorktrees
Worktrees let you have multiple working directories for a single repository, each on a different branch — without cloning. This is invaluable when you need to work on a hotfix while keeping your feature branch's working tree intact, or run a long build on one branch while editing another. All worktrees share the same .git object database, so disk usage is minimal.
# create a second working tree of the same repo
git worktree add ../app-feature feature/x
# list worktrees
git worktree list
# create a worktree with a new branch
git worktree add -b hotfix/y ../app-hotfix main
# remove a worktree
git worktree remove ../app-feature
# prune stale worktree metadata
git worktree pruneFilter & Rewrite History
Rewriting history is necessary when secrets or large files were committed. git filter-repo is the modern, fast replacement for git filter-branch. BFG Repo-Cleaner is a user-friendly alternative for removing large files or password patterns. After rewriting, you must force-push and notify all collaborators to re-clone — old commits remain in their reflogs until expired. Always rotate any leaked secrets immediately.
# remove a file from ALL of history (sensitive data)
git filter-repo --path secrets.env --invert-paths
# (install git-filter-repo; the old filter-branch is deprecated)
# rewrite author info across all commits
git filter-repo --mailmap mailmap.txt
# split a subdirectory into its own repo
git filter-repo --subdirectory-filter libs/lib
# the simpler BFG Repo-Cleaner (Java tool)
bfg --delete-files *.env
bfg --replace-text passwords.txtSparse Checkout & Partial Clone
Partial clone (--filter) fetches commits and trees but downloads blobs (file contents) lazily as you access them — dramatically speeding up clones of huge repos. Sparse checkout limits your working tree to specific directories, so you only see the parts you work on. Together they make enormous monorepos manageable: clone is fast, and your working tree stays small.
# partial clone: download history on demand
git clone --filter=blob:none https://github.com/user/huge-repo.git
# sparse checkout: only certain directories
git clone --no-checkout https://github.com/user/repo.git
cd repo
git sparse-checkout init --cone
git sparse-checkout set src docs
git checkout main
# add a directory to the sparse set later
git sparse-checkout add tests
# disable sparse checkout (fetch everything)
git sparse-checkout disableReflog, Refspec & Notes
Refspecs give explicit control over how refs are mapped during fetch/push — useful for unusual workflows like pushing a local feature branch to remote main. git notes attaches metadata to a commit without rewriting it, which is handy for review comments or CI links. Notes are stored in a separate ref (refs/notes/commits) and must be explicitly pushed and fetched.
# refspec: explicit fetch/push mapping
git fetch origin "refs/heads/*:refs/remotes/origin/*"
git push origin "refs/heads/feature:refs/heads/main"
# add a note to a commit (without changing it)
git notes add -m "Reviewed by Alice" abc1234
git notes show abc1234
git log --show-notes
# configure where notes are stored
git config --global notes.displayRef "refs/notes/*"
# push notes to a remote
git push origin refs/notes/commitsBest Practices & Tips
Commit Hygiene
Good commits are small, atomic, and self-contained: one logical change per commit. This makes code review easier, bisecting faster, and reverts surgical. Use git add -p to stage only the hunks relevant to a single concern. Imperative messages ('add' not 'added') read as instructions to the codebase. Rebasing feature branches onto the latest main before merging keeps history linear and intelligible.
# commit small, focused, logical units
git add -p # stage only relevant hunks
git commit -m "fix: handle null user in login"
# write clear, imperative messages
# GOOD: "feat: add password reset endpoint"
# BAD: "stuff", "fix", "WIP", "asdf"
# one concern per commit
git add login.ts && git commit -m "feat: add login"
git add styles.css && git commit -m "style: format login"
# rebase before merging to keep history clean
git fetch && git rebase origin/mainBranch Protection & Code Review
Branch protection rules prevent force-pushes, require PR reviews, and gate merges on passing CI — essential for team safety. Requiring linear history forces rebase or squash merges, keeping history readable. Signed commits (GPG or SSH) prove authorship and prevent impersonation. These settings are configured on the hosting platform (GitHub/GitLab), not in Git itself, but they enforce good Git hygiene organization-wide.
# on GitHub/GitLab: protect the main branch
# - require pull request reviews
# - require status checks (CI) to pass
# - require linear history (no merge commits)
# - dismiss stale reviews on push
# - require signed commits
# enforce via config (GitHub CLI)
gh api repos/:owner/:repo/branches/main/protection \
-X PUT -f required_status_checks[strict]=true
# sign commits and verify on receive
git config --global commit.gpgsign true
git config --global gpg.format sshIgnoring & Secrets Management
Committed secrets are the most common Git security incident. Once pushed, assume the secret is compromised — rotate it immediately, even after removing it from history, because clones and forks retain it. Prevention is best: .gitignore secrets, use environment variables or a secret manager (Vault, AWS Secrets Manager), and install git-secrets or a pre-commit hook that scans for API keys and passwords before each commit.
# never commit secrets — use .gitignore
echo ".env" >> .gitignore
echo "*.pem" >> .gitignore
echo "secrets/" >> .gitignore
# if you accidentally committed a secret:
# 1. rotate/revoke the secret immediately
# 2. remove from history
git filter-repo --path .env --invert-paths
git push --force-with-lease
# use environment files or secret managers
echo "API_KEY=$API_KEY" > .env.local # gitignored
# load via dotenv in your app
# use git-secrets to scan pre-commit
git secrets --install
git secrets --register-awsPerformance Tips
Large repositories can be slow. fsmonitor and untrackedcache make git status much faster by caching filesystem state. Shallow and partial clones reduce initial download. Run git gc periodically to compact objects. --no-verify skips pre-commit and commit-msg hooks — useful for emergencies but should not become a habit, as it bypasses your team's quality checks.
# speed up status on large repos
git config --global core.fsmonitor true
git config --global core.untrackedcache true
# shallow clone for CI
git clone --depth 1 https://github.com/user/repo.git
# partial clone for huge monorepos
git clone --filter=blob:none https://github.com/user/monorepo.git
# periodic garbage collection
git gc --prune=now
# disable slow hooks temporarily
git commit --no-verifyCommon Pitfalls
Avoid these classic mistakes: never force-push to shared branches (--force-with-lease is the safe option); never rebase commits others may have based work on; never commit on a detached HEAD without creating a branch first. Use Git LFS for large binaries — they bloat history permanently otherwise. Configure line endings (autocrlf) per OS to avoid noisy whitespace-only diffs in cross-platform teams.
# PITFALL: force-pushing to shared branches
git push --force origin main # DON'T — use --force-with-lease
# PITFALL: rebasing pushed commits
# rebase only your local, unpushed branches
# PITFALL: committing on detached HEAD
git checkout v1.0.0
# changes here are not on a branch — create one:
git checkout -b fix/v1.0.1
# PITFALL: large binary files bloating history
# use Git LFS for binaries
git lfs install
git lfs track "*.psd"
git add .gitattributes
# PITFALL: CRLF/LF line endings across OS
git config --global core.autocrlf input # on Linux/Mac
git config --global core.autocrlf true # on WindowsGit Flow Workflow
Git Flow Branch Model
Git Flow is a strict branching model for release-based projects. main always holds production code; develop holds integration work. Features branch from develop and merge back. Releases branch from develop, stabilize, then merge to both main (with a tag) and develop. Hotfixes branch from main and merge to both main and develop. This model suits projects with scheduled releases (desktop apps, on-premise software). For continuous deployment, GitHub Flow (main + feature branches) is simpler. The git-flow CLI tool automates the branch dance.
# Git Flow uses long-lived branches:
# main → production-ready code
# develop → latest delivered development changes
# feature/* → new features (branched from develop)
# release/* → release preparation (branched from develop)
# hotfix/* → urgent production fixes (branched from main)
# Initialize git-flow (interactive setup)
git flow init
# Start a new feature
git flow feature start my-feature
# Creates feature/my-feature from develop
# Finish a feature (merges to develop, deletes branch)
git flow feature finish my-feature
# Publish a feature to remote
git flow feature publish my-feature
# Start a release
git flow release start 1.2.0
# Creates release/1.2.0 from develop
# Finish a release (merges to main AND develop, tags)
git flow release finish 1.2.0GitHub Flow (Simpler)
GitHub Flow is the simplest workflow: main is always deployable, feature branches are short-lived, and everything merges via Pull Requests. There's no develop branch or release branches — main is deployed continuously. This works well for web apps with continuous deployment. The key rule: never commit directly to main; always use a PR for review. Keep feature branches small and short-lived (days, not weeks). Delete branches after merging to keep the repo clean. This model prioritizes speed and simplicity over Git Flow's structured release management.
# GitHub Flow: only main + feature branches
# 1. Create branch from main
git checkout main
git pull origin main
git checkout -b feature/add-login
# 2. Commit changes
git add .
git commit -m "feat: add login page"
# 3. Push to remote
git push -u origin feature/add-login
# 4. Open Pull Request on GitHub
# 5. Review, discuss, get approval
# 6. Merge to main (via GitHub UI or CLI)
git checkout main
git pull origin main
git branch -d feature/add-login # delete local branch
# 7. Deploy from main (continuous deployment)Trunk-Based Development
Trunk-Based Development is the most extreme: developers commit directly to main (or very short-lived branches merged within 24 hours). This enables true continuous integration — everyone integrates constantly. Incomplete features use feature flags (deployed but hidden) rather than long-lived branches. This requires strong CI/CD, comprehensive tests, and feature flag infrastructure. Used by Google, Facebook, and Netflix. Benefits: no merge hell, fast feedback, small changes. Challenges: requires discipline, test coverage, and feature flag management. Best for experienced teams with robust CI/CD.
# Trunk-Based: everyone commits to main (trunk)
# Short-lived feature branches optional
# Direct commit to main (small teams)
git checkout main
git pull
# make changes
git add . && git commit -m "fix: typo in header"
git push
# With short-lived branches (larger teams)
git checkout -b quick-fix
git commit -m "fix: validation bug"
git push
# PR merged within 24 hours
# Feature flags enable incomplete code on main
# Code is deployed but hidden behind a flag
if (featureFlag.isEnabled("new-checkout")) {
showNewCheckout();
} else {
showOldCheckout();
}Fork & Pull Workflow
The Fork & Pull workflow is standard for open source. Contributors fork the repo (creating their own copy), push branches to their fork, and open PRs to the original (upstream) repo. The upstream remote lets you sync your fork with the original. Always create feature branches from an updated main. This workflow allows anyone to contribute without write access. Maintainers review PRs and merge. For keeping your fork in sync, fetch upstream and merge/rebase regularly. Some projects use a 'clone, branch, PR' model for internal contributors with direct push access to feature branches.
# For open-source projects (contributors don't have push access)
# 1. Fork the repo on GitHub (UI button)
# 2. Clone YOUR fork
git clone https://github.com/YOUR-USERNAME/project.git
cd project
# 3. Add upstream (original repo)
git remote add upstream https://github.com/ORIGINAL/project.git
# 4. Keep your fork updated
git fetch upstream
git checkout main
git merge upstream/main # or: git rebase upstream/main
git push origin main
# 5. Create feature branch
git checkout -b feature/my-contribution
# 6. Push to YOUR fork
git push origin feature/my-contribution
# 7. Open Pull Request from your fork to upstreamBranch Naming Conventions
Consistent branch naming improves clarity and enables automation. Common prefixes: feature, bugfix, hotfix, release, chore, docs, refactor, experiment. Including ticket numbers (PROJ-123) links branches to issues and enables auto-linking. Slashes create visual hierarchy in Git GUIs. Some teams enforce naming via Git hooks or CI checks. The convention should be documented in CONTRIBUTING.md. Keep names descriptive but concise. Avoid personal names (johns-branch) — describe the work, not the author. Consistent naming makes branch cleanup and history navigation much easier.
# Common naming patterns:
feature/add-user-authentication
feature/PROJ-123-user-profile
bugfix/fix-login-redirect
bugfix/PROJ-456-crash-on-startup
hotfix/security-patch-xss
release/v2.0.0
chore/update-dependencies
docs/api-documentation
refactor/extract-auth-module
experiment/new-algorithm
# Using ticket numbers (Jira, Linear, etc.)
git checkout -b feature/PROJ-123-oauth-login
# Auto-linking in commit messages
git commit -m "PROJ-123: implement OAuth login"
# GitHub auto-links PROJ-123 to the Jira ticket
# Branch naming with slashes creates hierarchy
# in Git GUI tools (GitHub, GitKraken, SourceTree)Rebase Deep Dive
Interactive Rebase
Interactive rebase (-i) is the most powerful history-editing tool. It lets you rewrite, reorder, combine, split, or delete commits before pushing. squash combines a commit into its parent (merging messages); fixup does the same but discards the commit message (clean up 'WIP' commits). edit pauses the rebase so you can amend the commit (add files, change content). reword lets you change only the message. drop removes a commit. Always rebase before pushing to keep history clean. Never rebase commits that others have already pulled — it rewrites shared history.
# Rebase last 5 commits interactively
git rebase -i HEAD~5
# Opens editor with:
# pick a1b2c3d First commit
# pick e4f5g6h Second commit
# pick i7j8k9l Third commit
# pick m0n1o2p Fourth commit
# pick q3r4s5t Fifth commit
# Commands:
# pick = use commit as-is
# reword = use commit, edit message
# edit = pause to amend the commit
# squash = combine with previous commit
# fixup = like squash, discard this message
# drop = remove commit entirely
# exec = run a shell command
# reorder = move lines to reorder commitsSquash Commits
Squashing combines multiple commits into one, creating clean history. This is ideal for merging a feature branch: squash 20 'WIP' commits into one meaningful 'feat: add login' commit. The --fixup flag creates a special commit that --autosquash automatically places and squashes with its target — great for fixing review feedback without cluttering history. git reset --soft main followed by a single commit squashes everything at once (simpler than interactive rebase for total squashing). Many teams configure PR merges to auto-squash (GitHub's 'Squash and merge' option).
# Squash the last 3 commits into one
git rebase -i HEAD~3
# In editor, change:
# pick a1b2c3d First
# squash e4f5g6h Second (or 'fixup' to discard message)
# squash i7j8k9l Third
# Git prompts for combined commit message
# Squash everything since branching from main
git rebase -i main
# Auto-squash fixup commits
git commit --fixup a1b2c3d # creates fixup! commit
git rebase -i --autosquash main
# Git automatically reorders and squashes fixup commits
# Squash all commits into one on a branch
git reset --soft main
git commit -m "feat: complete feature X"Rebase vs Merge
Merge preserves the complete branch history (with merge commits showing where features diverged and merged). Rebase replays commits on top of the target, creating linear history without merge commits. Merge is safer (no history rewriting) and shows feature context. Rebase is cleaner but rewrites commit history. Common strategy: rebase your feature branch on latest main before merging, then merge (fast-forward or with --no-ff for a merge commit). This gives clean commits AND a merge commit marking the feature. For public/shared branches, prefer merge to avoid history conflicts.
# MERGE: preserves complete history with merge commits
git checkout main
git merge feature
# Creates a merge commit, history looks like a graph:
# * merge commit
# |\
# | * feature commit 2
# | * feature commit 1
# * main commit
# REBASE: linear history, no merge commits
git checkout feature
git rebase main
git checkout main
git merge feature # fast-forward, no merge commit
# History is linear:
# * feature commit 2
# * feature commit 1
# * main commit
# When to use each:
# Merge: preserve context of feature branch, public repos
# Rebase: clean linear history, before pushing feature branchResolving Rebase Conflicts
Rebase conflicts occur when replaying commits onto a changed base. Resolve each conflict, git add the resolved files, and git rebase --continue. The rebase processes one commit at a time, so you may hit multiple conflicts. --skip discards a commit (use if it becomes empty after rebase). --abort cancels everything and returns to the pre-rebase state — always a safe escape. For complex conflicts, git mergetool launches a visual merge tool (VS Code, Beyond Compare, etc.). The key difference from merge conflicts: rebase may require resolving the same conflict multiple times (once per replayed commit).
# During rebase, conflicts pause the process
git rebase main
# CONFLICT in file.txt
# 1. Resolve conflicts manually in the file
# (look for <<<<<<< ======= >>>>>>> markers)
# 2. Stage resolved files
git add file.txt
# 3. Continue the rebase
git rebase --continue
# Skip a commit (if it's empty after rebase)
git rebase --skip
# Abort the entire rebase (return to original state)
git rebase --abort
# Use a merge tool for conflicts
git mergetool
# After resolving, the rebase continues
# with the next commit automaticallyRebase Onto (Advanced)
git rebase --onto is an advanced form for precise commit transplantation. The syntax: rebase --onto NEW-BASE OLD-BASE BRANCH — it takes commits between OLD-BASE and BRANCH, and replays them onto NEW-BASE. This is useful for changing a branch's base point (e.g., your feature was based on another feature that got merged; rebase onto main to clean up). It's also used to remove specific commits from history (replay around them). This is a power-user feature — understand regular rebase first. Always have a backup (reflog) before advanced history rewriting.
# Scenario: feature branch was based on old-branch,
# but you want to rebase onto main instead
# git rebase --onto new-base old-base branch
git rebase --onto main old-branch feature
# This takes commits from old-branch..feature
# and replays them onto main
# Use case: remove commits from the middle
# Remove commit C from branch (replay A,B,D onto base)
# base -> A -> B -> C -> D
# git rebase --onto base C D
# Result: base -> A -> B -> D'
# Use case: move a branch to a different parent
git rebase --onto main feature/sub-feature feature/main-feature
# Moves main-feature's unique commits from sub-feature to mainCherry-pick & Bisect
git cherry-pick
cherry-pick applies a specific commit from one branch to another. Use cases: apply a bugfix to a release branch, copy a commit you forgot to merge, or selectively port features. The commit gets a new hash (different parent). Cherry-pick can cause conflicts if the target branch has diverged. --no-commit stages changes without committing (useful for combining multiple cherry-picks). Avoid excessive cherry-picking — it can create duplicate commits when branches eventually merge. For systematic backporting, use release branches with merges instead.
# Apply a specific commit to current branch
git cherry-pick a1b2c3d
# Cherry-pick multiple commits
git cherry-pick a1b2c3d e4f5g6h i7j8k9l
# Cherry-pick a range (exclusive start, inclusive end)
git cherry-pick A..E # commits B, C, D, E
# Cherry-pick without committing (stage only)
git cherry-pick --no-commit a1b2c3d
# or: -n
# Cherry-pick and edit commit message
git cherry-pick --edit a1b2c3d
# Cherry-pick from another branch
git cherry-pick feature-branch~2
# If conflicts occur:
git add . && git cherry-pick --continue
git cherry-pick --abort # cancelgit bisect (Binary Search)
git bisect performs a binary search through commit history to find the exact commit that introduced a bug. You mark the current state as 'bad' and a known-working commit as 'good'. Git checks out the midpoint; you test and mark good/bad. Each step halves the search space — finding a bug in 1000 commits takes ~10 steps. bisect reset returns to your original branch. This is invaluable for tracking down regressions. Combine with git bisect log to save/restore bisect sessions. The culprit commit often reveals the root cause immediately.
# Find which commit introduced a bug
git bisect start
# Mark current (bad) commit
git bisect bad
# Mark a known-good commit (last working version)
git bisect good v1.0.0
# or: git bisect good a1b2c3d
# Git checks out a middle commit
# Test it, then mark:
git bisect good # if this commit works
git bisect bad # if this commit is broken
# Repeat until Git identifies the culprit:
# "a1b2c3d is the first bad commit"
# When done:
git bisect reset # return to original branch
# Skip a commit (can't test, e.g., doesn't build)
git bisect skipAutomated Bisect
Automated bisect runs a test script for each commit, eliminating manual testing. The script exits 0 (good), non-zero (bad), or 125 (skip — e.g., build failure). Git automatically marks each commit and finds the culprit. This is extremely powerful with a test suite: git bisect run npm test finds the breaking commit in minutes. You can also restrict the search to specific files (git bisect start -- path/to/file) to speed things up. The script can be anything: a test, a build check, or a curl command checking an API. Save the bisect log to reproduce or resume later.
# Auto-bisect with a test script
git bisect start
git bisect bad HEAD
git bisect good v1.0.0
# Git runs this script for each commit
# Exit 0 = good, exit 1 = bad, 125 = skip
git bisect run npm test
# Or a custom script
git bisect run ./scripts/check-bug.sh
# Example check script:
#!/bin/bash
npm run build || exit 125 # skip if build fails
npm test -- --grep "login bug"
# exit 0 if test passes (good), 1 if fails (bad)
# Git automatically finds the bad commit
# without manual testing
# Bisect with a file range (faster)
git bisect start -- src/auth/login.jsgit blame & annotate
git blame shows the author and commit for each line of a file — essential for understanding why code exists. -L restricts to a line range (faster, more focused). -w ignores whitespace-only changes (shows the real content author). -M detects code moved within the same file; -C detects code copied from other files (shows the original author, not the copier). blame is for understanding, not blaming — use it to find context for code, then read the full commit with git show. GitHub's 'Blame' button provides a visual interface. Combine with git log -S to find when specific text was added.
# Show who last modified each line
git blame file.txt
# Blame specific line range
git blame -L 10,20 file.txt
# Show full commit hash (not abbreviated)
git blame -l file.txt
# Show email instead of name
git blame -e file.txt
# Ignore whitespace changes
git blame -w file.txt
# Detect moved lines within file
git blame -M file.txt
# Detect moved lines from other files
git blame -C file.txt
# Blame with commit message
git blame --show-name --show-email file.txt | headgit revert (Safe Undo)
git revert creates a new commit that undoes a previous commit — it's the safe way to undo changes on shared branches (unlike reset, which rewrites history). revert is ideal for production branches where you can't rewrite history. Reverting a merge commit requires -m 1 (mainline parent) — this undoes the merge while keeping the branch history. Reverting a revert re-applies the original change (common when a revert was mistaken). For multiple commits, revert in reverse order (newest first) to minimize conflicts. Always use revert on shared/public branches; use reset only on local branches.
# Revert a commit (creates a NEW commit that undoes it)
git revert a1b2c3d
# Revert multiple commits
git revert a1b2c3d e4f5g6h
# Revert a range
git revert A..E
# Revert without committing (stage the reversal)
git revert --no-commit a1b2c3d
# Revert a merge commit
git revert -m 1 a1b2c3d
# -m 1 specifies the mainline parent (parent 1 = the branch
# you merged INTO, parent 2 = the branch you merged FROM)
# If revert conflicts:
git add . && git revert --continue
git revert --abortReflog & Recovery
git reflog Basics
The reflog records every change to HEAD and branch pointers — even operations that 'destroy' commits (reset --hard, rebase, branch deletion). This is your safety net: 'lost' commits are recoverable via reflog for ~90 days (default). reflog is local (never pushed) and shows the chronological history of pointer movements. To recover, find the commit hash in reflog and checkout/reset to it. The @{N} syntax references entries: HEAD@{0} is current, HEAD@{1} is previous. If you ever think you 'lost' work, check reflog first — it's almost certainly still there.
# View the reflog (reference log)
git reflog
# Shows ALL reference changes, including:
# - commits, checkouts, resets, rebases, merges
# Example output:
# a1b2c3d HEAD@{0}: reset: moving to HEAD~1
# e4f5g6h HEAD@{1}: commit: add feature
# i7j8k9l HEAD@{2}: checkout: moving from main to feature
# Reflog for a specific branch
git reflog show feature-branch
# Reflog with dates
git reflog --date=iso
# Recover a "lost" commit
git reflog
# Find the commit hash, then:
git checkout a1b2c3d # or: git reset --hard a1b2c3dRecovering Deleted Branches
Deleted branches and reset commits are recoverable through reflog. The commit objects still exist in Git's object store until garbage collection (default: 90 days for unreachable objects). To recover a deleted branch, find its tip commit in reflog and create a new branch pointing to it. For reset --hard mistakes, reflog shows the previous HEAD position — reset back to it. For bad rebases, find the reflog entry before the rebase started and reset to it. The key lesson: in Git, almost nothing is truly lost immediately. Always check reflog before panicking.
# Accidentally deleted a branch?
git branch -D feature # force deleted
# Find it in reflog
git reflog
# e4f5g6h HEAD@{2}: commit: work on feature
# Recreate the branch at that commit
git branch feature e4f5g6h
# Or checkout and create
git checkout -b feature e4f5g6h
# Recover after reset --hard
git reset --hard HEAD~3 # oops, went too far
git reflog
# Find the commit before the reset
git reset --hard HEAD@{1} # undo the reset
# Recover after a bad rebase
git reflog
git reset --hard HEAD@{5} # before the rebase startedgit fsck (Dangling Objects)
git fsck checks repository integrity and finds dangling objects — commits, blobs, and trees not referenced by any branch or tag. --lost-found writes these to .git/lost-found/. This is the last resort when reflog doesn't have what you need (reflog entries expire, or git gc ran). Dangling commits are often the result of aborted operations or expired reflog entries. Inspect with git show, then recover by creating a branch. fsck --full verifies all objects' integrity (useful for detecting corruption). Run fsck periodically on important repos to catch issues early.
# Find dangling (unreachable) commits
git fsck --lost-found
# Output:
# dangling commit a1b2c3d...
# dangling blob e4f5g6h...
# Inspect a dangling commit
git show a1b2c3d
# Recover it
git branch recovered a1b2c3d
# Full fsck (check repository integrity)
git fsck --full
# Check for dangling objects only
git fsck --no-reflogs --dangling
# When reflog doesn't have what you need
# (e.g., expired, or gc ran), fsck finds
# ALL dangling objects in the object storegit stash Deep Dive
git stash temporarily shelves uncommitted changes. push -m adds a descriptive message (essential for managing multiple stashes). -u includes untracked files; -a includes ignored files too. apply reapplies without removing; pop applies and removes. stash branch creates a new branch from the stash (useful if the stash conflicts with current branch). Stashes are stored in a stack (LIFO) — reference by stash@{N}. Show -p displays the diff. Stashes persist across reboots but are local (never pushed). Clean up old stashes regularly; they accumulate. For long-term work, create a branch instead of stashing.
# Stash with message
git stash push -m "work in progress on login"
# Stash specific files
git stash push -m "wip" file1.txt file2.txt
# Stash including untracked files
git stash push -u # or --include-untracked
# Stash including ignored files
git stash push -a # or --all
# List stashes
git stash list
# stash@{0}: On feature: wip on login
# stash@{1}: On main: quick fix
# Apply a specific stash
git stash apply stash@{1}
# Apply and drop (pop)
git stash pop # applies stash@{0} and removes it
# Show stash contents
git stash show -p stash@{0}
# Create branch from stash
git stash branch new-branch stash@{0}
# Drop a stash
git stash drop stash@{1}
git stash clear # drop ALL stashesgit tag Management
Tags mark specific commits as important (releases, milestones). Annotated tags (-a) store metadata (tagger, date, message) and are recommended for releases. Lightweight tags are just named pointers (no metadata). Signed tags (-s) use GPG for verification (important for security releases). Tags are NOT pushed by default — use --tags to push them. Semantic versioning (v1.2.3) is the standard naming convention. Checkout a tag for a detached HEAD state (to inspect or build a release). GitHub releases are built on tags — create a tag, then publish a release with notes.
# Create annotated tag (recommended)
git tag -a v1.0.0 -m "Release 1.0.0"
# Create lightweight tag
git tag v1.0.0
# Tag a specific commit
git tag -a v0.9.0 -m "beta" a1b2c3d
# List all tags
git tag
git tag -l "v1.*" # filter by pattern
# Push tags to remote
git push origin v1.0.0 # single tag
git push origin --tags # all tags
# Delete a tag
git tag -d v1.0.0 # local
git push origin --delete v1.0.0 # remote
# Show tag details
git show v1.0.0
# Checkout a tag (detached HEAD)
git checkout v1.0.0
# Signed tags (GPG)
git tag -s v1.0.0 -m "signed release"Worktree & Submodules
git worktree
git worktree creates additional working directories from the same repository — no need to clone. Each worktree checks out a different branch simultaneously. This is perfect for: working on a hotfix while keeping your feature branch open, running tests on one branch while coding on another, or having long-running builds on one worktree. All worktrees share the same .git directory (objects, refs), so they stay in sync and save disk space. You can't check out the same branch in two worktrees (Git prevents this to avoid conflicts). Worktrees are faster than cloning for multi-branch workflows.
# Create a new working directory linked to the repo
git worktree add ../project-hotfix hotfix-branch
# Work in the new directory
cd ../project-hotfix
# This is a full working tree on hotfix-branch
# Shares the same .git, so no cloning needed
# List worktrees
git worktree list
# /path/to/project main
# /path/to/project-hotfix hotfix-branch
# Remove a worktree
git worktree remove ../project-hotfix
# Prune stale worktree entries
git worktree prune
# Create worktree at new branch
git worktree add -b feature-x ../project-feature maingit submodule Basics
Submodules embed one Git repository inside another — useful for including shared libraries or dependencies. The parent repo stores a pointer (commit hash) to the submodule, not its content. --recurse-submodules is essential when cloning (otherwise submodules are empty). Updating submodules (--remote) fetches the latest commits; you must then commit the new hash in the parent repo. Submodules are complex: branches, conflicts, and updates require careful handling. For simpler dependency management, consider Git subtrees, package managers (npm, pip), or monorepo strategies. Use submodules when you need to track a specific external commit.
# Add a submodule to your repo
git submodule add https://github.com/user/lib.git libs/lib
# Clone a repo with submodules
git clone --recurse-submodules https://github.com/user/project.git
# If you forgot --recurse-submodules:
git submodule update --init --recursive
# Pull latest changes in all submodules
git submodule update --remote
# Pull specific submodule
git submodule update --remote libs/lib
# After updating submodules, commit the new hash
git add libs/lib
git commit -m "update lib submodule"
# Execute command in all submodules
git submodule foreach 'git status'Submodule Workflows
Working inside a submodule is like working in a normal repo — you commit and push from within the submodule directory. The parent repo tracks the commit hash, so after changing a submodule, you must commit in the parent too. When switching branches, submodule content may not match — run git submodule update --init --recursive to sync. Deleting submodules requires three steps: deinit (unregister), git rm (remove from tracking), and manual deletion of .git/modules. Submodule workflows are error-prone; always communicate with your team when updating submodules to avoid hash mismatches.
# Change code inside a submodule
cd libs/lib
# Make changes, commit, and push
git add .
git commit -m "fix: bug in lib"
git push origin HEAD:main
# Back in parent repo, update the pointer
cd ../..
git add libs/lib
git commit -m "chore: update lib submodule"
# Switch branches with submodules
git checkout main
git submodule update --init --recursive
# Delete a submodule
git submodule deinit -f libs/lib
git rm libs/lib
rm -rf .git/modules/libs/lib
git commit -m "remove lib submodule"
# Move a submodule
git mv libs/lib libs/new-libGit Hooks
Git hooks run scripts at specific points in the Git lifecycle. Client-side hooks (pre-commit, pre-push, commit-msg) enforce local standards. pre-commit is ideal for linting/formatting; pre-push for running tests; commit-msg for enforcing conventional commits. Hooks are NOT tracked by Git (they live in .git/hooks/), so they don't sync between clones. To share hooks across a team, use a tool like Husky (npm), pre-commit (Python), or commit the hooks to a checked-in directory and symlink them. Server-side hooks (pre-receive, post-receive) run on the remote and can enforce policies for all contributors.
# Hooks live in .git/hooks/ (not tracked by Git)
# Sample hooks are provided with .sample extension
# pre-commit: runs before commit is created
# .git/hooks/pre-commit
#!/bin/bash
npm run lint || exit 1 # block commit if lint fails
# pre-push: runs before pushing
#!/bin/bash
npm test || exit 1 # block push if tests fail
# commit-msg: validate commit message format
#!/bin/bash
# $1 is the path to the commit message file
if ! grep -qE "^(feat|fix|docs|chore):" "$1"; then
echo "Use conventional commit format: type: message"
exit 1
fi
# prepare-commit-msg: auto-populate message
#!/bin/bash
echo "# Branch: $(git branch --show-current)" >> "$1"
# Make hooks executable
chmod +x .git/hooks/pre-commitGit LFS (Large File Storage)
Git LFS replaces large files (binaries, videos, datasets) with text pointers in Git, storing the actual content on a separate LFS server. This keeps the repo lightweight — without LFS, binary files bloat the repo permanently (every version is stored). Track file patterns with git lfs track, then commit .gitattributes. After that, large files work transparently. git lfs migrate import retroactively converts existing files to LFS (rewrites history — coordinate with team first). LFS requires server support (GitHub, GitLab, Bitbucket all support it). Note: LFS has bandwidth/storage quotas on hosted platforms. For truly huge files, consider external storage with URLs.
# Initialize Git LFS
git lfs install
# Track large file types
git lfs track "*.psd"
git lfs track "*.zip"
git lfs track "assets/**"
# This creates/updates .gitattributes
# Commit the .gitattributes file
git add .gitattributes
git commit -m "chore: configure LFS tracking"
# Add large files normally
git add design.psd
git commit -m "add design file"
git push
# View LFS tracked files
git lfs ls-files
# Pull LFS content (if not auto-downloaded)
git lfs pull
# Migrate existing files to LFS (rewrites history)
git lfs migrate import --include="*.psd" --everything
# Check LFS status
git lfs statusStash
Save & Pop
git stash shelves uncommitted changes so you can switch branches or pull updates with a clean working tree. pop applies and removes the top stash; apply keeps it. The stash is a LIFO stack.
# Save uncommitted changes (tracked files only)
git stash
# Save including untracked files
git stash -u
# Pop the most recent stash (removes it)
git stash pop
# Apply without removing (keeps stash in list)
git stash applyNamed Stashes
Always pass -m to label stashes — the default message is the branch and commit, which is rarely descriptive. stash@{N} references a specific stash by its index.
# Save with a descriptive message
git stash push -m "WIP: refactor auth flow"
# List all stashes with messages
git stash list
# Apply a specific stash by index
git stash apply stash@{1}Stash Branches
git stash branch creates a new branch from the commit where the stash was originally made, then applies the stash there. This is the cleanest way to recover when a stash no longer applies cleanly.
# Create a new branch from a stash
git stash branch feature-branch stash@{0}
# Useful when stash conflicts with current branch
# - Creates branch from the commit where stash was made
# - Applies stash on top
# - Drops stash if apply succeedsPartial Stash
Stash only what you need with -p (interactive hunk selection) or by listing specific files. --keep-index stashes unstaged changes but leaves staged changes in place.
# Interactively choose hunks to stash
git stash push -p
# Stash specific files only
git stash push -m "config tweaks" config.yml .env.local
# Stash with keep-index (staged changes stay)
git stash --keep-indexStash Management
git stash show -p displays the full diff of a stash. drop removes a single stash; clear wipes them all (irreversible). Stashes are local only; they never push to remotes.
# Show what's in a stash
git stash show -p stash@{0}
# Drop a specific stash
git stash drop stash@{0}
# Clear all stashes (irreversible)
git stash clear
# Show stash list with dates
git stash list --date=relativeRebase
Basic Rebase
Rebase moves your branch commits on top of another branch, producing a linear history. Unlike merge, it rewrites commit hashes. Never rebase commits that have been pushed and shared.
# Rebase current branch onto main
git checkout feature
git rebase main
# Abort if conflicts are too messy
git rebase --abort
# Continue after resolving conflicts
git rebase --continueInteractive Rebase
Interactive rebase (-i) lets you rewrite history before sharing: reorder commits, squash related ones into a single clean commit, reword messages, or drop mistakes. Always do this on unpushed commits.
# Rebase last 5 commits interactively
git rebase -i HEAD~5
# Commands: pick, reword, edit, squash, fixup, drop
# squash = combine with previous, keep message
# fixup = combine with previous, discard message
# reword = change commit message onlySquash & Fixup
--fixup creates a commit marked as a fix for another. --autosquash during rebase automatically places fixup! and squash! commits next to their targets. This streamlines the "commit early, clean up later" workflow.
# Create a fixup commit targeting an earlier commit
git commit --fixup a1b2c3
# Autosquash during rebase (auto-reorders fixups)
git rebase -i --autosquash HEAD~5Rebase --onto
--onto is surgical rebase: it moves a range of commits from one base to another. Use it to re-parent a branch, or to drop the first few commits of a branch.
# Move commits from one base to another
# git rebase --onto <new-base> <old-base> <branch>
git rebase --onto main feature-old feature-new
# Drop the first N commits of a branch
git rebase --onto main HEAD~3Rebase Conflicts
During rebase, conflicts stop at each commit. Resolve, git add, then --continue to proceed. --skip drops the conflicting commit entirely. --abort returns to the pre-rebase state.
# During rebase, conflicts pause the process
git rebase main
# CONFLICT (content): Merge conflict in file.js
# Resolve in editor, then:
git add file.js
git rebase --continue
# Skip a commit that conflicts (drop it)
git rebase --skip
# Give up entirely
git rebase --abortCherry-Pick
Cherry-Pick a Commit
cherry-pick applies a specific commit from another branch onto your current branch, creating a new commit with the same changes. The new commit has a different hash because the parent is different.
# Apply a specific commit to current branch
git cherry-pick a1b2c3d
# Cherry-pick multiple commits
git cherry-pick a1b2c3d e4f5g6h
# Cherry-pick a range (exclusive of start, inclusive of end)
git cherry-pick A..ENo-Commit Cherry-Pick
--no-commit (-n) stages the cherry-picked changes without creating a commit. This lets you combine multiple cherry-picks into one commit, or modify the changes before committing.
# Apply changes to working tree without committing
git cherry-pick --no-commit a1b2c3d
# Stage the changes yourself
git add -p
git commit -m "Custom message"Cherry-Pick Conflicts
Conflicts during cherry-pick pause the operation. Resolve, git add, and --continue. --skip abandons the current commit. --abort cancels the cherry-pick and restores the branch.
git cherry-pick a1b2c3d
# error: could not apply a1b2c3d...
# Resolve conflicts, then:
git add resolved-file.js
git cherry-pick --continue
# Skip this commit
git cherry-pick --skip
# Abort the entire cherry-pick
git cherry-pick --abortCherry-Pick from Another Branch
The classic hotfix workflow: fix a bug on a maintenance branch, then cherry-pick the same commit onto main (and other active branches). This avoids merging unrelated feature work.
# Find the commit hash on another branch
git log --oneline feature-branch
# Switch to target branch
git checkout main
# Cherry-pick the commit
git cherry-pick <hash>
# Useful for hotfixes: fix on a feature branch,
# then cherry-pick the fix to mainCherry-Pick Strategy
-X theirs/ours biases conflict resolution. -x adds a line recording the original commit hash — essential for audit trails when cherry-picking hotfixes across branches.
# Use a different merge strategy
git cherry-pick -X theirs a1b2c3d # prefer their changes on conflict
git cherry-pick -X ours a1b2c3d # prefer our changes on conflict
# Preserve original commit author
git cherry-pick -x a1b2c3d # adds "(cherry picked from ...)" to message
# Edit commit message before committing
git cherry-pick -e a1b2c3dBisect
Basic Bisect
git bisect performs a binary search through commit history to find which commit introduced a bug. You mark the current commit as bad and a known-good commit as good. After ~log2(N) steps, git names the offending commit.
# Start bisecting
git bisect start
# Mark current commit as bad (has the bug)
git bisect bad
# Mark a known-good commit (older)
git bisect good v1.0.0
# Git checks out the midpoint. Test, then mark:
git bisect good # or
git bisect bad
# Exit bisect mode
git bisect resetBisect Log & Replay
git bisect log records every good/bad decision. If you mis-mark a commit (a common mistake), reset and replay the log, then fix the wrong step.
# Record what happens during bisect
git bisect log > bisect.log
# If you make a mistake, replay from the log
git bisect reset
git bisect replay bisect.log
# Visualize the bisect state
git bisect visualizeAutomated Bisect
git bisect run automates the search: git checks out each candidate, runs your script, and marks the commit based on the exit code. This is dramatically faster than manual testing.
# Auto-bisect: git runs a test script and marks good/bad itself
git bisect start HEAD v1.0.0 -- # bad=HEAD, good=v1.0.0
git bisect run npm test # exit 0=good, 1-124=bad, 125=skip
# Or with a custom script
git bisect run ./scripts/check-bug.shBisect by File
Passing a path to git bisect start restricts the search to commits that modified that file. This skips hundreds of irrelevant commits and homes in on the file where the bug likely lives.
# Limit bisect to changes in a specific file
git bisect start -- path/to/file.js
# Combine with bad/good markers
git bisect bad HEAD
git bisect good v1.0.0
# Git only considers commits that touched that fileBisect Reset
Always run git bisect reset when done — it returns you to your original branch and cleans up bisect state. Without reset, your working tree stays on the last tested commit.
# Exit bisect mode and return to original branch
git bisect reset
# Reset to a specific branch/commit instead
git bisect reset <branch>
# View current bisect state
git bisect statusSubmodules
Add a Submodule
Submodules embed one Git repository inside another — useful for vendoring shared libraries. git submodule add registers the submodule in .gitmodules. New clones need --recurse-submodules.
# Add a submodule at a specific path
git submodule add https://github.com/user/lib.git libs/lib
# This creates:
# - libs/lib/ (the submodule checkout)
# - .gitmodules (tracks submodule URLs and paths)
# Commit the new submodule
git commit -m "Add lib submodule"
# Clone a repo with submodules included
git clone --recurse-submodules https://github.com/user/repo.gitUpdate Submodules
A submodule is pinned to a specific commit. git submodule update --remote fetches the latest commit on the tracked branch and updates the pointer. You must commit this pointer change in the parent repo.
# Pull latest changes in all submodules
git submodule update --remote
# Update a specific submodule
git submodule update --remote libs/lib
# Then commit the updated pointer
git add libs/lib
git commit -m "Bump lib submodule"
# Initialize submodules after cloning
git submodule update --init --recursiveSubmodule Foreach
foreach runs a shell command in each submodule directory — useful for bulk operations like checking status, pulling updates, or building. --recursive descends into nested submodules.
# Run a command in every submodule
git submodule foreach 'git status'
# With recursion into nested submodules
git submodule foreach --recursive 'git checkout main'
# Use --quiet to suppress the "Entering..." messages
git submodule foreach --quiet 'git pull'Deinit & Remove
Removing a submodule is a multi-step process: deinit unregisters it, rm -rf .git/modules/... deletes the submodule Git data, and git rm removes the working tree. Forgetting the .git/modules cleanup leaves orphan data.
# Deinit a submodule (unregisters, keeps files)
git submodule deinit libs/lib
# Fully remove a submodule
git submodule deinit -f libs/lib
rm -rf .git/modules/libs/lib
git rm -f libs/lib
git commit -m "Remove lib submodule"Submodule Branches
By default, submodules are in detached HEAD. Setting submodule.<name>.branch lets --remote track that branch. To make changes inside a submodule, cd in, checkout a branch, commit, and push.
# Configure a submodule to track a branch
git config -f .gitmodules submodule.libs/lib.branch main
# Now --remote updates from that branch
git submodule update --remote
# Work inside a submodule like a normal repo
cd libs/lib
git checkout feature-branch
git push
cd ../..
git add libs/lib
git commit -m "Update lib to feature-branch"Hooks
Common Hooks
Git hooks are scripts in .git/hooks/ that run automatically at specific points. Client-side hooks run on your machine and can block actions. Server-side hooks run on the remote and enforce policy. Hooks are not versioned by default — use Husky to share them.
# Client-side (run on your machine):
# pre-commit - runs before commit is created
# prepare-commit-msg - can edit commit message
# commit-msg - validates commit message
# pre-push - runs before pushing
# post-merge - runs after git merge
# Server-side (run on the remote):
# pre-receive - runs before refs are updated
# update - runs once per ref
# post-receive - runs after refs are updatedpre-commit Hook
pre-commit runs before the commit is created; a non-zero exit aborts the commit. Common uses: lint staged files, run focused tests, format code. Keep it fast (<5 seconds) or developers will bypass it with --no-verify.
#!/bin/sh
# .git/hooks/pre-commit
# Run linter on staged files
npm run lint-staged
if [ $? -ne 0 ]; then
echo "Linting failed. Fix errors and try again."
exit 1
fi
# Run tests
npm test -- --findRelatedTests
if [ $? -ne 0 ]; then
echo "Tests failed."
exit 1
fi
exit 0commit-msg Hook
commit-msg receives the path to the temporary commit message file as $1. It can validate or rewrite the message. A non-zero exit rejects the commit. This is the standard way to enforce Conventional Commits.
#!/bin/sh
# .git/hooks/commit-msg
# Enforce Conventional Commits format
pattern='^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)(\(.*\))?: .{1,72}$'
if ! grep -qE "$pattern" "$1"; then
echo "Invalid commit message format."
echo "Use: <type>(<scope>): <description>"
exit 1
fiHusky Setup
Husky installs Git hooks from a versioned .husky/ directory, so every team member gets the same hooks after npm install. lint-staged runs commands only on staged files, keeping pre-commit fast.
# Install Husky
npm install --save-dev husky
npx husky init
# Add a pre-commit hook
echo 'npm run lint-staged' > .husky/pre-commit
# Add a commit-msg hook
echo 'npx --no-install commitlint --edit $1' > .husky/commit-msg
# package.json
{
"scripts": { "prepare": "husky" },
"lint-staged": {
"*.{js,ts}": ["eslint --fix", "prettier --write"]
}
}pre-push Hook
pre-push runs before refs are pushed; non-zero exit aborts. It reads proposed pushes from stdin. Common uses: block pushes to protected branches, run the full test suite.
#!/bin/sh
# .husky/pre-push
# Reads stdin: <local ref> <local sha> <remote ref> <remote sha>
while read local_ref local_sha remote_ref remote_sha; do
if [ "$remote_ref" = "refs/heads/main" ]; then
echo "Direct push to main is not allowed."
exit 1
fi
done
npm test
if [ $? -ne 0 ]; then
echo "Tests failed. Push aborted."
exit 1
fiWorktrees
Add a Worktree
A worktree is a separate working directory linked to the same repository. You can have multiple branches checked out simultaneously in different directories — no stashing to switch context.
# Create a worktree at a path, on a new branch
git worktree add ../project-feature feature-branch
# Worktree on an existing branch
git worktree add ../project-hotfix hotfix-branch
# Worktree at a specific commit (detached HEAD)
git worktree add --detach ../project-inspect v1.0.0
# List all worktrees
git worktree listWorktree Workflow
Worktrees shine for context switching: an urgent bug arrives while you are deep in a feature. Instead of stashing and losing your IDE state, create a worktree on main, fix the bug there, and return.
# Scenario: working on feature, urgent bug comes in
git worktree add ../project-hotfix main
cd ../project-hotfix
git checkout -b fix-urgent
# ... fix the bug, commit, push ...
cd ../project-feature
# Your feature work is untouched
# Clean up when done
git worktree remove ../project-hotfixRemove & Prune
remove deletes a worktree directory and its admin metadata. The branch it was on remains. If the worktree has uncommitted changes, remove refuses unless you pass --force.
# Remove a worktree (must be clean or use --force)
git worktree remove ../project-feature
# Force remove even with uncommitted changes
git worktree remove --force ../project-feature
# Prune worktree admin files for deleted directories
git worktree prune
# Show what would be pruned
git worktree prune --dry-run -vWorktree Benefits
Worktrees solve several pain points: no stashing for context switches, parallel builds/tests, isolated node_modules per branch, and side-by-side branch comparison. The shared object database means minimal disk overhead.
# 1. No stashing needed for context switches
# 2. Run long builds/tests in one worktree while
# continuing work in another
# 3. Each worktree has its own node_modules
# 4. Compare two branches side-by-side in two
# editor windows
# 5. Shared object database = minimal disk overhead
# Typical layout:
# ~/projects/
# main-repo/ (main branch)
# main-repo-feature/ (feature worktree)
# main-repo-hotfix/ (hotfix worktree)Locked Worktrees
Lock a worktree when it contains work that should not be disturbed (long-running builds, attached debugger). Locked worktrees survive prune. move relocates a worktree; repair fixes the admin files.
# Lock a worktree (prevents it from being pruned)
git worktree lock ../project-feature --reason "Running long build"
# Unlock when done
git worktree unlock ../project-feature
# Move a worktree to a new path
git worktree move ../project-feature ../new-location/project-feature
# Repair after moving manually
git worktree repair ../new-location/project-featureReflog
View Reflog
The reflog records every change to HEAD and branch tips — commits, checkouts, resets, rebases. It is a local safety net: even after a destructive operation, the commits are still in the reflog.
# Show reflog for HEAD
git reflog
# a1b2c3d HEAD@{0}: commit: Fix bug
# e4f5g6h HEAD@{1}: checkout: moving to feature
# 789abc0 HEAD@{2}: reset: moving to HEAD~1
# Show reflog for a specific branch
git reflog show feature
# Show reflog with dates
git reflog --date=isoRecover Lost Commits
After a hard reset or rebase, "lost" commits are still reachable via the reflog. Find the hash in git reflog, then reset --hard or cherry-pick to recover. This is why Git is safe — almost nothing is truly gone.
# Find the commit you lost (e.g., after a hard reset)
git reflog
# a1b2c3d HEAD@{0}: reset: moving to HEAD~1
# e4f5g6h HEAD@{1}: commit: Important work <-- this one
# Reset back to the lost commit
git reset --hard e4f5g6h
# Or cherry-pick it onto your current branch
git cherry-pick e4f5g6hReflog & Reset
ORIG_HEAD is a convenience ref that points to the previous HEAD after destructive operations (reset, merge, rebase). git reset --hard ORIG_HEAD undoes the last such operation in one command.
# Undo a git reset --hard
git reflog
git reset --hard HEAD@{1}
# Undo a rebase
git reflog
git reset --hard HEAD@{5}
# Undo a merge
git reset --hard ORIG_HEAD
# ORIG_HEAD is set by merge, rebase, reset, and cherry-pickExpire & Cleanup
Reflog entries accumulate and consume disk space. expire removes old entries; --expire-unreachable targets only entries not reachable from any ref. After expiring, run git gc --prune=now to delete unreachable objects.
# Expire reflog entries older than 30 days
git reflog expire --expire=30.days
# Expire entries unreachable from current branches
git reflog expire --expire-unreachable=30.days
# Delete a specific reflog entry
git reflog delete HEAD@{2}
# Dry run (see what would be expired)
git reflog expire --dry-run --expire=now --allReflog for Branches
Each branch maintains its own reflog. If you delete a branch with git branch -D, the commits are still in the reflog — find the hash and recreate the branch with git branch <name> <hash>.
# Each branch has its own reflog
git reflog show main
# a1b2c3d main@{0}: commit: Update docs
# e4f5g6h main@{1}: pull origin main: Fast-forward
# Recover a deleted branch
git reflog # find the hash the branch pointed to
git branch recovered-branch e4f5g6h
# Compare current state with a past reflog entry
git diff HEAD@{1} HEADLFS
Install & Track
Git LFS stores large files (images, videos, binaries) outside the Git repository, replacing them with pointer files in commits. git lfs track registers patterns in .gitattributes. Always commit .gitattributes before adding large files.
# Install Git LFS (once per user)
git lfs install
# Track large file types
git lfs track "*.psd"
git lfs track "*.zip"
git lfs track "assets/*"
# View tracking rules
cat .gitattributes
# Commit the .gitattributes file
git add .gitattributes
git commit -m "Configure LFS tracking"Add & Commit LFS Files
Once a file pattern is tracked, git add stages the file through LFS automatically. The commit stores a small pointer file (~130 bytes) instead of the binary. The actual content is uploaded to the LFS server on push.
# After configuring tracking, add files normally
git add design.psd
git commit -m "Add design file"
# Verify LFS is handling the file
git lfs ls-files
# a1b2c3d4 * design.psd
# The commit contains a pointer, not the actual file
git show HEAD -- design.psd
# version https://git-lfs.github.com/spec/v1
# oid sha256:...
# size 12345678Clone & Pull
Cloning a repo with LFS downloads pointer files first, then the actual content. If the LFS download fails, git lfs pull retries. git lfs fetch downloads objects without writing them to the working tree.
# Clone a repo with LFS files (downloads them automatically)
git clone https://github.com/user/repo.git
# If LFS files are missing, fetch them
git lfs pull
# Fetch LFS objects without checking them out
git lfs fetch
# Checkout LFS files for specific paths
git lfs checkout assets/Migrate to LFS
git lfs migrate import retroactively converts large files in history to LFS pointers. This rewrites all commit hashes — every collaborator must re-clone. Run --dry-run first to see the impact.
# Convert existing large files to LFS (rewrites history!)
git lfs migrate import --include="*.psd" --include-ref=refs/heads/main
# Migrate all branches
git lfs migrate import --include="*.psd" --everything
# Check what would be migrated (dry run)
git lfs migrate import --include="*.psd" --include-ref=refs/heads/main --dry-runLFS Management
git lfs status shows pending LFS changes. git lfs env displays configuration. git lfs fsck verifies that all LFS objects referenced by pointer files are present and uncorrupted.
# View LFS status
git lfs status
# List all LFS files in the repo
git lfs ls-files
# Check LFS configuration
git lfs env
# Untrack a file type (does not remove existing LFS objects)
git lfs untrack "*.zip"
# Verify LFS objects are present and valid
git lfs fsckCI/CD Integration
GitHub Actions Basic
GitHub Actions runs workflows on push/PR. actions/checkout fetches the repo; fetch-depth: 0 gets full history. npm ci installs from lockfile (faster, stricter than install). Cache dependencies with the cache key.
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm test
- run: npm run buildConditional Workflows
Filter workflows by branch or event with on: push: branches. Use job-level if: conditions to skip jobs based on context. needs: creates dependencies between jobs — deploy only runs if test passes.
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- run: ./deploy.shGit Hooks in CI
CI pipelines often mirror local hooks: lint first (fast fail), then test. needs: lint ensures tests only run if linting passes, saving CI minutes. Splitting jobs allows parallel runners for speed.
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- run: npm run lint
- run: npm run typecheck
test:
needs: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test -- --coverageSecrets & Environment
Store secrets (API keys, tokens) in repo settings and reference them via ${{ secrets.NAME }}. They are never echoed in logs. Environments can require manual approval before deployment, adding a gate.
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # requires manual approval
steps:
- uses: actions/checkout@v4
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
DB_URL: ${{ secrets.DATABASE_URL }}
run: ./scripts/deploy.sh "$API_KEY" "$DB_URL"Matrix Builds
Matrix builds run the same job across multiple OS/language combinations in parallel. This catches platform-specific bugs early. Use fail-fast: false to run all combinations even if one fails. Keep matrices reasonable to control CI minutes.
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node-version: ['18', '20', '22']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm testRelated Git snippets
Copy-paste ready code for common tasks.
Was this helpful?