Skip to main content

Git

This is a practical reference for day-to-day Git work. It assumes you know the basics (init, add, commit, push) and focuses on the patterns and workflows that make you productive.

Branching strategies

Every team needs a branching strategy. The three most common:

Trunk-based development

Everyone works on short-lived branches off main and merges back quickly (hours to a day or two). The trunk is always deployable.

AspectDetail
Branch lifetimeHours to 1--2 days
Merge targetAlways main
ReleasesDeploy from main (or tag releases)
Best forCI/CD-heavy teams, small to medium teams, SaaS products

GitHub Flow

Similar to trunk-based but with pull requests and slightly longer branch lifetimes:

  1. Branch from main
  2. Make commits
  3. Open a Pull Request
  4. Review, discuss, iterate
  5. Merge to main
  6. Deploy

Best for open-source and teams that rely on code review.

Git Flow

Uses main, develop, feature/*, release/*, and hotfix/* branches. Well-defined but heavyweight.

AspectDetail
Branch lifetimeDays to weeks
Merge targetdevelop for features, main for releases
ReleasesCut from develop into release/*, then merge to main
Best forVersioned software, teams that ship on a schedule

Which strategy to pick?

SituationRecommendation
Continuous deployment, small teamTrunk-based
Code review required, PRs are normalGitHub Flow
Versioned releases, multiple versions in productionGit Flow
UnsureStart with GitHub Flow -- it is the simplest that still gives you pull requests

Branching and merging

Three ways to integrate changes from one branch into another:

Merge (no fast-forward)

git checkout main
git merge --no-ff feature/login

Creates a merge commit that preserves the branch history. The default for pull requests on most platforms.

  • Preserves full history of when the branch was created and merged
  • Can make the log noisy with many merge commits
  • Safe -- never rewrites history

Rebase

git checkout feature/login
git rebase main

Replays your branch's commits on top of the target branch. Produces a linear history.

  • Clean, linear log -- no merge commits
  • Rewrites commit hashes (new SHAs)
  • Never rebase commits that have been pushed and shared -- this is the "golden rule of rebasing"

Squash merge

git checkout main
git merge --squash feature/login
git commit -m "Add login feature"

Combines all branch commits into a single commit on the target. Useful for keeping a clean main history when the branch had messy work-in-progress commits.

When to use which

StrategyUse when
MergeYou want to preserve full history; default for most PRs
RebaseYou want linear history and the branch is local (not shared)
SquashThe branch has many small/WIP commits that should be one logical change

Interactive rebase

Interactive rebase lets you edit, reorder, squash, or drop commits before pushing:

# Rebase the last 4 commits interactively
git rebase -i HEAD~4

This opens an editor:

pick a1b2c3d Add user model
pick e4f5g6h Fix typo in user model
pick i7j8k9l Add login endpoint
pick m0n1o2p WIP cleanup

Change the commands:

CommandEffect
pickKeep the commit as-is
rewordKeep changes, edit the commit message
squashMerge into the previous commit, combine messages
fixupMerge into the previous commit, discard this message
dropDelete the commit entirely
editPause rebase at this commit so you can amend it

Common use cases

Squash WIP commits before opening a PR:

pick a1b2c3d Add user model
fixup e4f5g6h Fix typo in user model
pick i7j8k9l Add login endpoint
fixup m0n1o2p WIP cleanup

Result: two clean commits instead of four messy ones.

Reword a commit message:

reword a1b2c3d Add user model
pick i7j8k9l Add login endpoint

The editor opens again to let you write a better message.

Warning: Only interactive-rebase commits that have not been pushed. Rebasing shared history forces collaborators to deal with diverged branches.

Staging and stashing

Partial staging

Stage specific hunks (sections) of a file instead of the whole file:

git add -p

Git shows each change hunk and asks:

Stage this hunk [y,n,q,a,d,s,e,?]?
OptionMeaning
yStage this hunk
nSkip this hunk
sSplit into smaller hunks
eManually edit the hunk
qQuit (stop staging)

This lets you make one commit for a bug fix and a separate commit for a new feature, even if both changes are in the same file.

Stashing

Save work-in-progress without committing:

# Stash with a description
git stash push -m "WIP: login form validation"

# List stashes
git stash list
# stash@{0}: On feature/login: WIP: login form validation
# stash@{1}: On main: experiment with new layout

# Apply the most recent stash (keeps it in the stash list)
git stash apply

# Apply and remove from the stash list
git stash pop

# Apply a specific stash
git stash apply stash@{1}

# Drop a specific stash
git stash drop stash@{0}

# Clear all stashes
git stash clear

Include untracked files:

git stash push -u -m "WIP: includes new files"

Stash only specific files:

git stash push -m "just the config" -- config.json .env.example

Undoing things

The three resets

# Soft: undo commit, keep changes staged
git reset --soft HEAD~1

# Mixed (default): undo commit, keep changes unstaged
git reset HEAD~1

# Hard: undo commit, discard all changes
git reset --hard HEAD~1
Reset modeCommitStaging areaWorking directory
--softUndonePreservedPreserved
--mixedUndoneClearedPreserved
--hardUndoneClearedCleared

Warning: --hard permanently destroys uncommitted changes. Use with caution.

Revert (safe undo for shared history)

# Create a new commit that undoes the given commit
git revert abc1234

Unlike reset, revert does not rewrite history. Safe for commits that have already been pushed.

Restore (undo file changes)

# Discard unstaged changes in a file
git restore path/to/file.js

# Unstage a file (keep changes in working directory)
git restore --staged path/to/file.js

# Restore a file from a specific commit
git restore --source=abc1234 path/to/file.js

Recovering from mistakes with reflog

The reflog records every HEAD movement. Even after a hard reset, you can recover:

# Show the reflog
git reflog
# abc1234 HEAD@{0}: reset: moving to HEAD~3
# def5678 HEAD@{1}: commit: Add important feature
# ghi9012 HEAD@{2}: commit: Fix login bug

# Recover by resetting to the reflog entry
git reset --hard def5678

Reflog entries are kept for 90 days by default. As long as a commit exists in the reflog, it is not lost.

Cherry-pick and bisect

Cherry-pick

Apply a specific commit from one branch to another:

# Apply commit abc1234 to the current branch
git cherry-pick abc1234

# Cherry-pick without committing (stage the changes)
git cherry-pick --no-commit abc1234

# Cherry-pick a range of commits
git cherry-pick abc1234..def5678

Use cases:

  • Backporting a bug fix from main to a release branch
  • Pulling a single commit from a feature branch that is not ready to merge

Bisect

Binary search through commit history to find which commit introduced a bug:

# Start bisect
git bisect start

# Mark the current commit as bad (has the bug)
git bisect bad

# Mark a known good commit (before the bug)
git bisect good v1.0

# Git checks out a commit halfway between good and bad
# Test it, then mark:
git bisect good # if the bug is not present
git bisect bad # if the bug is present

# Git narrows down and eventually outputs the offending commit

# When done
git bisect reset

Bisect tests O(log n) commits instead of O(n). For 1000 commits, you only test ~10.

Tags and releases

Lightweight vs annotated tags

# Lightweight tag (just a pointer)
git tag v1.0.0

# Annotated tag (recommended -- includes metadata)
git tag -a v1.0.0 -m "Release 1.0.0: initial public release"

Annotated tags store the tagger name, date, and a message. Always use annotated tags for releases.

Tag operations

# List tags
git tag

# List tags matching a pattern
git tag -l "v1.*"

# Tag a specific commit (not HEAD)
git tag -a v0.9.0 abc1234 -m "Beta release"

# Push a single tag
git push origin v1.0.0

# Push all tags
git push origin --tags

# Delete a local tag
git tag -d v1.0.0

# Delete a remote tag
git push origin --delete v1.0.0

Semantic versioning

Follow SemVer: MAJOR.MINOR.PATCH

Change typeVersion bumpExample
Breaking API changesMAJOR1.0.0 -> 2.0.0
New features (backward compatible)MINOR1.0.0 -> 1.1.0
Bug fixesPATCH1.0.0 -> 1.0.1
Pre-releaseSuffix2.0.0-beta.1

Useful aliases and .gitconfig

Add these to ~/.gitconfig (or run git config --global alias.<name> "<command>"):

[alias]
# Pretty log with graph
lg = log --oneline --graph --decorate --all

# Short status
s = status -sb

# Quick amend (add staged changes to the last commit, keep message)
amend = commit --amend --no-edit

# Undo the last commit (soft reset)
undo = reset --soft HEAD~1

# List branches sorted by last commit date
recent = branch --sort=-committerdate --format='%(committerdate:relative) %(refname:short)'

# Delete merged branches (except main/master/develop)
cleanup = "!git branch --merged | grep -vE '(main|master|develop|\\*)' | xargs -r git branch -d"

# Show what changed in the last commit
last = log -1 --stat

# Diff with word-level changes (great for prose)
wdiff = diff --word-diff

[pull]
# Always rebase on pull instead of merge
rebase = true

[push]
# Push the current branch to its upstream automatically
default = current

# Enable --force-with-lease safety by default
autoSetupRemote = true

[init]
# Default branch name for new repos
defaultBranch = main

[core]
# Global gitignore
excludesFile = ~/.gitignore_global

# Better diff algorithm
algorithm = histogram

Global .gitignore

Create ~/.gitignore_global for editor and OS files you never want to commit:

# Editors
.idea/
.vscode/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Env files
.env
.env.local

Working with remotes

Multiple remotes

Common when you fork a repo:

# Add the original repo as "upstream"
git remote add upstream https://github.com/original/repo.git

# Fetch from upstream
git fetch upstream

# Merge upstream changes into your local main
git checkout main
git merge upstream/main

# List all remotes
git remote -v

Force push safely

Never use git push --force on shared branches. Use --force-with-lease instead:

git push --force-with-lease

--force-with-lease refuses to push if the remote has commits you have not seen. It prevents accidentally overwriting a colleague's work.

Upstream tracking

# Push and set upstream in one command
git push -u origin feature/login

# See what each branch tracks
git branch -vv

Commit message conventions

Good commit messages are a gift to your future self and your teammates.

Conventional Commits

A widely adopted format:

<type>(<scope>): <description>

[optional body]

[optional footer(s)]

Types:

TypeWhen to use
featNew feature
fixBug fix
docsDocumentation only
styleFormatting, whitespace (not CSS)
refactorCode change that neither fixes a bug nor adds a feature
perfPerformance improvement
testAdding or fixing tests
choreBuild process, dependencies, tooling
ciCI/CD configuration

Examples:

feat(auth): add OAuth2 login with Google

fix(cart): prevent negative quantities on line items

docs: update deployment guide for Docker

refactor(api): extract validation into middleware

chore(deps): upgrade Spring Boot to 3.2.1

Good vs bad messages

BadGood
fixfix(search): return empty array instead of null for no results
update stuffrefactor(user): extract email validation into shared utility
WIPfeat(dashboard): add skeleton loading states for chart widgets
changesfix(i18n): use correct locale fallback for unsupported languages

Rules of thumb

  1. Subject line under 72 characters -- fits in git log --oneline
  2. Use imperative mood -- "Add feature" not "Added feature"
  3. Explain why, not what -- the diff shows what changed; the message explains why
  4. Reference issues -- fix(auth): handle expired tokens (#142)

Git is central to the deployment workflows covered in the AEM: Deployment & Cloud Manager, Java: Deploy to VPS, and JavaScript: Deploy to VPS guides.

Summary

TopicKey takeaway
Branching strategiesStart with GitHub Flow; use Git Flow only if you need versioned releases
Merge vs rebaseMerge for shared branches, rebase for local cleanup, squash for messy feature branches
Interactive rebaseClean up commits before pushing -- squash, reword, reorder
Staginggit add -p for partial staging; stash for context switching
Undoingreset for local, revert for shared, reflog for recovery
Cherry-pickMove individual commits between branches
BisectBinary search for the commit that introduced a bug
TagsUse annotated tags for releases; follow SemVer
AliasesInvest 10 minutes in .gitconfig -- save hours over a career
Force pushAlways --force-with-lease, never --force on shared branches
Commit messagesConventional Commits format; imperative mood; explain why, not what