A safe, intelligent Git script that merges origin/main into remote feature branches using worktrees—so your current work is never disrupted.
🎯 Why Use This Script?
The Problem It Solves
When working on feature branches, you often need to merge the latest changes from main to:
- Keep your branch up-to-date
- Resolve conflicts early
- Ensure CI/CD pipelines pass with latest dependencies
Traditional approach problems:
git checkout feature-branch
git merge main
# ❌ Your working directory changes
# ❌ Uncommitted work gets in the way
# ❌ Conflicts force you to stop everything
# ❌ Multiple branches = lots of manual switching
How This Script Helps
✅ Non-Disruptive — Uses Git worktrees, never touches your current branch
✅ Safe — Detects conflicts and guides you through resolution
✅ Intelligent — Auto-selects when only one candidate branch exists
✅ Flexible — Supports dry-run mode, branch exclusions, batch operations
✅ Clean — Automatically removes worktrees and temporary branches when done
✅ User-Friendly — Clear prompts, helpful error messages, conflict resolution guides
📦 Installation
Option 1: ~/.local/bin (Recommended – XDG Standard)
The ~/.local/bin directory follows the XDG Base Directory specification and is the modern standard for user-local executables. Most Linux distributions automatically include it in PATH.
# 1. Create the directory (if it doesn't exist)
mkdir -p ~/.local/bin
# 2. Add to PATH if needed (most modern systems already include it)
# Check if it's already in your PATH:
echo $PATH | grep -q "$HOME/.local/bin" && echo "Already in PATH" || echo "Need to add to PATH"
# If you need to add it manually:
# For bash:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
# For zsh:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
# For fish:
fish_add_path ~/.local/bin
# 3. Install the script
curl -o ~/.local/bin/merge-main https://raw.githubusercontent.com/yourusername/yourrepo/main/merge-main-into-remote-branch.sh
chmod +x ~/.local/bin/merge-main
# Or if you already have the file locally:
cp merge-main-into-remote-branch.sh ~/.local/bin/merge-main
chmod +x ~/.local/bin/merge-main
Why ~/.local/bin?
- ✅ XDG Base Directory standard (widely adopted)
- ✅ Often already in PATH on modern Linux distributions
- ✅ Keeps user binaries separate from system binaries
- ✅ Respects the filesystem hierarchy standard
- ✅ Works seamlessly with systemd user services
Option 2: ~/bin (Alternative)
Traditional alternative, still widely used.
# Create a personal bin directory
mkdir -p ~/bin
# Add to PATH (usually required)
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
# Install the script
cp merge-main-into-remote-branch.sh ~/bin/merge-main
chmod +x ~/bin/merge-main
Option 3: System-Wide Installation
Requires sudo but makes it available to all users.
# Make executable and move to system bin
chmod +x merge-main-into-remote-branch.sh
sudo mv merge-main-into-remote-branch.sh /usr/local/bin/merge-main
Option 4: Shell Alias
Keep the script in your project and create an alias.
# Add to ~/.bashrc or ~/.zshrc
echo 'alias merge-main="/full/path/to/merge-main-into-remote-branch.sh"' >> ~/.bashrc
source ~/.bashrc
Verify Installation
# Check if the command is found
which merge-main
# Test with dry-run
DRY_RUN=1 merge-main
🚀 Usage
Basic Syntax
merge-main [module] [options]
Parameters:
module— (optional) Path to the Git repository folder- If omitted and current directory is a Git repo, uses it automatically
- Use
.to explicitly use the current directory - Otherwise, prompts for module name
Quick Examples
# Run in current directory (if it's a git repo)
merge-main
# Explicitly use current directory
merge-main .
# Specify a repository path
merge-main ~/projects/my-app
merge-main ./backend
# Preview without making changes
DRY_RUN=1 merge-main
# Exclude additional branches
EXCLUDE=develop,staging,release merge-main
# Combine options
DRY_RUN=1 EXCLUDE=hotfix,production merge-main ~/projects/api
🎛️ Environment Variables
DRY_RUN
Preview all Git commands without executing them.
# See what would happen without making changes
DRY_RUN=1 merge-main
# Output example:
# [dry-run] git fetch --prune origin
# [dry-run] git fetch origin main feature/user-auth
# [dry-run] git worktree add -b feature/user-auth ...
When to use:
- Testing the script for the first time
- Verifying which branch will be selected
- Checking excluded branches
- Understanding the workflow before committing
EXCLUDE
Comma-separated list of branch names to exclude from candidate selection.
# Default exclusions: develop, staging
# Also always excluded: main, HEAD
# Add more exclusions
EXCLUDE=develop,staging,hotfix,production merge-main
# Override defaults (only exclude release)
EXCLUDE=release merge-main
# No extra exclusions (only main and HEAD)
EXCLUDE= merge-main
Common use cases:
EXCLUDE=hotfix— Avoid merging into emergency fix branchesEXCLUDE=production,release— Skip protected deployment branchesEXCLUDE=archive/*— Skip archived branches
📖 Detailed Workflow
Step-by-Step Process
Repository Discovery
- Uses specified path or auto-detects current directory
- Validates it’s a Git repository
Fetch Latest State
- Runs
git fetch --prune originto sync remote branches - Ensures you’re working with up-to-date information
- Runs
Branch Selection
- Lists all remote branches except
main,HEAD, and excluded branches - Auto-selects if only one candidate exists
- Shows numbered menu if multiple candidates exist
- Lists all remote branches except
Worktree Setup
- Creates a temporary worktree in a safe location
- Checks out the target branch in the worktree
- Your current working directory remains untouched
Merge Execution
- Merges
origin/maininto the target branch - Creates a timestamped commit message
- Merges
Outcome Handling
- Success: Pushes to origin, cleans up worktree and temp branch
- Conflict: Shows conflicting files, provides resolution guide
🎯 Real-World Scenarios
Scenario 1: Single Feature Branch
You have one active feature branch that needs the latest main.
$ merge-main
==> No module specified — using current directory as repo
==> Fetching origin for '.'...
==> Fetching origin/main and origin/feature/user-auth...
==> Auto-selected branch: feature/user-auth
==> Creating worktree at '/tmp/_worktree-feature-user-auth' tracking origin/feature/user-auth
==> Merging origin/main into feature/user-auth...
==> Merge successful. Pushing feature/user-auth to origin...
==> Cleaning up worktree and local branch...
OK: merged main -> feature/user-auth in '.' at 10Apr2026 1430
Scenario 2: Multiple Branches
You have several feature branches and need to choose which one to update.
$ merge-main ~/projects/api
Multiple candidate branches found:
[1] feature/authentication
[2] feature/payment-gateway
[3] bugfix/timeout-issue
Pick one [1-3]: 2
==> Fetching origin/main and origin/feature/payment-gateway...
==> Creating worktree...
==> Merging origin/main into feature/payment-gateway...
==> Merge successful. Pushing feature/payment-gateway to origin...
OK: merged main -> feature/payment-gateway in 'api' at 10Apr2026 1445
Scenario 3: Merge Conflict
The merge encounters conflicts that need manual resolution.
$ merge-main
==> Merging origin/main into feature/new-ui...
!! Merge conflict detected. Conflicting files:
- src/components/Header.jsx
- src/styles/theme.css
Resolve conflicts manually, then push:
Step 1 — Go to the worktree where the conflict lives:
cd /tmp/_worktree-feature-new-ui
Step 2 — See the full status:
git status
Step 3 — Open each conflicting file and resolve the markers:
<<<<<<< HEAD ← your branch (feature/new-ui)
your code
=======
incoming code
>>>>>>> origin/main ← what came from main
Step 4 — Mark each resolved file as done:
git add <file>
Step 5 — Complete the merge commit:
git commit
Step 6 — Push the resolved branch:
git push -u origin feature/new-ui
Step 7 — Clean up the worktree:
cd -
git worktree remove /tmp/_worktree-feature-new-ui
git branch -D feature/new-ui
Resolution Example:
# Navigate to worktree
cd /tmp/_worktree-feature-new-ui
# Edit conflicting files in your editor
vim src/components/Header.jsx
# After resolving conflicts
git add src/components/Header.jsx src/styles/theme.css
git commit
git push -u origin feature/new-ui
# Return to original directory and clean up
cd -
git worktree remove /tmp/_worktree-feature-new-ui
git branch -D feature/new-ui
Scenario 4: Batch Processing
Update all feature branches across multiple repositories.
# Process all git repos in subdirectories
for dir in */; do
if [[ -d "$dir/.git" ]]; then
echo "Processing $dir..."
merge-main "${dir%/}"
fi
done
Example Output:
Processing api/...
OK: merged main -> feature/v2-endpoints in 'api' at 10Apr2026 1500
Processing frontend/...
OK: merged main -> feature/dashboard in 'frontend' at 10Apr2026 1502
Processing workers/...
!! Merge conflict detected in 'workers'...
Scenario 5: Dry Run Before Production
Test what will happen before making actual changes.
# Preview the entire workflow
DRY_RUN=1 merge-main
[dry-run] git fetch --prune origin
[dry-run] git fetch origin main feature/critical-fix
[dry-run] git worktree add -b feature/critical-fix /tmp/_worktree-feature-critical-fix origin/feature/critical-fix
[dry-run] git merge origin/main -m "merge: main into feature/critical-fix 10Apr2026 1515"
[dry-run] git push -u origin feature/critical-fix
[dry-run] git worktree remove /tmp/_worktree-feature-critical-fix
# If it looks good, run for real
merge-main
🛡️ Safety Features
1. Worktree Isolation
Your current branch and working directory are never modified. All merge operations happen in a temporary worktree.
Your Repo Temporary Worktree
--------- ------------------
main ← you're here feature/xyz ← merge happens here
feature/xyz (remote)
2. Stale Worktree Detection
If a previous run was interrupted (conflict, crash, manual abort), the script detects leftover worktrees and handles them intelligently.
With unresolved conflicts:
!! A worktree already exists at: /tmp/_worktree-feature-xyz
WARNING: These files still have unresolved merge conflicts:
- src/app.js
- config/settings.py
Force-removing this worktree will PERMANENTLY DISCARD those changes.
Force remove and start fresh? [y/N]
With uncommitted changes:
!! A worktree already exists at: /tmp/_worktree-feature-xyz
WARNING: The worktree has uncommitted changes:
M src/utils.js
A tests/new-test.js
Force-removing this worktree will PERMANENTLY DISCARD those changes.
Force remove and start fresh? [y/N]
Clean worktree (no changes):
==> Stale worktree has no pending changes — removing automatically
3. Branch Validation
Automatically excludes:
origin/HEAD(symbolic reference)origin/main(the source branch)- Branches in
EXCLUDElist (default:develop,staging)
4. Graceful Conflict Handling
Instead of leaving you in a broken state, the script:
- Clearly lists which files have conflicts
- Provides step-by-step resolution instructions
- Preserves the worktree so you can fix conflicts manually
- Exits with status code 1 (won’t silently fail in scripts)
🔧 Advanced Usage
Custom Commit Message Pattern
Edit the script to customize the commit message:
# Find this line in the script (around line 163):
MSG="merge: main into ${BRANCH} ${STAMP}"
# Change to your preferred format:
MSG="chore: sync ${BRANCH} with main (${STAMP})"
MSG="Merge main → ${BRANCH} | ${STAMP}"
MSG="🔀 main → ${BRANCH}"
Integration with CI/CD
Use in automation pipelines:
#!/bin/bash
# .github/workflows/sync-branches.sh
set -e
# Fail fast if merge has conflicts
merge-main || {
echo "Merge conflict detected - manual intervention required"
exit 1
}
# Continue with tests, deployments, etc.
Custom Exclusion Lists per Project
Create a wrapper script:
#!/bin/bash
# sync-api-branches.sh
# Project-specific exclusions
export EXCLUDE="hotfix,production,release,v1-stable"
merge-main ~/projects/api
Monitoring in Scripts
Capture output for logging:
LOG_FILE="merge-$(date +%Y%m%d).log"
merge-main 2>&1 | tee -a "$LOG_FILE"
if [[ ${PIPESTATUS[0]} -eq 0 ]]; then
echo "✓ Merge completed successfully" >> "$LOG_FILE"
else
echo "✗ Merge failed - check log" >> "$LOG_FILE"
# Send alert, create ticket, etc.
fi
❓ FAQ
Q: What if I accidentally run this on main itself?
The script explicitly excludes main from the candidate list, so it won’t let you merge main into main.
Q: Can I use this with GitHub/GitLab protected branches?
Yes, but you need push permissions. If the target branch is protected, the push step will fail with a permission error. The merge itself happens locally in the worktree.
Q: What happens to my uncommitted changes?
Nothing—they remain untouched. The script works in a separate worktree, not your current directory.
Q: Can I merge into multiple branches at once?
Not directly, but you can script it:
for branch in feature/auth feature/payments feature/notifications; do
echo "Processing $branch..."
# Script will auto-select if you filter candidates to match exactly one
EXCLUDE="$(git branch -r | sed 's|origin/||' | grep -v "$branch" | tr '\n' ',')" merge-main
done
Q: How do I abort a merge in progress?
# Navigate to the worktree
cd /tmp/_worktree-your-branch
# Abort the merge
git merge --abort
# Return and clean up
cd -
git worktree remove /tmp/_worktree-your-branch
git branch -D your-branch
Q: Does this work with submodules?
Yes, but each submodule is a separate Git repository. You’d need to run the script separately for each submodule, or create a wrapper that iterates through them.
🐛 Troubleshooting
“command not found: merge-main”
Problem: Script is not in PATH or not executable.
Solution:
# Check if it exists
ls -la ~/.local/bin/merge-main
# Make sure it's executable
chmod +x ~/.local/bin/merge-main
# Verify PATH includes ~/.local/bin
echo $PATH | grep "$HOME/.local/bin"
# If not in PATH, add it:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
# Reload shell config
source ~/.bashrc # or ~/.zshrc
“not a git repository”
Problem: Running in a directory that isn’t a Git repo.
Solution:
# Check if current dir is a git repo
git status
# Or specify the repo path explicitly
merge-main /path/to/your/repo
“no non-main remote branch found”
Problem: All branches are excluded or only main exists.
Solution:
# Check what branches exist
git branch -r
# Adjust exclusions
EXCLUDE= merge-main # Remove default exclusions
# Or create a feature branch first
git checkout -b feature/new-feature
git push -u origin feature/new-feature
Worktree conflicts with existing directory
Problem: /tmp/_worktree-branch-name already exists.
Solution:
# The script should handle this automatically
# If it doesn't, manually remove:
git worktree remove --force /tmp/_worktree-branch-name
# Then re-run the script
merge-main
📝 Script Reference
Full Script
#!/usr/bin/env bash
# =============================================================================
# merge-main-into-remote-branch.sh
#
# PURPOSE:
# Merges origin/main into a non-main remote branch inside a git repo (module).
# Uses git worktree so your current working branch is never touched.
#
# USAGE:
# ./merge-main-into-remote-branch.sh [module]
#
# module — (optional) path to the git repo folder.
# If omitted, the script checks if the current directory is a git
# repo and uses it automatically, otherwise it prompts.
# Pass "." to explicitly use the current directory.
#
# OPTIONS (environment variables):
# DRY_RUN=1 — Preview all git commands without executing them.
# EXCLUDE=a,b,c — Comma-separated branch names to exclude from candidates
# in addition to main and HEAD (default: "develop,staging").
#
# EXAMPLES:
# ./merge-main-into-remote-branch.sh managebac
# ./merge-main-into-remote-branch.sh .
# DRY_RUN=1 ./merge-main-into-remote-branch.sh managebac
# EXCLUDE=develop,staging,release ./merge-main-into-remote-branch.sh managebac
#
# # Run across ALL subdirectories that are git repos:
# for dir in */; do
# [[ -d "$dir/.git" ]] && ./merge-main-into-remote-branch.sh "${dir%/}"
# done
# =============================================================================
# NOTE: We intentionally do NOT use `set -e` globally here.
# The merge step is expected to exit non-zero on conflicts, and we need to
# handle that gracefully ourselves rather than letting bash abort the script.
# We keep -u (undefined variable check) and -o pipefail for safety.
set -uo pipefail
# -----------------------------------------------------------------------------
# CONFIG
# -----------------------------------------------------------------------------
# Dry-run mode: set DRY_RUN=1 to preview without making any changes
DRY_RUN="${DRY_RUN:-0}"
# Branches to always exclude from candidate list (on top of main and HEAD)
EXCLUDE="${EXCLUDE:-develop,staging}"
# -----------------------------------------------------------------------------
# HELPERS
# -----------------------------------------------------------------------------
# Print a timestamped info message
info() { echo "==> $*"; }
# Print an error to stderr and exit immediately
die() { echo "error: $*" >&2; exit 1; }
# Run a command normally, or just print it when DRY_RUN=1
run() {
if [[ "$DRY_RUN" == "1" ]]; then
echo "[dry-run] $*"
else
"$@"
fi
}
# Ask a yes/no question — returns 0 for yes, 1 for no
# Usage: confirm "Are you sure?" && do_something
confirm() {
local prompt="${1:-Are you sure?}"
local reply
read -rp "$prompt [y/N] " reply
[[ "${reply,,}" == "y" || "${reply,,}" == "yes" ]]
}
# -----------------------------------------------------------------------------
# RESOLVE MODULE / REPO PATH
# -----------------------------------------------------------------------------
MODULE="${1:-}"
# If no argument given, check if the current directory is already a git repo
if [[ -z "$MODULE" ]]; then
if git rev-parse --git-dir > /dev/null 2>&1; then
info "No module specified — using current directory as repo"
MODULE="."
else
read -rp "Module name (e.g. managebac) or '.' for current dir: " MODULE
fi
fi
[[ -z "$MODULE" ]] && die "module name required"
[[ -d "$MODULE" ]] || die "'$MODULE' is not a directory"
# Move into the module directory
cd "$MODULE"
# Confirm this is actually a git repository
git rev-parse --git-dir > /dev/null 2>&1 || die "'$MODULE' is not a git repository"
# -----------------------------------------------------------------------------
# FETCH LATEST REMOTE STATE
# -----------------------------------------------------------------------------
info "Fetching origin for '$MODULE'..."
run git fetch --prune origin
# -----------------------------------------------------------------------------
# BUILD EXCLUDE PATTERN
# -----------------------------------------------------------------------------
# Convert comma-separated EXCLUDE into a regex alternation e.g. "develop|staging"
EXCLUDE_PATTERN=$(echo "$EXCLUDE" | tr ',' '|')
# -----------------------------------------------------------------------------
# DISCOVER CANDIDATE BRANCHES
# -----------------------------------------------------------------------------
# List all remote branches, strip whitespace, filter out:
# - origin/HEAD — just a symbolic pointer, not a real branch
# - origin/main — this is the source, never the target
# - EXCLUDE list — e.g. develop, staging
mapfile -t CANDIDATES < <(
git branch -r \
| sed 's/^[[:space:]]*//' \
| grep -v '^origin/HEAD' \
| grep -v '^origin/main$' \
| grep -vE "^origin/(${EXCLUDE_PATTERN})$" \
| sed 's|^origin/||'
)
# -----------------------------------------------------------------------------
# SELECT TARGET BRANCH
# -----------------------------------------------------------------------------
if [[ ${#CANDIDATES[@]} -eq 0 ]]; then
die "no non-main remote branch found in '$MODULE' (excluded: main, $EXCLUDE)"
elif [[ ${#CANDIDATES[@]} -eq 1 ]]; then
# Only one candidate — select it automatically, no prompt needed
BRANCH="${CANDIDATES[0]}"
info "Auto-selected branch: $BRANCH"
else
# Multiple candidates — present a numbered menu and let the user choose
echo "Multiple candidate branches found:"
for i in "${!CANDIDATES[@]}"; do
printf " [%d] %s\n" "$((i+1))" "${CANDIDATES[$i]}"
done
read -rp "Pick one [1-${#CANDIDATES[@]}]: " PICK
[[ "$PICK" =~ ^[0-9]+$ ]] || die "invalid pick: '$PICK'"
(( PICK >= 1 && PICK <= ${#CANDIDATES[@]} )) || die "out of range: $PICK"
BRANCH="${CANDIDATES[$((PICK-1))]}"
fi
# -----------------------------------------------------------------------------
# PREPARE WORKTREE PATH & COMMIT MESSAGE
# -----------------------------------------------------------------------------
# Sanitize branch name: lowercase, replace slashes and spaces with dashes
SAFE_BRANCH="$(echo "$BRANCH" | tr '[:upper:]/ ' '[:lower:]-')"
if [[ "$MODULE" == "." ]]; then
# Running inside the repo itself — place worktree in /tmp to avoid nesting
WORKTREE="/tmp/_worktree-${SAFE_BRANCH}"
else
# Place worktree as a sibling of the module directory
WORKTREE="../_${MODULE}-${SAFE_BRANCH}"
fi
STAMP="$(date +'%d%b%Y %H%M')"
MSG="merge: main into ${BRANCH} ${STAMP}"
# -----------------------------------------------------------------------------
# FETCH MAIN + TARGET BRANCH (ensure both are up to date)
# -----------------------------------------------------------------------------
info "Fetching origin/main and origin/$BRANCH..."
run git fetch origin main "$BRANCH"
# -----------------------------------------------------------------------------
# HANDLE STALE WORKTREE FROM A PREVIOUS FAILED RUN
# -----------------------------------------------------------------------------
# `git worktree list` prints absolute paths — use grep -F (fixed string, no regex)
# to safely match the exact path without worrying about special characters
if git worktree list | grep -qF "$WORKTREE"; then
echo ""
echo "!! A worktree already exists at: $WORKTREE"
echo " This is likely left over from a previous run that had conflicts."
echo ""
# Check for files that are still in an unresolved conflict state (diff-filter=U)
UNMERGED=$(git -C "$WORKTREE" diff --name-only --diff-filter=U 2>/dev/null || true)
# Check for any other uncommitted changes (staged, unstaged, untracked)
UNCOMMITTED=$(git -C "$WORKTREE" status --porcelain 2>/dev/null || true)
if [[ -n "$UNMERGED" ]]; then
# Unresolved conflict markers still present — highest risk, warn loudly
echo " WARNING: These files still have unresolved merge conflicts:"
echo "$UNMERGED" | sed 's/^/ - /'
echo ""
echo " Force-removing this worktree will PERMANENTLY DISCARD those changes."
echo ""
confirm " Force remove and start fresh?" \
|| die "aborted — resolve conflicts manually in $WORKTREE then re-run"
elif [[ -n "$UNCOMMITTED" ]]; then
# Changes exist but no conflict markers — could be partial manual fixes
echo " WARNING: The worktree has uncommitted changes:"
echo "$UNCOMMITTED" | sed 's/^/ /'
echo ""
echo " Force-removing this worktree will PERMANENTLY DISCARD those changes."
echo ""
confirm " Force remove and start fresh?" \
|| die "aborted — inspect $WORKTREE before proceeding"
else
# Worktree is clean — safe to remove silently without asking
info "Stale worktree has no pending changes — removing automatically"
fi
run git worktree remove --force "$WORKTREE"
fi
# Remove any local tracking branch left over from a prior run.
# Try safe delete (-d) first; only force-delete (-D) if git considers it unmerged.
if git branch --list "$BRANCH" | grep -q .; then
git branch -d "$BRANCH" 2>/dev/null \
|| git branch -D "$BRANCH" 2>/dev/null \
|| true
fi
# -----------------------------------------------------------------------------
# CREATE FRESH WORKTREE TRACKING THE TARGET BRANCH
# -----------------------------------------------------------------------------
info "Creating worktree at '$WORKTREE' tracking origin/$BRANCH"
run git worktree add -b "$BRANCH" "$WORKTREE" "origin/$BRANCH"
# -----------------------------------------------------------------------------
# MERGE origin/main INTO TARGET BRANCH
# -----------------------------------------------------------------------------
info "Merging origin/main into $BRANCH..."
# Temporarily disable pipefail around the merge command.
# git merge exits non-zero on conflicts — that is expected and handled below.
# Without this, pipefail would cause the script to abort before we can react.
pushd "$WORKTREE" > /dev/null
set +e
run git merge origin/main -m "$MSG"
MERGE_EXIT=$?
set -e
popd > /dev/null
# -----------------------------------------------------------------------------
# OUTCOME: SUCCESS — push and clean up
# -----------------------------------------------------------------------------
if [[ $MERGE_EXIT -eq 0 ]]; then
info "Merge successful. Pushing $BRANCH to origin..."
pushd "$WORKTREE" > /dev/null
run git push -u origin "$BRANCH"
popd > /dev/null
info "Cleaning up worktree and local branch..."
run git worktree remove "$WORKTREE"
git branch -d "$BRANCH" 2>/dev/null \
|| git branch -D "$BRANCH" 2>/dev/null \
|| true
echo ""
echo "OK: merged main -> $BRANCH in '$MODULE' at $STAMP"
# -----------------------------------------------------------------------------
# OUTCOME: CONFLICT — show exactly what conflicts exist and guide resolution
# -----------------------------------------------------------------------------
else
# List the conflicting files immediately so the user knows where to look
echo ""
echo "!! Merge conflict detected. Conflicting files:"
git -C "$WORKTREE" diff --name-only --diff-filter=U \
| sed 's/^/ - /'
cat <<EOF
Resolve conflicts manually, then push:
Step 1 — Go to the worktree where the conflict lives:
cd $WORKTREE
Step 2 — See the full status:
git status
Step 3 — Open each conflicting file and resolve the markers:
<<<<<<< HEAD ← your branch ($BRANCH)
your code
=======
incoming code
>>>>>>> origin/main ← what came from main
Step 4 — Mark each resolved file as done:
git add <file>
Step 5 — Complete the merge commit:
git commit
Step 6 — Push the resolved branch:
git push -u origin $BRANCH
Step 7 — Clean up the worktree (run from the repo root, not inside the worktree):
cd -
git worktree remove $WORKTREE
git branch -D $BRANCH
TIP: To abandon the merge entirely and start over:
git merge --abort ← run this inside $WORKTREE first
then re-run this script from the repo root.
EOF
exit 1
fi
🤝 Contributing
Found a bug? Have a feature request? Contributions are welcome!
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
📄 License
This script is provided as-is under the MIT License. Feel free to use, modify, and distribute.
🙏 Acknowledgments
Built with love for developers who are tired of:
- Switching branches constantly
- Merge conflicts destroying their flow
- Forgetting which branches need updates
- Manual, error-prone sync processes
Happy merging! 🚀


