Git Rebase, Merge, Fastforward, Cherry-pick & History Rewriting — End-to-End Guide
This guide walks through how Git represents history as a graph and how rebase, merge, fast-forward, cherry-pick, and history rewriting reshape that graph. You’ll learn exactly what each command does, how it affects collaborators, how to recover from mistakes, and how to design safe, production-grade Git workflows for real teams.
1 · Git History & Mental Model
Commits, parents, and the DAG
- Commits as nodes: Each Git commit stores a snapshot plus metadata and a pointer to one or more parents. The result is a directed acyclic graph (DAG) of history rather than a simple list. Understanding this structure explains why operations like merge or rebase can create new nodes without mutating old ones, which is critical for safe collaboration.
- Branches as movable labels: A branch name (like
mainorfeature/login) is just a pointer to a specific commit. When you add a new commit, Git advances that label forward. This makes branch creation nearly free (O(1) time and space) and enables cheap experiments without copying entire directories. - HEAD and checked-out state:
HEADpoints to “where you are” in the graph, typically the current branch. In a detached HEAD state, it points directly to a commit. Many history operations are just pointer rewrites ofHEADand branch refs, so understandingHEADprevents “lost” commits and surprising resets.
Merge vs rebase at a high level
Both merge and rebase combine histories, but they do so with different philosophies. Merge preserves original topology and introduces new merge commits, while rebase rewrites your branch so it looks as if it were developed on top of a new base. The choice affects log readability, conflict patterns, and how easy it is to debug regressions later.
A practical rule of thumb is: use rebase to clean up your own work before sharing and use merges to integrate branches that are already public. This keeps individual feature branches tidy while preserving an auditable, time-accurate record of how mainline code evolved across teams and environments.
Visualizing the commit graph for a small project
# Initialize a sample repo to explore history
mkdir git-history-basics
cd git-history-basics
git init
# Configure a demo identity (avoid polluting global config)
git config user.name "Demo User"
git config user.email "[email protected]"
# Create an initial commit on main
echo "v1" > app.txt
git add app.txt
git commit -m "chore: initial version"
# Create a feature branch and two commits
git switch -c feature/experiment
echo "exp change 1" >> app.txt
git commit -am "feat: experiment step 1"
echo "exp change 2" >> app.txt
git commit -am "feat: experiment step 2"
# Switch back to main and add a new commit
git switch main
echo "hotfix on main" >> app.txt
git commit -am "fix: hotfix on main"
# Inspect the graph with ASCII art
git log --oneline --graph --decorate --all
This script deliberately creates divergent histories so you can see Git’s DAG in action. The
feature/experiment branch advances away from main, and later main itself moves forward. Running git log --graph --all renders a text-based visualization of forks and joins, which is the raw material that merge, rebase, and cherry-pick manipulate.
Key points:
The cost of creating branches is effectively O(1) since they are just refs. The graph view reveals which commits are unique to each branch, which is the same set of nodes Git will consider during merges or rebases. Practicing on a throwaway repo lets you experiment with destructive commands safely before applying them to shared, production-critical repositories.
| Concept | What it Points To | Mutated By | Impact on History |
|---|---|---|---|
| Commit | Snapshot + parent commit IDs | Never mutated once created | Immutable building block; new commits reference old ones, forming the DAG while old commits remain unchanged. |
| Branch (ref) | Single commit hash | New commits, reset, rebase |
Moves along the graph like a bookmark; history appears different depending on which ref the team treats as canonical. |
| HEAD | Branch or direct commit | checkout, switch, reset |
Controls which tree is in your working directory and which ref will advance when you commit new changes. |
| Tag | Usually a single commit | Created/removed manually | Provides named anchors (e.g., releases). Doesn’t move automatically, so it’s safe for pinning builds and deployments. |
| Remote-tracking branch | Last fetched remote state | fetch, pull, push |
Represents the remote server’s view of history; used by rebase/merge to decide what’s “ahead” or “behind”. |
git reflog.
git reset --hard or git rebase on shared branches can rewrite history other people depend on. Always confirm whether a branch has been pushed or used by CI before rewriting it, and consider locking protected branches like main on your Git hosting platform.
.git/objects keyed by SHA-1 or SHA-256 hashes. Commits point to trees (file snapshots) and parent commits, forming an immutable chain. Branch names in .git/refs/heads simply point to commit hashes, so moving a branch is just writing a new hash into a tiny file, making history operations extremely fast.
2 · Merge Strategies & Fast-forward Behavior
Fast-forward merges
- Definition: A fast-forward merge occurs when the target branch has no new commits since the source branch split. Git simply moves the branch pointer forward with O(1) work and no new merge commit. This keeps history linear and clean but discards the visual record of when branches were integrated into mainline development.
- Ideal scenarios: Fast-forward merges shine for short-lived feature branches in solo work or trunk-based development where you rebase frequently. When branches live only for minutes or hours and CI verifies each commit, there’s little value in dedicated merge commits, and fast-forwarding keeps
git logandgit bisectoutput compact and easy to scan. - Tooling behavior: Platforms like GitHub and GitLab allow “Rebase and merge” or “Squash and merge” buttons that often create fast-forwardable histories. Understanding when a pull request becomes fast-forwardable helps predict whether your team will see explicit merge commits or a purely linear stream of changes on the default branch.
Merge commits and --no-ff
Non-fast-forward merges create a dedicated merge commit even when a fast-forward is possible. That extra node records the integration point explicitly, preserving branch topology. This is particularly useful in large teams where audits, release notes, and incident investigations depend on knowing exactly when a feature landed.
Using --no-ff slightly increases history size but provides high-value structure. Whole feature branches can be reverted later by reverting a single merge commit, and tools that visualize graphs (like gitk or hosting provider UIs) can show feature work as cohesive units. Many teams standardize on --no-ff for any multi-commit feature branch.
Scripted example of fast-forward and no-FF merges
# Start from a clean repo
mkdir git-merge-strategies
cd git-merge-strategies
git init
git config user.name "Demo User"
git config user.email "[email protected]"
# Base commit on main
echo "base" > app.txt
git add app.txt
git commit -m "chore: base"
# Create a feature branch and add two commits
git switch -c feature/fast-forward
echo "line 1" >> app.txt
git commit -am "feat: add line 1"
echo "line 2" >> app.txt
git commit -am "feat: add line 2"
# MAIN HAS NOT MOVED → merge is fast-forward
git switch main
git merge feature/fast-forward # fast-forward, no merge commit
# Now create another feature but add commits to main first
git switch -c feature/no-ff
echo "feature change A" >> app.txt
git commit -am "feat: change A"
git switch main
echo "main only change" >> app.txt
git commit -am "feat: main track"
# Merge preserving a merge commit
git merge --no-ff feature/no-ff -m "merge: integrate no-ff feature"
git log --oneline --graph --decorate --all
The first merge is fast-forwardable because
main has not advanced since the feature branch diverged; Git can simply move the main ref. In the second case, main accumulates its own commit, forcing a true merge with two parents. Using --no-ff guarantees a merge commit even when a fast-forward is technically possible.
Key points:
Fast-forward merges minimize commit noise but make reverting entire features harder when they consist of many commits. Non-fast-forward merges consume a tiny amount of extra storage but provide strong semantic grouping. In production, many teams fast-forward single-commit fixes but enforce
--no-ff via branch protection rules for larger features and release branches.
| Aspect | Fast-forward Merge | Merge Commit (--no-ff) |
Recommended Use Case |
|---|---|---|---|
| History shape | Strictly linear; no explicit branch integration points. | Graph shows merges; clear branching and joining structure. | Fast-forward for tiny, local changes; merge commits for multi-commit branches that must be auditable. |
| Revert complexity | Must revert each commit individually or carefully use ranges. | Can often revert the single merge commit to back out an entire feature. | Use merge commits when a feature could be rolled back as a unit during an incident. |
| CI pipeline triggers | CI usually runs once per merged commit. | CI runs on feature commits and again on the merge commit. | Consider cost: on very large monorepos you may prefer fewer CI runs to keep throughput acceptable. |
| Developer onboarding | Simpler git log, good for newcomers. |
Richer graph may require more Git literacy. | Training-heavy teams can exploit structured graphs; newbie-heavy teams might bias toward linear histories. |
| Tool compatibility | Works well with any Git tool; minimal configuration. | Required for strategies like “merge-only main” and release orchestration. | Use merge commits when external governance tools key off merge messages and branch names. |
--no-ff merges for long-running features, release branches, and any work that must remain intelligible during audits, postmortems, or complex dependency rollbacks under time pressure.
3 · Rebase Deep Dive
Basic and interactive rebase
- Rewriting the base:
git rebase maintakes commits that are unique to your branch, replays them on top ofmain, and moves your branch pointer to the newly created commits. This makes your work look as if it started from the latest mainline code, which can significantly reduce merge conflicts and simplify regression debugging. - Interactive editing:
git rebase -iopens an editor where you can reorder, squash, drop, or edit individual commits. This allows you to condense noisy “fix typo” commits into a single, coherent change set. Many teams expect developers to squash features into 1–3 well-described commits before opening a pull request, improving review quality and bisecting accuracy. - Autosquash and fixups: Using
git commit --fixupand--squashin combination withgit rebase -i --autosquashautomatically groups related commits. This pattern keeps daily work fast while still giving you a clean history: developers commit frequently but signal which commits are intended to be folded away at cleanup time, avoiding manual rebase list editing.
Rebase conflicts and safety
During a rebase, Git stops at the first commit that cannot be applied cleanly and asks you to resolve conflicts. Each conflict is local to a single replayed commit, which can be easier to reason about than monolithic merge-conflict resolutions. However, aborting or continuing at the wrong time can easily discard work.
You should always confirm that you’re rebasing the correct branch onto the correct base, especially on shared repositories. Taking five seconds to run git status and git log --oneline --graph before a rebase often avoids minutes or hours of recovery work after a misapplied history rewrite.
Interactive rebase with autosquash for a feature branch
# Starting from an existing repo with main
git switch -c feature/profile-page
# Make several noisy commits while developing
echo "<h1>Profile</h1>" > profile.html
git add profile.html
git commit -m "wip: basic profile markup"
echo "<!-- TODO: style profile -->" >> profile.html
git commit -am "wip: add todo"
# Realize you forgot tests, create a fixup
mkdir -p tests
echo "describe('profile', () => {})" > tests/profile.test.js
git add tests
git commit --fixup HEAD~1 # mark as fixup for "wip: basic profile markup"
# Upstream main has advanced; fetch updates
git fetch origin
git switch main
git pull --ff-only
# Rebase feature onto the latest main with autosquash
git switch feature/profile-page
git rebase -i --autosquash main
# After resolving conflicts and saving, verify result
git log --oneline --graph --decorate main..feature/profile-page
The
--fixup flag annotates commits so the interactive rebase sequence knows they should be squashed into specific parents. --autosquash then automatically reorders the todo list so fixups appear immediately after their targets with fixup or squash actions preselected. This reduces mental overhead during cleanup and encourages frequent, granular commits while coding.
Key points:
Interactive rebase is easiest on local, unpushed branches; rewriting commits that others have already pulled leads to duplicated history and confusing force pushes. In CI environments, you can enforce “no merge commits” on feature branches and rely on rebasing before merge to keep mainline history linear without relying on server-side squash mechanisms.
| Operation | History Effect | Risk Level (Shared Branch) | Typical Use Case |
|---|---|---|---|
git rebase main |
Replays your commits onto tip of main; original commits replaced with new ones. |
High if branch already pushed; commits get rewritten and require force push. | Keeping personal feature branches up to date with main before opening a pull request. |
git rebase -i |
Allows reordering, squashing, and editing of commits in-place. | Very high when others depend on the branch; commit IDs change significantly. | Cleaning noisy work-in-progress history into a handful of logical commits before review. |
git commit --amend |
Rewrites only the latest commit’s message and/or content. | Moderate; safe locally but disruptive if last commit is already pushed. | Fixing typos, test issues, or metadata in the most recent commit before pushing. |
git pull --rebase |
Rebases local commits on top of fetched remote commits instead of merging. | Medium; behavior may surprise users expecting merge commits. | Keeping local clones clean when frequently syncing with shared branches without merge bubbles. |
git rebase --onto |
Moves a subset of commits from one base onto another base. | High; powerful but error-prone if ranges are chosen incorrectly. | Extracting a subset of work from one branch and transplanting it onto a different starting point. |
main, to clean up local history before sharing, and to extract subsets of work with --onto. Avoid rebase on protected or release branches and whenever an explicit audit trail of how changes landed over time is more important than pristine linear history.
4 · Cherry-pick & Selective History Editing
Selective commit application
- Purpose of cherry-pick:
git cherry-pick <commit>takes the change introduced by a specific commit and applies it as a new commit on your current branch. This is invaluable when you want a bug fix or small improvement without merging an entire feature branch, such as backporting security fixes to an older release line while leaving new features behind. - Handling sequences: Cherry-picking multiple commits in order preserves logical progression but can cause conflicts if earlier picks depend on later ones or vice versa. Using ranges (for example,
git cherry-pick A..B) and grouping related commits thoughtfully can reduce conflict rates and keep patch application deterministic across multiple release branches. - Duplicate history trade-offs: Cherry-pick duplicates content changes into new commits with new hashes. This makes
git logshow similar commit messages across branches, which is good for traceability but slightly complicates tools that rely on commit IDs alone. Naming conventions like “backport:” prefixes help differentiate cherry-picked commits from their originals.
Backporting & failure recovery
Cherry-picking into a branch that diverged months ago often surfaces conflicts because the surrounding code has evolved. Resolving these conflicts is similar to resolving merge conflicts, but you may need to adapt logic to older APIs, removed functions, or different configuration formats in legacy branches.
If a cherry-pick goes badly, you can usually run git cherry-pick --abort to return to a clean state. For already recorded commits, git revert can undo the effect while preserving history. Combining cherry-pick, revert, and the reflog allows you to experiment with patch application while staying confident you can return to a known-good baseline.
Backporting a hotfix to a release branch with cherry-pick
# Assume we have main and release/1.0 branches
git switch main
# Introduce and fix a bug on main
echo "BUGGY=1" >> config.env
git commit -am "feat: add experimental flag (buggy)"
sed -i.bak 's/BUGGY=1/BUGGY=0/' config.env
git commit -am "fix: disable experimental flag by default"
# Identify the fix commit hash
git log --oneline -5
# Switch to the older release branch
git switch release/1.0
# Cherry-pick only the fix, not the experimental feature
git cherry-pick <fix-commit-sha>
# Resolve conflicts if any, then continue
# git status
# edit files...
git add config.env
git cherry-pick --continue
# Verify release branch history
git log --oneline --graph --decorate -10
Instead of merging
main into release/1.0 and risk pulling in unrelated features, cherry-pick isolates the exact bug fix you need. The new commit on the release branch has a different hash but a similar message and diff, ensuring the patch is applied while keeping release-line scope tightly controlled for stability and compliance.
Key points:
Backport-heavy workflows benefit from disciplined commit messages and atomic changes. If a “fix” commit depends on previous refactors, backporting may require cherry-picking multiple commits or reimplementing the fix in an older codebase style. Always run the full release test suite after cherry-picking into long-lived branches to catch subtle behavioral differences.
| Strategy | Scope of Imported Changes | History Clarity | Typical Use Case |
|---|---|---|---|
| Full merge | All commits from source branch since divergence. | Shows complete feature history and integration points. | Bringing whole features from a development branch into main or a staging branch. |
| Rebase onto target | All commits, but rewritten to sit on top of the target tip. | Linear history but hides original branch topology. | Integrating active feature work while keeping mainline commit graphs simple. |
| Cherry-pick single commit | Exactly one change; applied as a new commit. | Clear mapping between original and backported fix with matched messages. | Urgent hotfixes that must reach older releases quickly without pulling unrelated features or refactors. |
| Cherry-pick range | Multiple consecutive commits between two points. | Maintains partial history while skipping unwanted work. | Backporting a small feature subset that has cleanly separated commits. |
| Revert commit | Introduces a new commit that reverses a previous change. | Preserves audit trail while restoring prior behavior. | Undoing faulty cherry-picks or merges in shared branches where rewriting history is forbidden. |
5 · History Rewriting & Recovery
Amend, squash, and filter
- Amending the latest commit:
git commit --amendreplaces the HEAD commit with a new one containing updated content and/or message. This is perfect for adding a missing file or fixing a typo seconds after committing. On shared branches, however, amending a pushed commit changes its ID and requires a force push, which can surprise collaborators. - Squashing noisy history: Interactive rebase with
squashandfixuplets you roll a series of small commits into a single, well-described change. This reduces noise in logs, improvesgit blameusefulness, and makes bisecting faster because fewer intermediate commits represent half-done states and broken tests scattered across history. - Filtering entire histories: Tools like
git filter-repo(and legacyfilter-branch) allow rewriting large parts of a repository, such as removing accidentally committed secrets or heavy binary files. These operations can touch thousands of commits and dramatically shrink repository size but require every clone to be recloned or hard-reset to the rewritten history.
Reflog and disaster recovery
The reflog (git reflog) records movements of HEAD and branches for around 30–90 days by default, depending on configuration. Even after aggressive rebases or resets, you can often recover lost work by locating the previous commit IDs in the reflog and resetting or cherry-picking them back into current branches.
Knowing how to read the reflog turns “I lost everything” incidents into minor detours. Before running any risky history rewrite on an important branch, it is good practice to copy the latest commit hash to a safe note or create a temporary backup branch like backup/pre-rewrite pointing at the current tip as an extra safety net.
Cleaning up history and recovering from a bad reset
# Start on a feature branch with several commits
git switch -c feature/payment-refactor
# ... perform work, create multiple commits ...
# Realize the last commit message is wrong
git commit --amend -m "feat: refactor payment pipeline for idempotency"
# Squash the last 3 noisy commits into one using interactive rebase
git rebase -i HEAD~3
# In the editor, mark two commits as 'squash' and save
# Accidentally reset too far back
git reset --hard HEAD~5
# Panic! Use reflog to find the old tip
git reflog --date=iso | head -n 10
# Suppose the previous tip was at abc1234
git switch -c rescue/restore-history abc1234
# Merge or cherry-pick the rescued commits back into your feature
git switch feature/payment-refactor
git merge rescue/restore-history
# Verify that work is restored
git log --oneline --graph --decorate -10
Even though
reset --hard appears to “delete” commits, it only moves the branch reference and updates the working tree. The reflog still tracks previous positions of HEAD, allowing you to recover accidentally discarded commits by checking out or creating branches at those older hashes. This practice transforms destructive commands into safe, reversible operations when used thoughtfully.
Key points:
Always assume that anything rewriting history might require recovery. Creating temporary backup branches and consulting the reflog before running long, complex rebase or filter operations reduces risk. In regulated environments, coordinate large-scale history rewrites with operations and security teams, documenting new “blessed” commit IDs for audit trails.
| Operation | Scope | Recovery Strategy | Recommended Context |
|---|---|---|---|
git commit --amend |
Last commit only. | Use reflog or old hash if needed; usually trivial to redo. |
Local branches, quick fixes before pushing to remote repositories. |
git rebase -i |
Selected range of commits. | Backup branches, reflog entries, or abort during rebase. |
Feature branches owned by a single author or small pair-programming group. |
git reset --hard |
Moves branch pointer and working tree. | reflog to find prior HEAD, then new branch or reset back. |
Local cleanup when you are certain no one else depends on the discarded commits. |
git filter-repo |
Potentially all commits in repository. | Pre-run backups and mirrored clones; often no easy in-place undo. | Secret removal, large binary purges, or repo slimming coordinated across an organization. |
git revert |
Specific commits, one at a time. | Re-revert or reapply reverted patches if necessary. | Shared branches where rewriting history is forbidden but behavior must be changed. |
git filter-repo creates a new, rewritten history by visiting each commit, transforming it according to filters (such as path removals or text replacements), and writing out a new DAG. It then updates branch and tag refs to point at the new commit IDs. Old objects remain in the object store until garbage collection, but are effectively unreachable to normal Git operations.
filter-repo for rare, high-impact events such as secret exposure or repository bloat, and always pair them with clear communication to every consumer of the repository.
6 · Team Workflows & Best Practices
Choosing rebase vs merge in teams
- Linear mainline policy: Many teams enforce a linear
mainwhere all work is rebased before merging. This yields clean logs and simpler bisects at the cost of more frequent rebasing for developers. When combined with robust CI, this approach keeps deployment pipelines predictable and reduces the chance of “merge-only” failures that are hard to reproduce locally. - Merge-driven policy: Other teams value preserving exact integration events with merge commits. This strategy scales well for monorepos with many concurrent branches but makes logs more complex. Tools such as GitHub’s “Merge commit” option and protected branch rules ensure history reflects the real-order integration flow, which some auditors and SRE teams prefer.
- Hybrid approach: A pragmatic compromise is to rebase feature branches locally but always merge them into
mainwith--no-ff. Developers enjoy tidy feature histories while mainline retains explicit feature boundaries. Over time, these merge commits become the units around which release notes, changelogs, and rollbacks can be organized efficiently.
Code review and CI integration
Git history strategies interact strongly with code review tools and CI/CD systems. For example, requiring a green pipeline after every rebase or merge reduces “works on my machine” issues but increases CI load. Conversely, batching multiple changes into one merge commit can save compute but make failure attribution harder.
Align your use of rebase, merge, fast-forward, and cherry-pick with how your review platform displays diffs. A rebase-before-merge approach often yields compact diffs focused on the final, squashed feature, which reviewers appreciate. Merge-heavy approaches instead emphasize seeing progress over time, which can be valuable for mentoring and architectural oversight.
Sample Git configuration and policies for a linear main branch
# Enforce rebase on pull locally
git config --global pull.rebase true
# Optional: use 'merges' or 'simplify-by-decoration' history views
git config --global log.decorate auto
# Example branch protection checklist (conceptual, configured in UI):
# - main is protected
# - Require pull request for all changes
# - Require branch to be up-to-date before merging
# - Allow only "Rebase and merge" or "Squash and merge"
# - Require at least 1-2 approving reviews
# - Require all required status checks (CI, linters, security scans) to pass
# Feature branch workflow (developer commands)
git switch -c feature/search-api main
# ... commits ...
git fetch origin
git rebase origin/main # keep branch fresh
git push origin feature/search-api
# After review, maintainers click "Rebase and merge"
# main history stays linear and easy to bisect
Combining local rebase-on-pull with server-side “Rebase and merge” creates a strongly linear history on
main while giving developers freedom to experiment on branches. Branch protection rules ensure only validated, reviewed commits land on main. This pattern scales well across dozens of services and teams, keeping debugging and compliance audits relatively straightforward.
Key points:
Policy decisions should be explicit and documented: developers need to know whether force pushes are allowed, whether rebasing shared branches is okay, and how to handle backports. Periodically revisiting these rules as the team size or system complexity grows prevents fossilized workflows that no longer match reality.
| Dimension | Rebase-heavy Workflow | Merge-heavy Workflow | Hybrid (Rebase + No-FF Merge) |
|---|---|---|---|
| Mainline readability | Very linear; easy to follow but hides branch structure. | Shows full branch topology; may be noisy. | Linear per feature group with visible integration points. |
| Onboarding difficulty | Requires strong rebase literacy from developers. | Requires understanding merge commits and conflict patterns. | Moderate; developers learn both concepts incrementally. |
| Backporting effort | Cherry-pick from linear main; straightforward. | Must carefully traverse merges; slightly more complex. | Merge commits encapsulate features, easing identification. |
| CI behavior | More frequent pipeline triggers per rebased push. | Fewer rebases but more merge-time conflicts. | Balanced; CI runs on both feature branches and merges. |
| Preferred team size | Small to mid-sized teams comfortable with Git internals. | Large organizations with strong tooling support. | Any size that values both auditability and cleanliness. |
FAQ — Common Questions
Should I rebase or merge my feature branch before opening a pull request?
If your team has no explicit policy, a good default is to rebase your feature branch onto the latest main locally, resolve any conflicts, and then push. This keeps your branch up to date, reduces surprise conflicts during review, and typically yields smaller, more focused diffs. Once the pull request is approved, maintainers can choose whether to merge, squash, or rebase according to repository-wide rules.
Is it ever safe to rebase a branch that has already been pushed?
It can be safe if you are the only person using the branch, or if everyone who depends on it agrees to reset their local clones after the rewrite. For example, shared “work in progress” branches in a small team may be rebased during active collaboration. However, rebasing widely used branches (such as main or shared release branches) is almost always a bad idea and can break CI and deployment automation.
What’s the difference between git pull and git pull --rebase?
git pull is effectively git fetch followed by a merge of the remote branch into your current branch, creating merge commits when histories diverge. git pull --rebase is git fetch plus a rebase of your local commits on top of the remote. The former preserves merge structure; the latter keeps your local history linear at the cost of rewriting commits you haven’t pushed yet.
How do I undo a bad rebase or reset?
The usual recovery path is to run git reflog, locate the commit hash representing the state you want to restore, and then either create a new branch at that hash or move your current branch pointer back with git reset. For example, git reset --hard HEAD@{2} returns to the state from two reflog entries ago. Practicing on a disposable repo builds confidence in these techniques.
When should I use cherry-pick instead of merge?
Cherry-pick is the right tool when you need a specific fix or small feature but do not want the entire source branch. This commonly occurs when backporting security fixes to older releases or applying a single customer-specific patch. If you find yourself cherry-picking many commits from the same branch, consider merging instead and evaluating whether your branching model can be simplified.
Does squashing commits lose important history?
Squashing can discard the fine-grained details of how a change evolved (for example, “fix typo” or “attempt 2”). For most teams, that level of noise is more distracting than useful. Keeping a few cohesive commits per feature preserves enough context for blame and bisect while avoiding the clutter of dozens of intermediate steps. For truly critical work, you can retain raw branches in tags or archives if needed.
How do merge strategies affect git bisect performance?
Linear histories produced by rebase or squash make bisect straightforward because each step usually corresponds to a self-contained change. Merge-heavy histories may contain merge commits that are “bad” even when neither parent is; bisect will land on these occasionally, requiring extra manual interpretation. Still, bisect remains effective in both cases provided each commit keeps tests meaningful and reliable.
Can I rewrite history after secrets were pushed to a public repo?
You can and should rewrite history to remove the secrets from Git’s object store using tools like filter-repo, but that alone is insufficient. Anyone who cloned the repository before cleanup may still have the secret. Always assume leaked secrets are compromised: rotate keys, revoke tokens, and update credentials in all dependent systems in addition to cleaning the Git history itself.
Why do my collaborators complain when I force push after rebasing?
Force pushes change commit IDs, causing everyone else’s local branches to diverge. Their attempts to pull or push may fail with non-fast-forward errors until they reset their local branches. Without coordination, this feels like data loss or corruption. Limiting force pushes to private branches or using explicit agreements (for example, “force pushes allowed to feature/* only”) prevents frustration and confusion.
How can I practice Git history rewriting safely?
Create a dedicated sandbox repository and intentionally perform “dangerous” operations like rebase -i, reset --hard, cherry-pick, and filter-repo while observing their effects with git log --graph and git reflog. By repeatedly breaking and fixing history in a safe environment, you build intuition and muscle memory that carries over when working on critical production repositories.