Git is far more than add, commit, and push. Mastering Git internals, branching strategies, interactive rebase, hooks, and CI/CD integration transforms your development workflow. This guide covers 13 essential Git topics with practical examples for professional developers.
Key Takeaways
- Git stores everything as objects (blobs, trees, commits) identified by SHA-1 hashes, not as file diffs.
- Choose Git Flow for complex releases, GitHub Flow for simplicity, or trunk-based for high-velocity teams.
- Interactive rebase rewrites history cleanly β use squash, fixup, reword, and edit to craft meaningful commits.
- Git bisect performs binary search to find the exact commit that introduced a bug.
- Git hooks (pre-commit, commit-msg, pre-push) automate linting, testing, and commit message validation.
- Sign commits with GPG or SSH keys to verify author identity and establish trust in your codebase.
Git Internals
Git is a content-addressable filesystem. Every file, directory, and commit is stored as an object identified by a SHA-1 hash. Understanding this model explains why Git is so fast and how operations like branch, merge, and rebase actually work under the hood.
# Explore Git objects
git cat-file -t HEAD # commit
git cat-file -p HEAD # show commit content
git cat-file -p HEAD^{tree} # show tree (directory listing)
# Every object is stored in .git/objects/
# Object types: blob (file), tree (dir), commit, tag
echo "hello" | git hash-object --stdin # compute SHA-1
git rev-parse HEAD # full SHA of HEAD
git rev-parse --short HEAD # short SHA
# Pack files compress objects for efficiency
git count-objects -vH # show object stats
git gc # garbage collect and pack
git verify-pack -v .git/objects/pack/*.idx | headThe Object Model
When you run git add, Git creates a blob object for each file. When you commit, Git creates a tree object for each directory and a commit object pointing to the root tree. Branches and tags are simply files containing a commit SHA, making them lightweight references rather than copies of data.
Branching Strategies
A branching strategy defines how your team creates, merges, and releases code. The right choice depends on team size, release cadence, and deployment model. Three dominant strategies have emerged in the industry.
# Git Flow: structured releases
git flow init # initialize git-flow
git flow feature start user-auth # create feature branch
git flow feature finish user-auth # merge to develop
git flow release start 2.0.0 # create release branch
git flow release finish 2.0.0 # merge to main + develop
# GitHub Flow: simple continuous delivery
git checkout -b feat/add-search # branch from main
git push -u origin feat/add-search # push and open PR
# PR review -> merge -> auto deploy
# Trunk-based: short-lived branches
git checkout -b fix/typo main
git commit -am "fix: correct typo in header"
git push origin fix/typo # merge within hoursBranch Naming Conventions
Consistent branch names improve readability and automation. Common patterns include type/description (feat/user-auth, fix/login-bug), type/ticket-id (feat/JIRA-123), and owner/description (alice/refactor-api). Many teams prefix with feat/, fix/, hotfix/, chore/, or docs/ to categorize branches automatically.
Interactive Rebase
Interactive rebase lets you rewrite commit history before sharing it. You can squash multiple commits into one, reword commit messages, reorder commits, or edit a commit to split it. This keeps your history clean and meaningful.
# Interactive rebase last 4 commits
git rebase -i HEAD~4
# Editor opens with:
# pick a1b2c3d Add user model
# pick e4f5g6h Fix typo in model -> fixup
# pick i7j8k9l Add user controller -> squash
# pick m0n1o2p Update error messages -> reword
# Commands:
# pick = keep commit as-is
# squash = combine with previous, edit message
# fixup = combine with previous, discard message
# reword = keep commit, edit message
# edit = pause to amend the commit
# drop = remove commit entirely
# Abort if something goes wrong
git rebase --abortAutosquash Workflow
Use git commit --fixup=<sha> to create fixup commits, then git rebase -i --autosquash automatically moves them next to the target commit with the fixup command. This workflow lets you make corrections as you work and clean up later without manual reordering.
Cherry-Pick and Bisect
Cherry-pick applies specific commits from one branch to another without merging the entire branch. Bisect uses binary search to find the commit that introduced a bug. Together, they are essential debugging and selective-merge tools.
# Cherry-pick a single commit to current branch
git cherry-pick abc1234
# Cherry-pick range without committing
git cherry-pick abc1234..def5678 --no-commit
git commit -m "feat: backport fixes from develop"
# Bisect: find the bug-introducing commit
git bisect start
git bisect bad # current commit is broken
git bisect good v1.0.0 # this tag was working
# Git checks out midpoint, test it, then:
git bisect good # or git bisect bad
# Repeat until Git identifies the culprit
git bisect reset # return to original HEAD
# Automated bisect with a test script
git bisect start HEAD v1.0.0
git bisect run npm testAutomating Bisect
For maximum efficiency, write a small script that reproduces the bug and exits with non-zero on failure. Pass it to git bisect run and Git will perform the entire binary search unattended. With 1000 commits, bisect only needs about 10 tests to find the culprit.
Stash and Worktrees
Git stash temporarily shelves uncommitted changes so you can switch branches. Worktrees let you check out multiple branches simultaneously in separate directories, avoiding constant stashing and branch switching.
# Stash with a descriptive message
git stash push -m "WIP: auth refactor"
git stash list # list all stashes
git stash show -p stash@{0} # show stash diff
git stash pop # apply and remove
git stash apply stash@{1} # apply without removing
git stash drop stash@{2} # delete specific stash
# Stash including untracked files
git stash push -u -m "include new files"
# Worktrees: multiple branches simultaneously
git worktree add ../hotfix-branch hotfix/v2
git worktree add ../feature-branch feat/search
git worktree list # list all worktrees
git worktree remove ../hotfix-branchWorktree Use Cases
Common worktree scenarios include: reviewing a pull request while working on your own feature, running a long build on one branch while coding on another, comparing behavior between branches side-by-side, and maintaining a production hotfix branch that is always checked out and ready for emergency patches.
Creating a Branch from a Stash
If you stashed changes but then realize they deserve their own branch, use git stash branch new-branch-name stash@{0}. This creates a new branch from the commit where the stash was originally created, applies the stash, and drops it. It is the cleanest way to promote stashed work into a proper feature branch.
Git Hooks
Hooks are scripts that Git runs before or after certain events like commit, push, or merge. They enforce code quality standards, validate commit messages, run tests, and prevent bad code from entering the repository.
#!/bin/sh
# .husky/pre-commit
npx lint-staged
# .husky/commit-msg
npx --no -- commitlint --edit "\$1"
# package.json lint-staged config
# "lint-staged": {
# "*.{js,ts}": ["eslint --fix", "prettier --write"],
# "*.css": ["stylelint --fix"]
# }
# Setup husky in a project
npx husky init
echo "npx lint-staged" > .husky/pre-commit
echo "npx commitlint --edit \$1" > .husky/commit-msgHook Manager Comparison
Husky is the most popular hook manager with 30k+ GitHub stars and simple configuration via .husky/ directory. Lefthook is faster, written in Go, and supports parallel hook execution. simple-git-hooks is a zero-dependency alternative for projects that need minimal setup. All three store hooks in the repository so every contributor runs the same checks.
lint-staged Configuration
lint-staged runs linters only on staged files, keeping pre-commit hooks fast even in large repos. Configure it in package.json or a separate .lintstagedrc file. Map glob patterns to commands: JavaScript files to ESLint and Prettier, CSS to Stylelint, and Markdown to markdownlint. Failed checks abort the commit, preventing poorly formatted code from entering the repo.
Submodules and Subtrees
Submodules and subtrees let you include external repositories inside your project. Submodules maintain a pointer to a specific commit in another repo. Subtrees merge the external repo history directly into your project tree.
# Submodules: pointer to external repo commit
git submodule add https://github.com/lib/utils.git libs/utils
git submodule update --init --recursive
git submodule update --remote # pull latest
# Clone repo with submodules
git clone --recurse-submodules https://github.com/org/project
# Subtrees: merge external repo into your tree
git subtree add --prefix=libs/utils \
https://github.com/lib/utils.git main --squash
# Pull updates from subtree remote
git subtree pull --prefix=libs/utils \
https://github.com/lib/utils.git main --squash
# Push changes back to subtree remote
git subtree push --prefix=libs/utils \
https://github.com/lib/utils.git mainMonorepo vs Multi-repo
Monorepos store all projects in a single repository, simplifying dependency management and atomic cross-project changes. Multi-repos give teams autonomy and isolation. Submodules bridge both approaches: a parent repo references child repos at specific commits. Tools like Nx, Turborepo, and Lerna help manage monorepo builds and dependency graphs.
Merge vs Rebase
Merge creates a merge commit preserving branch history. Rebase replays commits on top of another branch creating a linear history. Each approach has tradeoffs in readability, conflict resolution, and collaboration safety.
# Merge: preserves branch history
git checkout main
git merge feature/auth --no-ff # always create merge commit
# Rebase: linear history
git checkout feature/auth
git rebase main # replay commits on main
git checkout main
git merge feature/auth # fast-forward merge
# Resolve conflicts during rebase
git rebase main
# CONFLICT in file.js
# Edit file.js to resolve
git add file.js
git rebase --continue
# Merge strategies
git merge -s ours legacy-branch # keep ours, discard theirs
git merge -X theirs feature # prefer theirs on conflictConflict Resolution Tips
Use git mergetool to open a visual diff tool. Configure merge.conflictstyle=diff3 to see the common ancestor alongside both sides of a conflict. After resolving, use git diff --check to verify no conflict markers remain. For rerere (reuse recorded resolution), enable it with git config rerere.enabled true to auto-resolve repeated conflicts.
Squash Merge Strategy
Squash merge combines all commits from a feature branch into a single commit on the target branch. This creates a clean main branch history where each commit represents a complete feature. The tradeoff is losing individual commit history from the feature branch. GitHub and GitLab offer squash merge as a PR merge option alongside regular merge and rebase merge.
Git Reflog
The reflog records every change to HEAD and branch tips, including commits, rebases, resets, and checkouts. It is your safety net for recovering lost commits, undoing bad rebases, and restoring deleted branches.
# View reflog (every HEAD movement)
git reflog
# abc1234 HEAD@{0}: commit: add feature
# def5678 HEAD@{1}: rebase: updating HEAD
# ghi9012 HEAD@{2}: checkout: moving to main
# Recover from bad rebase
git reset --hard HEAD@{2} # go back to before rebase
# Restore a deleted branch
git reflog | grep "checkout: moving from feature"
git checkout -b feature/restored abc1234
# Recover a dropped stash
git fsck --unreachable | grep commit
git stash apply <sha>
# Reflog for a specific branch
git reflog show feature/authCommon Recovery Patterns
The most frequent recovery scenarios are: undoing a bad rebase (reset to reflog entry before rebase), restoring a deleted branch (find last commit SHA in reflog), recovering an amended commit (the previous version exists in reflog), and retrieving a dropped stash (use git fsck to find unreachable commit objects).
Reflog vs git fsck
Reflog tracks HEAD and branch tip movements, so it finds commits that were recently referenced. Git fsck --unreachable finds all orphaned objects, including those never referenced by any branch. Use reflog first for recent recovery, and fsck as a last resort for objects that fell outside the reflog window.
Signing Commits
Signed commits prove that code was authored by a verified identity. GitHub and GitLab display a verified badge next to signed commits. You can sign with GPG keys or SSH keys (Git 2.34+).
# GPG signing setup
gpg --full-generate-key # generate GPG key
gpg --list-secret-keys --keyid-format=long
git config --global user.signingkey ABC123DEF456
git config --global commit.gpgsign true
# SSH signing (Git 2.34+)
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
# Sign a single commit
git commit -S -m "feat: signed commit"
# Verify signatures
git log --show-signature -1
git verify-commit HEAD
git verify-tag v1.0.0Vigilant Mode
GitHub offers vigilant mode which marks unsigned commits as unverified, making it clear when a commit lacks a valid signature. Enable it in your GitHub settings under SSH and GPG keys. Combined with branch protection rules requiring signed commits, this creates a strong chain of trust for your codebase.
Signing Tags for Releases
Use git tag -s v1.0.0 to create a signed annotated tag. Signed tags verify that a release was created by an authorized person. CI pipelines can verify tag signatures before building and deploying. This is especially important for open source projects where anyone can submit pull requests but only maintainers should create releases.
Large File Storage
Git LFS replaces large files (images, videos, binaries, datasets) with lightweight pointer files in your repository, storing the actual file content on a remote server. This keeps your repo small and clone times fast.
# Install and initialize Git LFS
git lfs install
# Track file patterns
git lfs track "*.psd"
git lfs track "*.zip"
git lfs track "datasets/**"
# .gitattributes is updated automatically
# *.psd filter=lfs diff=lfs merge=lfs -text
git add .gitattributes
git add assets/design.psd
git commit -m "feat: add design files with LFS"
# Check LFS status
git lfs ls-files # list tracked files
git lfs status # show pending changes
git lfs env # show LFS config
# Migrate existing files to LFS
git lfs migrate import --include="*.psd"LFS Best Practices
Add .gitattributes to your repo before tracking files with LFS. Use git lfs migrate to convert existing large files. Set up LFS locking for binary files that cannot be merged (design files, compiled assets). Monitor your LFS storage usage, as most hosts charge for bandwidth and storage beyond free tiers.
Advanced Log and Diff
Git log and diff have powerful formatting and filtering options. Pretty formats create custom output for changelogs and reports. Diff algorithms like patience and histogram produce cleaner diffs for complex changes.
# Pretty log formats
git log --oneline --graph --all --decorate
git log --pretty=format:"%h %an %ar %s" -10
git log --since="2 weeks ago" --author="Alice"
git log --grep="fix:" --oneline
# Diff algorithms for cleaner output
git diff --patience file.js # better for moved blocks
git diff --histogram # improved patience
git diff --word-diff # inline word changes
# Find who changed a line
git blame -L 10,20 src/app.js
git log -p -S "functionName" # pickaxe search
# Shortlog for changelogs
git shortlog -sn --no-merges # commit count by author
git log --format="%s" v1.0..v2.0 # messages between tagsUseful Git Aliases
Create aliases for frequently used log and diff commands. Add them to your global .gitconfig under the [alias] section. Popular aliases include lg for a decorated graph log, last for showing the last commit, unstage for resetting staged files, and amend for amending the last commit without editing the message.
Repository Statistics
Use git shortlog -sn to rank contributors by commit count. Use git log --format="%ae" | sort | uniq -c | sort -rn to list contributors by email. The git diff --stat command shows a summary of changed files. For detailed contribution analytics, tools like git-fame and gitstats generate comprehensive reports with charts and timelines.
CI/CD Integration
Git integrates tightly with CI/CD pipelines. Conventional commits enable automated versioning with semantic release. GitHub Actions workflows trigger on push, pull request, or tag events to build, test, and deploy automatically.
# .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm test
# Conventional commits + semantic release
# feat: -> minor, fix: -> patch, BREAKING CHANGE: -> major
npm install -D semantic-release
npx semantic-release # auto version + changelogBranch Protection Rules
Configure branch protection on main to require pull request reviews, passing CI checks, signed commits, and linear history. This prevents direct pushes to main, ensures code review, and maintains a clean commit history. GitHub, GitLab, and Bitbucket all support these rules with varying granularity.
Automated Changelog Generation
Tools like conventional-changelog and release-please parse conventional commit messages to generate formatted changelogs. Each release section groups changes by type: features, bug fixes, breaking changes, and documentation updates. This eliminates manual changelog maintenance and ensures every merged PR is documented.
Frequently Asked Questions
What is the difference between git merge and git rebase?
Merge creates a merge commit that combines two branches, preserving full history. Rebase replays your commits on top of the target branch, creating a linear history. Use merge for shared branches, rebase for local cleanup.
How do I undo a git rebase?
Use git reflog to find the commit before the rebase, then git reset --hard to that commit. The reflog records every HEAD change, so you can always recover from a bad rebase.
Should I use Git Flow or trunk-based development?
Use Git Flow for teams with scheduled releases and multiple environments. Use trunk-based development for teams practicing continuous deployment with feature flags and short-lived branches.
How do I sign commits with SSH keys?
Configure git with gpg.format=ssh and user.signingkey pointing to your SSH public key. Then use git commit -S to sign. Git 2.34+ supports SSH signing natively.
What is git bisect and when should I use it?
Git bisect performs a binary search through your commit history to find the exact commit that introduced a bug. Mark a known good commit and a bad commit, then bisect tests the midpoint until it isolates the offending commit.
How does Git LFS work?
Git LFS replaces large files with small pointer files in your repo. The actual file content is stored on a separate LFS server. Git LFS hooks intercept checkout and push operations to download and upload large files transparently.
What are the most useful git hooks?
Pre-commit runs linters and formatters before each commit. Commit-msg validates commit message format. Pre-push runs tests before pushing. Use Husky or lefthook to manage hooks across your team.
How do I recover a deleted branch?
Use git reflog to find the last commit on the deleted branch, then git checkout -b branch-name commit-sha to recreate it. Reflog entries persist for 90 days by default, giving you a generous recovery window.