merge-main-into-remote-branch-guide

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

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 branches
  • EXCLUDE=production,release β€” Skip protected deployment branches
  • EXCLUDE=archive/* β€” Skip archived branches

πŸ“– Detailed Workflow

Step-by-Step Process

  1. Repository Discovery

    • Uses specified path or auto-detects current directory
    • Validates it’s a Git repository
  2. Fetch Latest State

    • Runs git fetch --prune origin to sync remote branches
    • Ensures you’re working with up-to-date information
  3. 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
  4. Worktree Setup

    • Creates a temporary worktree in a safe location
    • Checks out the target branch in the worktree
    • Your current working directory remains untouched
  5. Merge Execution

    • Merges origin/main into the target branch
    • Creates a timestamped commit message
  6. 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 EXCLUDE list (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!

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. 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! πŸš€