TL;DR
Git is a content-addressable object store. Every file, directory, commit, and tag is a SHA-hashed object. Mastering interactive rebase cleans history before PRs; reflog saves you from nearly any disaster; git bisect finds bugs in O(log n) steps; hooks enforce quality gates; worktrees enable true parallel branch work; and Git LFS keeps large binaries out of your object store. Use our Git command generator to build complex commands visually.
Key Takeaways
- Git objects (blob, tree, commit, tag) are content-addressed by SHA — identical content is stored once
- Interactive rebase (
git rebase -i) rewrites local history — never use on shared branches git reflogis your 90-day local safety net for recovering from any reset, rebase, or checkout mistakegit bisect run <script>automates bug hunting with binary search — O(log n) steps- Git hooks (pre-commit, commit-msg, pre-push) enforce linting, tests, and conventions automatically
- Worktrees let you work on multiple branches simultaneously without stashing or switching
- Git LFS replaces large binaries with pointer files — essential for repos with assets > 50 MB
- Choose workflow by release cadence: Gitflow for versioned releases, GitHub Flow for continuous delivery, Trunk-Based for high-velocity CI/CD
Git Internals: The Object Model
Understanding Git at the storage level transforms it from a magical version-control system into a predictable, debuggable tool. Git is fundamentally a content-addressable key-value store. Every piece of data — file contents, directory trees, commits, tags — is stored as an object identified by the SHA-1 hash of its content.
The Four Object Types
Git has exactly four object types. Each is stored in .git/objects/ as a zlib-compressed file named by its 40-character hex SHA.
# Inspect any object
git cat-file -t <sha> # show type: blob | tree | commit | tag
git cat-file -p <sha> # pretty-print content
git cat-file -s <sha> # show size in bytes
# 1. BLOB: stores raw file content (no filename, no permissions)
git cat-file -p HEAD:src/index.ts
# → raw TypeScript source
# 2. TREE: stores a directory listing
git cat-file -p HEAD^{tree}
# 100644 blob a1b2c3... README.md
# 100755 blob d4e5f6... scripts/deploy.sh
# 040000 tree 7g8h9i... src
# 3. COMMIT: snapshot + metadata
git cat-file -p HEAD
# tree 7g8h9i...
# parent 0a1b2c...
# author Alice <alice@example.com> 1700000000 +0000
# committer Alice <alice@example.com> 1700000000 +0000
#
# feat: add user authentication
# 4. TAG: annotated tag (points to a commit)
git cat-file -p v1.0.0
# object 0a1b2c...
# type commit
# tag v1.0.0
# tagger Alice <alice@example.com> 1700000000 +0000
#
# Release 1.0.0 - stable auth moduleRefs and the HEAD Pointer
Refs are human-readable names that point to SHA hashes. Branch refs live in .git/refs/heads/, remote-tracking refs in .git/refs/remotes/, and tags in .git/refs/tags/. HEAD is a special ref stored in .git/HEAD — it usually contains a symbolic ref (e.g., ref: refs/heads/main) pointing to the current branch, but in detached HEAD state it contains a direct SHA.
# Read HEAD directly
cat .git/HEAD
# ref: refs/heads/main
# Read a branch ref
cat .git/refs/heads/main
# a1b2c3d4e5f6...
# List all refs
git show-ref
# Symbolic ref resolution
git symbolic-ref HEAD # → refs/heads/main
git rev-parse HEAD # → full SHA
# Relative ref syntax
HEAD~1 # first parent of HEAD
HEAD~3 # three commits back
HEAD^2 # second parent (merge commit)
HEAD@{1} # previous value of HEAD (reflog)Packfiles: How Git Compresses History
Initially Git stores each object as a loose file. When you push, clone, or run git gc, Git creates packfiles — binary files that store many objects together using delta compression (storing the difference between similar objects rather than full copies). Packfiles live in .git/objects/pack/ as a .pack (data) and .idx (index) pair.
# Manually trigger garbage collection + repacking
git gc
# Aggressive repack (maximum compression, slow)
git gc --aggressive
# Inspect packfile contents
git verify-pack -v .git/objects/pack/*.idx | sort -k3 -n | tail -20
# Shows largest objects by uncompressed size
# Count loose objects vs packed
git count-objects -v
# count: 4 (loose objects)
# size: 16 (KB)
# in-pack: 8423 (packed objects)
# packs: 1
# size-pack: 2840 (KB)Interactive Rebase: Squash, Fixup, Reorder, Edit
Interactive rebase is Git's history-editing power tool. It lets you rewrite any sequence of local commits before sharing them, turning messy "WIP" histories into clean, reviewable commit chains. The fundamental rule: only rebase commits you haven't pushed to a shared remote.
Starting an Interactive Rebase
# Rebase last 4 commits interactively
git rebase -i HEAD~4
# Rebase everything since branching from main
git rebase -i main
# Rebase since a specific commit (not including it)
git rebase -i a1b2c3d
# The editor opens with a list like:
pick 1a2b3c feat: add login form
pick 4d5e6f wip: half-done validation
pick 7g8h9i fix typo in variable name
pick 0a1b2c feat: complete validation logicAll Interactive Rebase Commands
# Available actions (replace 'pick' with any of these):
pick p # use commit as-is
reword r # use commit but edit the commit message
edit e # use commit but stop for amending (git commit --amend)
squash s # combine with previous commit; merge both messages
fixup f # combine with previous commit; discard this message
drop d # remove the commit entirely
exec x # run a shell command after this commit
break # pause here (continue with: git rebase --continue)
label l # label HEAD with a name
reset t # reset HEAD to a label
merge m # create a merge commit
# Common patterns:
# 1. Squash 4 WIP commits into 1
pick 1a2b3c feat: start login module
squash 4d5e6f wip: add form component
squash 7g8h9i wip: wire up state
squash 0a1b2c wip: final cleanup
# 2. Fixup a typo commit
pick 1a2b3c feat: add user profile page
fixup 4d5e6f fix typo in prop name
# 3. Reorder commits (just move lines)
pick 7g8h9i refactor: extract validation helper
pick 1a2b3c feat: add login form
pick 4d5e6f feat: add registration form
# 4. Edit a commit mid-rebase
# In editor: change 'pick' to 'edit' for a commit
# Git pauses; make changes, then:
git add -p
git commit --amend
git rebase --continueAuto-Squash with --fixup Commits
Create fixup commits during development with git commit --fixup <sha>, then use git rebase -i --autosquash to automatically arrange and mark them as fixup for the target commit.
# While developing, create fixup commits
git commit -m "feat: add login form"
# later...
git commit --fixup HEAD # creates: "fixup! feat: add login form"
# Alternatively, fixup a specific earlier commit
git commit --fixup a1b2c3d
# At cleanup time, autosquash automatically places fixups
git rebase -i --autosquash main
# Enable autosquash by default (optional)
git config --global rebase.autoSquash trueGit Reflog: Recovering Lost Commits
The reflog records every time a Git ref (branch, HEAD) is updated — commits, resets, rebases, merges, checkouts. It's your local undo history for Git operations themselves, not just file changes. Entries expire after 90 days (configurable) and are never pushed to remotes.
# View reflog for HEAD
git reflog
# HEAD@{0}: reset: moving to HEAD~3
# HEAD@{1}: commit: feat: add payment module
# HEAD@{2}: commit: feat: add cart
# HEAD@{3}: checkout: moving from main to feature/auth
# HEAD@{4}: commit: fix: correct email validation
# View reflog for a specific branch
git reflog show feature/auth
# Recover after accidental git reset --hard
git reset --hard HEAD~5 # oops! lost 5 commits
git reflog # find the SHA before reset
git reset --hard HEAD@{1} # or: git reset --hard <sha>
# Recover a deleted branch
git branch deleted-branch # accidentally deleted
git reflog # find last commit of that branch
git checkout -b deleted-branch HEAD@{3}
# Recover commits after a bad rebase
git rebase main # rebase went wrong
git reflog # find HEAD before rebase started
git reset --hard ORIG_HEAD # Git stores pre-rebase position in ORIG_HEADConfiguring Reflog Expiry
# Default expiry values
git config gc.reflogExpire # 90 days (normal entries)
git config gc.reflogExpireUnreachable # 30 days (unreachable entries)
# Extend reflog retention (e.g., 1 year)
git config --global gc.reflogExpire 365.days
git config --global gc.reflogExpireUnreachable 90.days
# Never expire reflog (use with caution on large repos)
git config --global gc.reflogExpire neverCherry-Pick Strategies
git cherry-pick applies the changes introduced by one or more commits onto the current branch, creating new commits with the same diff but different SHAs. It is ideal for backporting fixes to release branches, selectively applying commits without merging entire branches, or rescuing commits from abandoned branches.
# Cherry-pick a single commit
git cherry-pick a1b2c3d
# Cherry-pick a range of commits (inclusive)
git cherry-pick a1b2c3d..f0e1d2c # from after a1b2c3d to f0e1d2c
git cherry-pick a1b2c3d^..f0e1d2c # include a1b2c3d itself
# Cherry-pick multiple specific commits
git cherry-pick a1b2c3d e4f5g6h i7j8k9l
# Cherry-pick without committing (stage only)
git cherry-pick --no-commit a1b2c3d
git cherry-pick -n a1b2c3d
# Cherry-pick with a custom commit message
git cherry-pick a1b2c3d
git commit --amend -m "backport: fix null pointer in auth (cherry-picked from a1b2c3d)"
# Resolving cherry-pick conflicts
git cherry-pick a1b2c3d
# CONFLICT: resolve files
git add .
git cherry-pick --continue
# Abort cherry-pick
git cherry-pick --abort
# Backport pattern: applying a fix to multiple release branches
git checkout release/2.1
git cherry-pick a1b2c3d # apply the fix
git push origin release/2.1
git checkout release/2.0
git cherry-pick a1b2c3d
git push origin release/2.0Cherry-Pick vs Merge vs Rebase
| Operation | Use Case | New SHAs? | History Impact |
|---|---|---|---|
cherry-pick | Apply specific commits across branches | Yes | Adds commits to current branch only |
merge | Integrate entire branch history | No (adds merge commit) | Preserves full branch topology |
rebase | Replay branch commits on new base | Yes | Linear history, rewrites SHAs |
Git Bisect: Binary Search for Bugs
git bisect automates the process of finding which commit introduced a regression. Instead of manually checking commits one by one (O(n)), bisect uses binary search to find the culprit in O(log n) steps. For a repository with 1,000 commits between good and bad, that means at most 10 checks instead of 1,000.
# Manual bisect session
git bisect start
git bisect bad HEAD # current commit is broken
git bisect good v2.0.0 # this release was good
# Git checks out a midpoint commit...
# Test the midpoint, then mark it:
git bisect good # this commit is fine
git bisect bad # this commit is broken
# Git continues halving the range...
# Eventually: "a1b2c3d is the first bad commit"
# End the session (restore HEAD to original position)
git bisect reset
# ── Automated bisect with a test script ──
# Script must exit 0 = good, 1-127 = bad, 125 = skip
cat > /tmp/test-bug.sh << 'EOF'
#!/bin/bash
npm run build 2>/dev/null && npm test -- --testPathPattern="auth" 2>/dev/null
EOF
chmod +x /tmp/test-bug.sh
git bisect start
git bisect bad HEAD
git bisect good v2.0.0
git bisect run /tmp/test-bug.sh
# Git runs the script at each midpoint automatically
# Skip untestable commits (build failures, etc.)
git bisect skip HEAD
# View bisect log
git bisect log
# Replay a bisect from a saved log
git bisect log > bisect.log
git bisect replay bisect.logAdvanced Merge Strategies
Git supports multiple merge strategies and strategy options that control how conflicts are resolved. Choosing the right strategy can save significant manual conflict resolution effort.
# Specify a strategy: git merge -s <strategy>
git merge -s ort feature/auth # default (Ostensibly Recursive's Twin)
git merge -s recursive feature/auth # older default, same concept
git merge -s resolve feature/auth # two-way merge (simpler, faster)
git merge -s octopus branch1 branch2 branch3 # merge multiple branches at once
git merge -s ours feature/deprecated # keep our version entirely (ignores theirs)
git merge -s subtree contrib/plugin # for subtree merges
# Strategy OPTIONS: -X <option>
git merge -X ours feature/auth # auto-resolve conflicts: prefer our version
git merge -X theirs feature/auth # auto-resolve conflicts: prefer their version
git merge -X patience feature/auth # use patience diff algorithm (better for large files)
git merge -X diff-algorithm=histogram # histogram diff (often best for code)
git merge -X ignore-space-change # ignore whitespace-only changes in conflicts
git merge -X ignore-all-space # ignore all whitespace
git merge -X no-renames # do not detect renames during merge
# The 'ours' strategy vs 'ours' option
git merge -s ours feature/deprecated
# Creates a merge commit but discards ALL changes from feature/deprecated
# The result is identical to HEAD — useful to mark a branch as "merged" without taking its changes
git merge -X ours feature/auth
# Uses recursive/ort strategy but auto-resolves all conflicts by keeping OUR version
# Changes from feature/auth that don't conflict ARE still included
# Octopus merge: integrate multiple branches in a single commit
git merge branch-a branch-b branch-c
# Requires no conflicts; good for topic branches with no overlapping files| Strategy / Option | Description | Best For |
|---|---|---|
ort (default) | Improved recursive 3-way merge | General purpose, most scenarios |
octopus | Merge 3+ branches at once | Non-conflicting topic branches |
ours (strategy) | Discard all incoming changes | Mark deprecated branches as merged |
-X ours | Auto-resolve conflicts with our version | When our branch is authoritative |
-X theirs | Auto-resolve conflicts with their version | Accepting upstream changes wholesale |
-X patience | Better diff for large moved blocks | Large refactors, moved functions |
Git Hooks: Automating Quality Gates
Git hooks are scripts in .git/hooks/ that Git executes at specific lifecycle points. They can be written in any language (bash, Python, Node.js) and enforce policies locally before code reaches the server. Unlike CI/CD, hooks run instantly on the developer's machine. Use Husky or Lefthook to manage hooks as part of your repository so they are shared across the team.
Client-Side Hooks
# .git/hooks/pre-commit — runs before a commit is created
#!/bin/bash
set -e
echo "Running pre-commit checks..."
# 1. Run linter
npx eslint --ext .ts,.tsx src/ || {
echo "ESLint failed. Fix errors before committing."
exit 1
}
# 2. Run type check
npx tsc --noEmit || {
echo "TypeScript errors found. Fix before committing."
exit 1
}
# 3. Run tests related to staged files
STAGED=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(ts|tsx)$' || true)
if [ -n "$STAGED" ]; then
npx jest --findRelatedTests $STAGED --passWithNoTests
fi
echo "All pre-commit checks passed!"
exit 0# .git/hooks/commit-msg — validates commit message format
#!/bin/bash
# Enforce Conventional Commits: type(scope): description
COMMIT_MSG_FILE=$1
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
PATTERN="^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)(\([a-z0-9-]+\))?(!)?: .{1,72}"
if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
echo "ERROR: Commit message does not follow Conventional Commits format."
echo "Expected: type(scope): description"
echo "Examples:"
echo " feat(auth): add OAuth2 login"
echo " fix: correct null check in payment service"
echo " docs: update API documentation"
echo ""
echo "Your message: $COMMIT_MSG"
exit 1
fi
exit 0# .git/hooks/pre-push — runs before git push
#!/bin/bash
set -e
REMOTE=$1
URL=$2
echo "Running pre-push checks against $REMOTE ($URL)..."
# Block pushing to main directly
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "$CURRENT_BRANCH" = "main" ] && [ "$REMOTE" = "origin" ]; then
echo "ERROR: Direct pushes to main are not allowed."
echo "Please create a feature branch and open a pull request."
exit 1
fi
# Run full test suite before pushing
npm test -- --ci --coverage || {
echo "Tests failed. Fix before pushing."
exit 1
}
echo "Pre-push checks passed!"
exit 0Managing Hooks with Husky
# Install Husky
npm install --save-dev husky
npx husky init
# .husky/pre-commit
npx lint-staged
# .husky/commit-msg
npx commitlint --edit $1
# package.json: configure lint-staged
{
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md}": ["prettier --write"]
}
}
# Install commitlint for conventional commits
npm install --save-dev @commitlint/cli @commitlint/config-conventional
# commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [2, 'always', [
'feat', 'fix', 'docs', 'style', 'refactor',
'perf', 'test', 'chore', 'ci', 'build', 'revert'
]],
'subject-max-length': [2, 'always', 72]
}
};Server-Side Hooks
# Server-side hooks (in bare repository on server)
# These run on the remote/server and cannot be bypassed by clients
# pre-receive: runs before any refs are updated
# Receives: old-sha new-sha refname (one per line on stdin)
#!/bin/bash
while read oldrev newrev refname; do
# Block force pushes to main
if [ "$refname" = "refs/heads/main" ]; then
if git merge-base --is-ancestor "$newrev" "$oldrev" 2>/dev/null; then
echo "ERROR: Force push to main is not allowed."
exit 1
fi
fi
done
# post-receive: runs after refs are updated (for notifications)
#!/bin/bash
# Send Slack notification, trigger CI, update documentation, etc.
while read oldrev newrev refname; do
branch=${refname#refs/heads/}
curl -s -X POST https://hooks.slack.com/... \
-d "{"text": "Branch $branch updated"}"
doneSubmodules vs Subtrees
Both submodules and subtrees let you embed one Git repository inside another. They have fundamentally different tradeoffs in complexity, contributor experience, and update workflows.
Git Submodules
# Add a submodule
git submodule add https://github.com/org/lib.git vendor/lib
# After cloning a repo with submodules
git clone --recurse-submodules https://github.com/org/project.git
# or for existing clones:
git submodule update --init --recursive
# Update a submodule to its latest remote commit
cd vendor/lib
git checkout main
git pull origin main
cd ../..
git add vendor/lib
git commit -m "chore: update lib submodule to latest"
# Update all submodules at once
git submodule update --remote --merge
# Remove a submodule
git submodule deinit vendor/lib
git rm vendor/lib
rm -rf .git/modules/vendor/lib
# .gitmodules (auto-generated)
[submodule "vendor/lib"]
path = vendor/lib
url = https://github.com/org/lib.git
branch = mainGit Subtree
# Add a subtree (copies content into your repo)
git subtree add --prefix=vendor/lib https://github.com/org/lib.git main --squash
# Update subtree to latest upstream
git subtree pull --prefix=vendor/lib https://github.com/org/lib.git main --squash
# Push local changes back to upstream (contribute upstream)
git subtree push --prefix=vendor/lib https://github.com/org/lib.git my-feature-branch
# Add remote for easier management
git remote add lib https://github.com/org/lib.git
git subtree add --prefix=vendor/lib lib main --squash
git subtree pull --prefix=vendor/lib lib main --squash| Aspect | Submodule | Subtree |
|---|---|---|
| Storage | SHA pointer only | Full copy in repo |
| Clone experience | Needs --recurse-submodules | Normal git clone |
| Version pinning | Exact SHA (explicit) | Manual pull required |
| Contributing upstream | Work inside submodule dir | git subtree push |
| Complexity | High (two repos to manage) | Lower (one repo) |
| Best for | Strict versioned dependencies | Simpler contributor workflow |
Git Worktrees for Parallel Development
Git worktrees let you check out multiple branches simultaneously in separate directories — all sharing the same .git folder and object store. This is the clean solution to "I need to review a PR but I'm in the middle of something", with no stashing, no branch switching, and no wasted disk space for object duplication.
# Add a new worktree for an existing branch
git worktree add ../project-hotfix hotfix/critical-fix
# Add a worktree and create a new branch
git worktree add -b feature/new-dashboard ../project-dashboard
# Add a worktree for a remote branch
git worktree add ../project-v2 origin/release/v2.0
# List all worktrees
git worktree list
# /home/user/project a1b2c3d [main]
# /home/user/project-hotfix d4e5f6g [hotfix/critical-fix]
# /home/user/project-dashboard f7g8h9i [feature/new-dashboard]
# Work in worktrees independently
cd ../project-hotfix
git commit -m "fix: resolve null pointer in payment"
git push origin hotfix/critical-fix
cd ../project-dashboard
# Continue work without affecting hotfix worktree
# Remove a worktree when done
git worktree remove ../project-hotfix
# Prune stale worktree metadata (if directory was manually deleted)
git worktree prune
# Lock a worktree (prevent accidental removal)
git worktree lock ../project-hotfix --reason "CI is running tests here"
git worktree unlock ../project-hotfixGit Stash Advanced Usage
git stash saves dirty working directory state to a stack, letting you switch context quickly. Beyond the basic stash and stash pop, there are many powerful options for partial stashes, naming, and branch creation.
# Basic stash (tracked files only)
git stash
# Include untracked files
git stash --include-untracked
git stash -u
# Include untracked AND ignored files
git stash --all
# Stash with a descriptive message
git stash push -m "WIP: half-done auth refactor"
# Stash only specific files (partial stash)
git stash push src/auth/login.ts src/auth/types.ts
git stash push -m "auth changes only" -- src/auth/
# List all stash entries
git stash list
# stash@{0}: WIP on main: a1b2c3d feat: add user profile
# stash@{1}: WIP: half-done auth refactor on feature/auth
# stash@{2}: On main: auth changes only
# Show stash contents
git stash show stash@{1} # stat view
git stash show -p stash@{1} # full diff
# Apply without removing from stack
git stash apply stash@{1}
# Apply and remove from stack
git stash pop stash@{1}
# Drop a specific stash
git stash drop stash@{2}
# Clear all stash entries
git stash clear
# Create a branch from stash (apply + new branch)
git stash branch feature/auth-work stash@{0}
# Creates branch, checks it out, applies stash, drops it
# Interactive patch stashing (choose hunks)
git stash push --patch
git stash push -pSigning Commits with GPG
GPG-signed commits cryptographically verify author identity. GitHub, GitLab, and Bitbucket display a "Verified" badge on signed commits. This is particularly important for regulated industries, open-source projects, and supply-chain security.
# 1. Generate a GPG key (if you don't have one)
gpg --full-generate-key
# Choose RSA 4096 bit, no expiry for personal use
# 2. List your keys to get the key ID
gpg --list-secret-keys --keyid-format=long
# sec rsa4096/A1B2C3D4E5F6G7H8 2024-01-01 [SC]
# LONGFINGERPRINTHERE
# uid Alice Smith <alice@example.com>
# 3. Configure Git to use your key
git config --global user.signingkey A1B2C3D4E5F6G7H8
# 4. Enable automatic commit signing
git config --global commit.gpgsign true
# 5. Enable tag signing
git config --global tag.gpgsign true
# 6. Sign a single commit manually
git commit -S -m "feat: add payment module"
# 7. Sign an annotated tag
git tag -s v1.0.0 -m "Release 1.0.0"
# 8. Verify a signed commit
git verify-commit HEAD
git log --show-signature -1
# 9. Export public key to add to GitHub/GitLab
gpg --armor --export A1B2C3D4E5F6G7H8
# Copy the output → GitHub Settings → SSH and GPG Keys
# SSH signing (Git 2.34+, simpler alternative to GPG)
git config --global gpg.format ssh
git config --global user.signingkey "~/.ssh/id_ed25519.pub"
git config --global commit.gpgsign trueGit LFS for Large Files
Git LFS (Large File Storage) stores large binary files outside the Git object store, replacing them with small pointer files in the repository. Without LFS, every version of every large binary is stored permanently in .git/objects, bloating clone times and repo size. With LFS, only the pointer is cloned; actual content is downloaded on demand.
# 1. Install Git LFS
git lfs install
# 2. Track file patterns (adds rules to .gitattributes)
git lfs track "*.psd"
git lfs track "*.png"
git lfs track "*.mp4"
git lfs track "*.zip"
git lfs track "models/**"
git lfs track "*.bin" "*.h5" "*.pkl"
# IMPORTANT: commit .gitattributes first
git add .gitattributes
git commit -m "chore: configure Git LFS tracking"
# 3. Use git normally — LFS handles the rest
git add assets/logo.psd
git commit -m "design: update brand logo"
git push origin main
# 4. Inspect LFS-tracked files
git lfs ls-files # list LFS-tracked files in current revision
git lfs status # show LFS status of working directory
git lfs env # show LFS configuration
# 5. Check what .gitattributes patterns are configured
git lfs track
# 6. Download LFS files (if cloned without LFS)
git lfs pull
git lfs fetch --all # fetch all LFS versions (for mirrors)
# 7. Migrate existing large files to LFS
git lfs migrate import --include="*.psd,*.png" --everything
git push --force --all
git push --force --tags
# 8. Show LFS storage usage
git lfs du
# .gitattributes (auto-generated by git lfs track)
*.psd filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -textMonorepo Strategies with Git
A monorepo stores multiple projects (packages, services, apps) in a single Git repository. Companies like Google, Meta, Microsoft, and Airbnb use monorepos. The trade-offs involve tooling complexity vs atomic cross-project changes and unified dependency management.
Sparse Checkout for Large Monorepos
# Sparse checkout: check out only the packages you need
git clone --no-checkout https://github.com/org/monorepo.git
cd monorepo
git sparse-checkout init --cone
git sparse-checkout set packages/frontend packages/shared
git checkout main
# Add more packages as needed
git sparse-checkout add packages/api
# List current sparse-checkout paths
git sparse-checkout list
# Disable sparse-checkout (full checkout)
git sparse-checkout disablePartial Clone for Bandwidth Efficiency
# Blobless clone: download commits and trees but not blobs
# Blobs are fetched lazily when accessed
git clone --filter=blob:none https://github.com/org/monorepo.git
# Treeless clone: even more aggressive (commits only)
git clone --filter=tree:0 https://github.com/org/monorepo.git
# Combine partial clone with sparse checkout
git clone --filter=blob:none --no-checkout https://github.com/org/monorepo.git
cd monorepo
git sparse-checkout init --cone
git sparse-checkout set packages/frontend
git checkout mainMonorepo Tooling
# npm/pnpm/yarn workspaces — package.json
{
"name": "monorepo-root",
"private": true,
"workspaces": [
"packages/*",
"apps/*"
]
}
# pnpm workspace (pnpm-workspace.yaml)
packages:
- 'packages/*'
- 'apps/*'
# Nx monorepo — only rebuild affected projects
npx nx affected:build --base=main
npx nx affected:test --base=HEAD~1
# Turborepo — pipeline-aware caching
# turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"]
}
}
}
turbo run build --filter=./packages/frontend
# Changesets — versioning and changelogs in monorepos
npx changeset # create a changeset
npx changeset version # bump versions
npx changeset publish # publish to npmGit Workflow Comparison: Gitflow vs Trunk-Based vs GitHub Flow
Choosing the right branching workflow has a larger impact on team velocity than almost any other engineering process decision. Here is a comprehensive comparison of the three most widely used strategies.
| Aspect | Gitflow | Trunk-Based Dev | GitHub Flow |
|---|---|---|---|
| Main branches | main + develop | main (trunk) only | main only |
| Feature branch lifetime | Days to weeks | Hours to 2 days max | Days to 1 week |
| Release cadence | Scheduled (weekly/monthly) | Continuous (multiple/day) | On merge (continuous) |
| Feature flags needed? | Optional | Yes (required) | Sometimes |
| CI/CD requirement | Moderate | Very high (must be reliable) | Moderate–High |
| Multiple prod versions? | Yes (release branches) | No (single trunk) | No |
| Team size | Medium–Large | Any (Google-scale to startup) | Small–Medium |
| Merge conflicts | High (long-lived branches) | Low (short branches) | Low–Moderate |
| Best for | SaaS with enterprise tiers, apps with versioned APIs | Google/Netflix-style continuous deployment | Most startups and modern SaaS |
| Key risk | Branch divergence, integration hell | Requires mature CI/CD and feature flags | Incomplete features can ship without flags |
GitHub Flow in Practice
# GitHub Flow: Simple and effective for most teams
# Rule: main is always deployable
# 1. Create a descriptive feature branch
git checkout main
git pull origin main
git checkout -b feat/user-authentication
# 2. Make small, focused commits
git commit -m "feat: add login form component"
git commit -m "feat: implement JWT token validation"
git commit -m "test: add auth unit tests"
# 3. Push early and open a PR
git push -u origin feat/user-authentication
# Open PR on GitHub → CI runs → code review
# 4. Merge when approved (use squash or merge commit)
# Via GitHub UI: "Squash and merge" or "Create a merge commit"
# 5. Delete the branch and deploy
git checkout main
git pull origin main
git branch -d feat/user-authenticationTrunk-Based Development in Practice
# Trunk-Based Development: high-frequency, small commits
# Rule: no branch lives more than 2 days
# 1. Pull trunk, create short-lived branch
git checkout main && git pull origin main
git checkout -b feat/add-search-icon # tiny change
# 2. Commit and push within hours
git add src/components/SearchIcon.tsx
git commit -m "feat: add search icon component"
git push origin feat/add-search-icon
# CI passes → auto-merge to main
# 3. For larger features: use feature flags
// config/features.ts
export const features = {
newSearchBar: process.env.NEXT_PUBLIC_FF_SEARCH_BAR === 'true',
};
// In component:
import { features } from '@/config/features';
{features.newSearchBar && <NewSearchBar />}
# 4. Commit-to-trunk approach (senior devs)
git checkout main
# ... small change ...
git add src/utils/format.ts
git commit -m "refactor: extract formatDate utility"
git push origin main # only if CI is green (pre-push hook)Frequently Asked Questions
What is the difference between git reset --soft, --mixed, and --hard?
# --soft: moves HEAD only; staging and working tree unchanged
git reset --soft HEAD~1
# Commit is undone; all changes remain staged (green in git status)
# Use to: undo last commit but keep changes ready to re-commit
# --mixed (default): moves HEAD + resets staging; working tree unchanged
git reset HEAD~1
git reset --mixed HEAD~1
# Commit is undone; changes are unstaged (red in git status)
# Use to: unstage files, undo commits while keeping working changes
# --hard: moves HEAD + resets staging + resets working tree
git reset --hard HEAD~1
# DANGER: commit undone AND all working changes are discarded
# Use to: completely discard commits (recoverable via reflog within 90 days)How do I undo a pushed commit without rewriting history?
# git revert: creates a NEW commit that undoes the changes
# Safe for shared branches — does not rewrite history
# Revert the last commit
git revert HEAD
# Revert a specific commit
git revert a1b2c3d
# Revert a range of commits (most recent first)
git revert HEAD~3..HEAD
# Revert without auto-committing (stage the undo, review first)
git revert --no-commit HEAD
git revert -n HEAD
# Revert a merge commit (specify which parent to follow)
git revert -m 1 <merge-commit-sha>
# -m 1: keep the mainline (parent 1)
# -m 2: keep the feature branch (parent 2)How do I clean up stale remote branches?
# Prune deleted remote branches from local tracking refs
git fetch --prune
git fetch -p
# Set as default (fetch always prunes)
git config --global fetch.prune true
# List branches already merged into main
git branch -r --merged main | grep -v "main|HEAD"
# Delete a remote branch
git push origin --delete feature/old-feature
# Delete local branch
git branch -d feature/old-feature # safe: fails if unmerged
git branch -D feature/old-feature # force delete
# Clean up all local branches merged into main
git branch --merged main | grep -v "^* |main" | xargs git branch -dHow do I write better commit messages?
Follow the Conventional Commits specification and the 7 rules of great commit messages:
# Conventional Commits format:
# type(scope): description
#
# [optional body]
#
# [optional footer(s)]
# Good examples:
feat(auth): add OAuth2 login with Google
fix(payment): resolve null pointer when card expires
docs(api): update rate limiting documentation
refactor(cache): extract Redis client to dedicated module
perf(images): lazy-load below-fold product images
test(auth): add integration tests for JWT refresh
chore(deps): upgrade Next.js to 15.1.0
ci(actions): add automated security scanning workflow
# 7 Rules of Great Commit Messages:
# 1. Separate subject from body with a blank line
# 2. Limit subject to 72 characters
# 3. Use imperative mood: "add feature" not "added feature"
# 4. Do not end subject with a period
# 5. Wrap body at 100 characters
# 6. Use body to explain WHAT and WHY, not HOW
# 7. Reference issues in footer: "Closes #123"
# Multi-line example:
git commit -m "fix(auth): prevent session fixation on login
Before this fix, the session ID was not regenerated after successful
authentication, leaving users vulnerable to session fixation attacks
where an attacker pre-sets a known session ID.
This commit regenerates the session ID immediately after the user
authenticates, following OWASP recommendation.
Closes #247
Security: CVE-2024-XXXX"