Stop Losing Commits Between Branches - A Better Way to Manage Releases
December 15, 2025
I’ve cherry-picked the wrong commits into production three times in the last year. Once, I missed a critical bug fix. Once, I accidentally included an unfinished feature. Once, I spent two hours manually comparing git logs to figure out what was already in main versus what was still stuck in dev.
Every team I’ve worked with has the same problem: keeping long-lived branches in sync is a mess.
You start with good intentions. “We’ll merge dev into main for releases,” you say. Then someone rebases. Someone else squashes commits. Someone cherry-picks a hotfix directly to main. Suddenly, your branch histories diverge, and you have no idea what’s actually in production versus what’s waiting in development.
I built cherrypick-interactive because I was tired of this pain. It’s an interactive CLI that compares branches, shows you exactly what’s missing, and lets you cherry-pick commits safely with conflict resolution built in.
Here’s what I learned building it.
The Problem Nobody Talks About
Most Git tutorials assume a simple workflow: develop on dev, merge to main, done. But reality is messier.
What actually happens:
Week 1: Merge dev → main for release v1.0.0
Week 2: Hotfix committed directly to main
Week 3: Rebase dev to clean up history
Week 4: Cherry-pick some commits to main for v1.1.0
Week 5: Squash merge a feature branch
Week 6: "Which commits are in production again?"Now your branches have diverged. git log shows different commits even though they contain the same changes. Cherry-picking becomes guesswork.
The manual approach:
# Compare branches manually
git log origin/main..origin/dev --oneline
# Hope you can identify which commits are actually missing
# Copy commit hashes one by one
# Cherry-pick each one
git cherry-pick abc123
git cherry-pick def456
# Resolve conflicts manually
# Repeat 20 times
# Miss one critical commit
# Deploy broken codeThis is tedious, error-prone, and doesn’t scale.
What I Wanted: Git Cherry-Pick, But Interactive
I wanted something that felt like yarn upgrade-interactive but for Git commits.
Requirements:
- Show me commits in
devthat aren’t inmain - Let me select which ones to cherry-pick
- Handle conflicts without making me leave the terminal
- Preserve commit messages (even from squashed commits)
- Optionally bump version and create a release branch
- Make it impossible to lose track of what’s been deployed
The Solution: cherrypick-interactive
I built a CLI tool that does exactly this. Here’s how it works:
The Basic Workflow
npm install -g cherrypick-interactive
cherrypick-interactiveThis opens an interactive prompt showing commits in origin/dev that aren’t in origin/main:
? Select commits to cherry-pick (Press <space> to select)
❯ ◯ feat(auth): add OAuth support (2 days ago)
◯ fix(api): correct pagination offset (1 day ago)
◉ feat(ui): add dark mode (3 hours ago)
◯ chore(deps): bump dependencies (1 hour ago)Select the commits you want, press Enter, and they’re cherry-picked in the correct order (oldest → newest).
Handling Conflicts (The Hard Part)
When a cherry-pick has conflicts, you get an interactive wizard:
⚠️ Conflict in src/api/auth.js
? How would you like to resolve this conflict?
Use ours (keep current branch version)
Use theirs (accept cherry-picked version)
Open in editor (manual resolution)
Show diff (view conflicting changes)
Mark resolved (stage file as-is)
────────────────────────
Use ours for ALL conflicts
Use theirs for ALL conflicts
Stage ALL files
Launch mergetoolThis was the key insight: most conflicts have patterns. If you’re cherry-picking dependency updates, you probably want “theirs” for package-lock.json. If you’re cherry-picking a hotfix over a feature branch, you might want “ours” for certain files.
The wizard lets you resolve conflicts file-by-file or in bulk, without leaving the terminal.
The Release Branch Workflow
Here’s where it gets powerful. Most teams do this:
# Manually bump version in package.json
git checkout main
git pull
git checkout -b release/1.2.0
# Cherry-pick commits one by one
# Hope you got them all
# Push and open PRWith cherrypick-interactive, it’s one command:
cherrypick-interactive \
--semantic-versioning \
--version-file ./package.json \
--create-release \
--push-release \
--draft-prThis:
- Compares
devandmain - Shows missing commits
- Analyzes commit messages for version bump (using Conventional Commits)
- Creates
release/1.2.0branch frommain - Cherry-picks selected commits
- Updates
package.jsonversion - Pushes branch and opens a draft PR on GitHub
All automated. All safe. All traceable.
The Features I Needed (That Didn’t Exist)
1. Preserve Commit Messages from Squashed Commits
When you squash-merge a feature branch, the original commits disappear. If you later cherry-pick from dev to main, you need to preserve those original commit messages for semantic versioning to work.
Most cherry-pick tools lose this information. cherrypick-interactive preserves it using git commit -C <hash>.
2. Pattern-Based Filtering
Not all commits should be cherry-picked. I added filtering:
# Exclude dependency updates and CI changes
cherrypick-interactive --ignore-commits "^chore\(deps\)|^ci:"3. Semantic Version Detection
The tool analyzes commit messages to determine the version bump:
feat(auth): add OAuth → minor bump (1.0.0 → 1.1.0)
fix(api): correct offset → patch bump (1.0.0 → 1.0.1)
feat!: breaking change → major bump (1.0.0 → 2.0.0)You can also exclude certain commits from version calculation:
cherrypick-interactive --ignore-semver "bump|dependencies|merge"4. Dry Run Mode
Before doing anything risky:
cherrypick-interactive --dry-runShows exactly what would happen without making any changes.
How I Built It
The Tech Stack
- Node.js - For the CLI runtime
- Inquirer.js - For interactive prompts
- simple-git - For Git operations
- conventional-commits-parser - For semantic versioning
- GitHub CLI (gh) - For creating PRs
The Core Algorithm
async function findMissingCommits(devBranch, mainBranch) {
// Get commits in dev
const devCommits = await git.log([devBranch]);
// Get commits in main
const mainCommits = await git.log([mainBranch]);
// Find commits in dev that aren't in main
// This is trickier than it sounds because:
// - Squashed commits have different hashes
// - Rebased commits have different hashes
// - Cherry-picked commits have different hashes
// Solution: Compare commit messages and timestamps
const missing = devCommits.filter(devCommit => {
return !mainCommits.some(mainCommit => {
// Same message and similar timestamp = likely the same commit
return mainCommit.message === devCommit.message &&
Math.abs(mainCommit.date - devCommit.date) < 60000;
});
});
return missing;
}This handles squashed commits, rebased commits, and cherry-picked commits by comparing content rather than hashes.
Conflict Resolution
The conflict resolution wizard was the hardest part:
async function resolveConflicts(conflictedFiles) {
for (const file of conflictedFiles) {
const choice = await inquirer.prompt([{
type: 'list',
name: 'resolution',
message: `Conflict in ${file}. How to resolve?`,
choices: [
'Use ours',
'Use theirs',
'Open in editor',
'Show diff',
// ... more options
]
}]);
switch (choice.resolution) {
case 'Use ours':
await git.raw(['checkout', '--ours', file]);
await git.add(file);
break;
case 'Use theirs':
await git.raw(['checkout', '--theirs', file]);
await git.add(file);
break;
// ... handle other cases
}
}
// Preserve original commit message
await git.raw(['commit', '-C', originalCommitHash]);
}The -C flag is crucial. It preserves the original commit’s message, author, and date even after resolving conflicts.
Real-World Usage
Here’s how I use it in practice:
Weekly Release Process
# Every Friday, prepare release
cherrypick-interactive \
--semantic-versioning \
--version-file ./package.json \
--create-release \
--draft-pr \
--ignore-commits "^ci:|^docs:" \
--ignore-semver "bump|dependencies"This creates a draft PR with all commits from the last week, excluding CI and docs changes, with the correct semantic version bump.
Hotfix Cherry-Picking
# Cherry-pick specific commits for hotfix
cherrypick-interactive \
--since "2 days ago" \
--ignore-commits "^feat:"Shows only non-feature commits from the last 2 days (likely bug fixes).
Preview Before Release
# See what would be released without doing it
cherrypick-interactive --dry-runWhat I Learned Building This
1. Git Is More Complex Than You Think
Cherry-picking seems simple until you deal with:
- Squashed commits
- Rebased branches
- Merge commits
- Empty commits
- Binary files
- Submodules
Each case needs special handling.
2. Interactive CLIs Need Good UX
Early versions just printed commit hashes. Useless. The interactive selection with commit messages and dates made it actually usable.
Key UX decisions:
- Show commit message + relative time (“2 days ago”)
- Default to most recent commits selected
- Allow bulk actions for conflicts
- Show progress during cherry-picking
- Provide clear error messages
3. Semantic Versioning Needs Context
Not all commits should affect versioning. Dependency updates, documentation changes, and CI tweaks shouldn’t bump the version.
The --ignore-semver flag was essential for real-world usage.
4. Dry Run Mode Is Essential
Nobody trusts a tool that modifies Git history without a preview. --dry-run was the first feature users asked for.
Installation & Usage
Install globally:
npm install -g cherrypick-interactiveOr run without installing:
npx cherrypick-interactiveBasic usage:
cherrypick-interactiveFull release workflow:
cherrypick-interactive \
--semantic-versioning \
--version-file ./package.json \
--create-release \
--push-release \
--draft-prPreview changes:
cherrypick-interactive --dry-runFilter commits:
cherrypick-interactive \
--ignore-commits "^ci:|^chore\(deps\):" \
--ignore-semver "bump|dependencies"For full documentation and options, check out the npm package.
When You Should Use This
This tool is most valuable if your team:
- Maintains separate
devandmainbranches - Uses semantic versioning based on commit messages
- Cherry-picks commits between branches regularly
- Deals with squashed commits or rebased branches
- Creates release branches for review before merging
- Wants to automate the release preparation process
When not to use it:
- You merge everything linearly (no cherry-picking needed)
- You use GitHub Flow (single main branch)
- You have simple release processes that don’t need automation
What’s Next
I’m working on:
- Changelog generation - Automatically create release notes from selected commits
- Slack/Teams notifications - Alert your team when a release branch is ready
- Multiple branch comparison - Compare more than two branches at once
- Web UI - For teams who prefer graphical interfaces
In Closing
Cherry-picking commits between branches shouldn’t be this hard. Git gives you the primitives, but doesn’t give you the workflow.
cherrypick-interactive is the workflow I wish existed when I started doing release management. It’s saved me hours of manual work and prevented several “oops, we deployed the wrong thing” incidents.
If you’re tired of manually comparing branches, losing track of commits, or dealing with messy cherry-pick conflicts, give it a try:
npm install -g cherrypick-interactiveThe source code is open source and available on npm. Contributions, bug reports, and feature requests are welcome.
What’s your release management process like? What tools do you use to keep branches in sync? I’d love to hear about your workflow.
