Skip to main content
Git is the one tool you use in every project, and knowing it deeply saves you from costly mistakes and speeds up your daily work. This reference covers the decisions engineers face most often: when to merge vs. rebase, how to tag releases, how to collaborate safely on shared branches, and which commands to reach for in common situations. All examples use the command line — the most portable interface across environments.

Merge vs. Rebase

Both git merge and git rebase integrate changes from one branch into another, but they produce different history shapes and require different precautions.

How They Differ

git merge creates a new merge commit that has two parents — the tip of your current branch and the tip of the branch you are merging. The branch history is preserved exactly as it happened. You can see when a feature branch diverged, what happened on main in the meantime, and exactly where the two lines of work came together.
# Switch to the branch you want to merge INTO
git checkout main

# Merge the feature branch
git merge feature/my-feature

# The result: a new merge commit with two parents
git rebase takes the commits from your branch and replays them one by one on top of the target branch, as if you had started your work from the current tip of that branch. The result is a perfectly linear history — no merge commits, no fork-and-join shape — but the commit SHAs change because the commits are rewritten.
# Rebase your feature branch onto the current main
git checkout feature/my-feature
git rebase main

# Then fast-forward merge (no merge commit needed)
git checkout main
git merge feature/my-feature

Interactive Rebase

Interactive rebase (-i) lets you rewrite commits on your branch before sharing them — squash trivial “fix typo” commits, reorder commits for logical clarity, or split a large commit into smaller ones.
# Rebase the last 3 commits interactively
git rebase -i HEAD~3

# In the editor, change 'pick' to:
# 'squash' (s) - combine with previous commit
# 'reword' (r) - edit the commit message
# 'drop' (d) - remove the commit entirely

Pros and Cons

PropertyMergeRebase
Preserves historyYes — exact record of when branches divergedNo — rewrites commit SHAs
Linear historyNo — merge commits create a graphYes — clean, straight line
Safe on shared branchesYesNo — never rebase commits already pushed to a shared branch
Conflict handlingResolve once in the merge commitResolve once per replayed commit
Best forLong-lived shared branches, clear release historyFeature branches before opening a PR, local cleanup
Never rebase commits that you have already pushed to a shared remote branch. When you rebase, you rewrite commit history. Other developers who pulled your original commits will have divergent histories and will face painful conflict resolution. Rebase only commits that exist solely in your local repository or in a personal fork branch that nobody else has based work on.

When to Use Each

  • Use merge for integrating main into a long-lived feature branch when you want an honest record of the merge point, or when merging pull requests on GitHub/GitLab (the PR merge creates a documented integration point).
  • Use rebase to update a local feature branch with the latest main before opening a PR, and to clean up messy commit history (squash fixups) before the PR is reviewed.

Common Workflow

The feature branch workflow is the standard for teams using pull requests:
# 1. Always start from an up-to-date main
git checkout main
git pull

# 2. Create a descriptive feature branch
git checkout -b feature/add-flight-status-endpoint

# 3. Make changes and commit frequently
git add .
git commit -m "feat: add flight status endpoint with IATA code validation"

# 4. Stay current with main by rebasing (local only)
git fetch origin
git rebase origin/main

# 5. Push and open a pull request
git push -u origin feature/add-flight-status-endpoint

Commit Message Best Practices

Good commit messages make git log useful for debugging and git bisect effective for tracking regressions. Follow the Conventional Commits format:
<type>(<scope>): <short summary>

<optional body explaining WHY, not what>

<optional footer: BREAKING CHANGE, closes #123>
Common types: feat (new feature), fix (bug fix), refactor, test, docs, chore (build scripts, dependencies), perf.
# Good examples
git commit -m "feat(api): add rate limiting middleware using token bucket algorithm"
git commit -m "fix(db): handle nil pointer when flight record has no departure time"
git commit -m "perf(cache): prefetch top 100 routes on startup to reduce cold-start latency"

# Bad examples (avoid)
git commit -m "fix stuff"
git commit -m "wip"
git commit -m "changes"

Tags

Tags mark specific commits as significant — typically releases. Unlike branches, tags do not move as new commits are added.

Creating Tags

Git supports two tag types: Lightweight tag — just a named pointer to a commit. No metadata.
git tag v1.0-lw
Annotated tag — a full Git object with tagger name, email, date, and message. Recommended for releases because the metadata is preserved in the repository.
git tag -a v1.0 -m "Release v1.0: initial production deployment"

Semantic Versioning

Tag releases following Semantic Versioning: vMAJOR.MINOR.PATCH
  • PATCH (v1.0.1): Backwards-compatible bug fixes.
  • MINOR (v1.1.0): New backwards-compatible features.
  • MAJOR (v2.0.0): Breaking changes.

Tagging a Past Commit

# View commit history
git log --pretty=oneline

# a4c3f21 (HEAD -> main) feat: add webhook support
# 8b2d109 fix: correct timezone handling in departure times
# 3f9a0e2 feat: initial flight status endpoint

# Tag an earlier commit
git tag -a v0.9.0 8b2d109 -m "Pre-release: fix timezone bug"

Pushing and Managing Tags

git push does not push tags by default.
# Push a single tag
git push origin v1.0

# Push all local tags not yet on remote
git push origin --tags

# List all tags (optionally filtered)
git tag
git tag -l "v1.*"

# View tag details
git show v1.0

# Delete a local tag
git tag -d v1.0-lw

# Delete a remote tag
git push origin --delete v1.0-lw
# Or equivalently:
git push origin :refs/tags/v1.0-lw

Working with Remote Repos

Cloning

# Clone a public repository
git clone https://github.com/flightaware/piaware.git

# Clone a private repository (prompted for username and password/token)
git clone https://git.example.com/org/private-repo.git
# Username for 'https://git.example.com': your-username
# Password for '...': your-personal-access-token
For private repositories, use a personal access token (PAT) rather than your account password. Most platforms (GitHub, GitLab, Gitea) support PATs with fine-grained scope control.

Fetch vs. Pull

# git fetch: download remote changes but do NOT merge them
# Safe to run any time — does not touch your working tree
git fetch origin

# See what changed
git log HEAD..origin/main --oneline

# git pull: fetch + merge (or fetch + rebase if configured)
git pull
# Equivalent to: git fetch && git merge origin/main

# Pull with rebase instead of merge (preferred by many teams)
git pull --rebase
# Or set as default:
git config --global pull.rebase true

Dealing with Conflicts

# After a merge or rebase hits a conflict:

# 1. See which files have conflicts
git status

# 2. Open each conflicted file, resolve the conflict markers:
#    <<<<<<< HEAD
#    your version
#    =======
#    their version
#    >>>>>>> feature/other-branch

# 3. Stage the resolved files
git add path/to/resolved-file.go

# 4. Continue the merge or rebase
git merge --continue
# or
git rebase --continue

# To abort and return to the pre-conflict state:
git merge --abort
git rebase --abort

Useful Commands

Cherry-Pick

Apply a specific commit from another branch onto the current branch without merging the entire branch.
# Apply commit abc1234 to the current branch
git cherry-pick abc1234

# Cherry-pick a range of commits
git cherry-pick abc1234^..def5678
Useful when a bug fix on a feature branch needs to go to main immediately without waiting for the full PR.

Stash

Temporarily save uncommitted changes so you can switch branches without committing half-done work.
# Save current changes to the stash
git stash

# List all stashes
git stash list
# stash@{0}: WIP on feature/xyz: a4c3f21 feat: add webhook support

# Restore the most recent stash
git stash pop

# Apply a specific stash without removing it from the list
git stash apply stash@{1}

# Stash with a descriptive message
git stash push -m "half-done rate limiter refactor"

Reset vs. Revert

CommandWhat It DoesRewrites History?Safe on Shared Branches?
git reset --soft HEAD~1Move branch pointer back one commit; changes go back to staging areaYesNo
git reset --mixed HEAD~1Move branch pointer back; changes go to working directory (unstaged)YesNo
git reset --hard HEAD~1Move branch pointer back; discard changes entirelyYesNo
git revert <commit>Create a new commit that undoes the specified commitNoYes
# Undo the last commit but keep the changes staged
git reset --soft HEAD~1

# Undo the last commit and unstage changes (working directory intact)
git reset --mixed HEAD~1

# Permanently discard the last commit and all its changes
# WARNING: unrecoverable without reflog
git reset --hard HEAD~1

# Safely undo a commit that is already on main (creates a new commit)
git revert a4c3f21
Use git revert when undoing commits that are already on a shared branch. Use git reset only on commits that are purely local. git reflog can recover commits deleted by --hard reset, but only for 30 days and only locally.