Merge Upstream
Sync the current fork branch with upstream/<branch> using a merge commit by default. Preserve local commit hashes, keep push non-destructive, and never rewrite history.
Usage
Treat these as equivalent triggers:
/merge-upstream [--base=<branch>] [--ff-allow]
merge upstream [--base=<branch>] [--ff-allow]
sync fork with upstream
Options:
--base=<branch>: use that upstream branch instead of auto-detecting the upstream default branch.
--ff-allow: allow git merge --ff-only when the current branch has no unique local commits. Without this, use git merge --no-ff to leave an explicit sync commit.
Invariants
- Do not run
git rebase.
- Do not run
git push --force or git push --force-with-lease.
- Do not bypass hooks or signing with
--no-verify or --no-gpg-sign.
- Ask before pushing.
Workflow
Validate the repository:
git rev-parse --is-inside-work-tree
git branch --show-current
git remote get-url upstream
git remote get-url origin
Abort on detached HEAD or missing upstream. If origin is missing, continue locally and skip push.
Require a clean worktree:
worktree_status=$(git status --porcelain) || exit 1
test -z "$worktree_status"
If dirty, stop and ask the user to clean or commit the changes, or use a clean task worktree.
Detect the upstream target branch:
- If
--base=<branch> is provided, fetch and verify upstream/<branch>.
- Otherwise run
git remote set-head upstream -a, then read refs/remotes/upstream/HEAD.
- If detection fails, ask the user for
--base=<branch>.
Fetch enough history for a reliable merge base:
git rev-parse --is-shallow-repository
git fetch --tags upstream "+refs/heads/${upstream_branch}:refs/remotes/upstream/${upstream_branch}"
origin_branch_exists=false
if git remote get-url origin >/dev/null 2>&1; then
if ! origin_branch=$(git ls-remote --heads origin "refs/heads/${current_branch}"); then
echo "failed to inspect origin/${current_branch}" >&2
exit 1
elif [ -n "$origin_branch" ]; then
git fetch origin "+refs/heads/${current_branch}:refs/remotes/origin/${current_branch}" || exit 1
origin_branch_exists=true
else
echo "origin/${current_branch} does not exist; current branch is unpublished"
fi
fi
git merge-base HEAD "upstream/${upstream_branch}"
If the repository is shallow, unshallow origin first when available, then upstream only if still shallow.
Always use the fetched remote-tracking ref as the target; do not decide from a previously cached upstream/<branch> tip.
Record the exact refs and report divergence:
current_head=$(git rev-parse HEAD)
upstream_tip=$(git rev-parse "upstream/${upstream_branch}")
git rev-list --count "upstream/${upstream_branch}..HEAD"
git rev-list --count "HEAD..upstream/${upstream_branch}"
GIT_PAGER=cat git log --oneline "HEAD..upstream/${upstream_branch}"
GIT_PAGER=cat git log --first-parent --oneline "upstream/${upstream_branch}..HEAD"
Report HEAD and upstream/${upstream_branch} with their full SHAs.
Merge:
Behind 0 means the fetched upstream_tip is already an ancestor of current_head. Confirm that state with both checks:
git merge-base --is-ancestor "$upstream_tip" "$current_head"
upstream_range=$(git rev-list "$current_head..$upstream_tip") || exit 1
test -z "$upstream_range"
This completes upstream integration as a successful no-op. Report the exact refs, SHAs, ancestry result, and empty range; skip the merge, release, and other change-dependent gates. Do not create an empty commit or pull request, or publish a branch solely to represent the sync. If an independent request explicitly approves pushing existing local commits, use step 9's non-destructive push semantics.
If behind is greater than 0 and --ff-allow is set with ahead 0, run:
git merge --ff-only "upstream/${upstream_branch}"
Otherwise capture previous_head and upstream_tip, then run:
git merge --no-ff "upstream/${upstream_branch}" -m "merge: sync ${current_branch} with upstream/${upstream_branch}"
Resolve conflicts locally when they occur:
git diff --name-only --diff-filter=U
Read each conflicted file. Auto-resolve only mechanically obvious conflicts such as non-overlapping additions, import unions, formatting-only differences, or generated lockfile refreshes. For semantic conflicts, present the specific conflict and ask whether to keep ours, keep theirs, manually edit, or abort. After resolution:
git add <file>
git -c core.editor=true merge --continue
Verify:
git rev-parse --git-path MERGE_HEAD
git rev-parse --git-path rebase-merge
git rev-parse --git-path rebase-apply
git show -s --format=%P HEAD
git rev-list --left-right --count "upstream/${upstream_branch}...HEAD"
if [ "$origin_branch_exists" = true ]; then
git merge-base --is-ancestor "origin/${current_branch}" HEAD
fi
In default mode, verify HEAD has two parents: previous_head as first parent and upstream_tip as second parent. In --ff-allow fast-forward mode, verify HEAD equals upstream_tip.
Push only with explicit approval:
GIT_PAGER=cat git log --oneline --graph --decorate -10
git push origin "${current_branch}"
If the remote branch does not exist, use git push -u origin "${current_branch}". If push is rejected as non-fast-forward, re-fetch and offer only non-destructive options: merge origin/<branch> into HEAD and retry, or stop.
Final Report
Include:
- branch and upstream target, including their full SHAs
- whether merge, fast-forward, or no-op happened
- for a no-op, confirmed ancestry and the empty
HEAD..upstream/<branch> range
- fork commits preserved
- upstream commits integrated
- push status
- any conflicts and how they were resolved
1---2name: merge-upstream3description: Sync a fork branch with an upstream remote using a history-preserving merge. Use this whenever the user says /merge-upstream, merge upstream, sync upstream, sync fork, or wants upstream changes integrated without rebasing or force-pushing.4---56# Merge Upstream78Sync the current fork branch with `upstream/<branch>` using a merge commit by default. Preserve local commit hashes, keep push non-destructive, and never rewrite history.910## Usage1112Treat these as equivalent triggers:1314```text15/merge-upstream [--base=<branch>] [--ff-allow]16merge upstream [--base=<branch>] [--ff-allow]17sync fork with upstream18```1920Options:2122- `--base=<branch>`: use that upstream branch instead of auto-detecting the upstream default branch.23- `--ff-allow`: allow `git merge --ff-only` when the current branch has no unique local commits. Without this, use `git merge --no-ff` to leave an explicit sync commit.2425## Invariants2627- Do not run `git rebase`.28- Do not run `git push --force` or `git push --force-with-lease`.29- Do not bypass hooks or signing with `--no-verify` or `--no-gpg-sign`.30- Ask before pushing.3132## Workflow33341. Validate the repository:3536 ```bash37 git rev-parse --is-inside-work-tree38 git branch --show-current39 git remote get-url upstream40 git remote get-url origin41 ```4243 Abort on detached HEAD or missing `upstream`. If `origin` is missing, continue locally and skip push.44452. Require a clean worktree:4647 ```bash48 worktree_status=$(git status --porcelain) || exit 149 test -z "$worktree_status"50 ```5152 If dirty, stop and ask the user to clean or commit the changes, or use a clean task worktree.53543. Detect the upstream target branch:5556 - If `--base=<branch>` is provided, fetch and verify `upstream/<branch>`.57 - Otherwise run `git remote set-head upstream -a`, then read `refs/remotes/upstream/HEAD`.58 - If detection fails, ask the user for `--base=<branch>`.59604. Fetch enough history for a reliable merge base:6162 ```bash63 git rev-parse --is-shallow-repository64 git fetch --tags upstream "+refs/heads/${upstream_branch}:refs/remotes/upstream/${upstream_branch}"65 origin_branch_exists=false66 if git remote get-url origin >/dev/null 2>&1; then67 if ! origin_branch=$(git ls-remote --heads origin "refs/heads/${current_branch}"); then68 echo "failed to inspect origin/${current_branch}" >&269 exit 170 elif [ -n "$origin_branch" ]; then71 git fetch origin "+refs/heads/${current_branch}:refs/remotes/origin/${current_branch}" || exit 172 origin_branch_exists=true73 else74 echo "origin/${current_branch} does not exist; current branch is unpublished"75 fi76 fi77 git merge-base HEAD "upstream/${upstream_branch}"78 ```7980 If the repository is shallow, unshallow `origin` first when available, then `upstream` only if still shallow.8182 Always use the fetched remote-tracking ref as the target; do not decide from a previously cached `upstream/<branch>` tip.83845. Record the exact refs and report divergence:8586 ```bash87 current_head=$(git rev-parse HEAD)88 upstream_tip=$(git rev-parse "upstream/${upstream_branch}")89 git rev-list --count "upstream/${upstream_branch}..HEAD"90 git rev-list --count "HEAD..upstream/${upstream_branch}"91 GIT_PAGER=cat git log --oneline "HEAD..upstream/${upstream_branch}"92 GIT_PAGER=cat git log --first-parent --oneline "upstream/${upstream_branch}..HEAD"93 ```9495 Report `HEAD` and `upstream/${upstream_branch}` with their full SHAs.96976. Merge:9899 - Behind `0` means the fetched `upstream_tip` is already an ancestor of `current_head`. Confirm that state with both checks:100101 ```bash102 git merge-base --is-ancestor "$upstream_tip" "$current_head"103 upstream_range=$(git rev-list "$current_head..$upstream_tip") || exit 1104 test -z "$upstream_range"105 ```106107 This completes upstream integration as a successful no-op. Report the exact refs, SHAs, ancestry result, and empty range; skip the merge, release, and other change-dependent gates. Do not create an empty commit or pull request, or publish a branch solely to represent the sync. If an independent request explicitly approves pushing existing local commits, use step 9's non-destructive push semantics.108 - If behind is greater than `0` and `--ff-allow` is set with ahead `0`, run:109110 ```bash111 git merge --ff-only "upstream/${upstream_branch}"112 ```113114 - Otherwise capture `previous_head` and `upstream_tip`, then run:115116 ```bash117 git merge --no-ff "upstream/${upstream_branch}" -m "merge: sync ${current_branch} with upstream/${upstream_branch}"118 ```1191207. Resolve conflicts locally when they occur:121122 ```bash123 git diff --name-only --diff-filter=U124 ```125126 Read each conflicted file. Auto-resolve only mechanically obvious conflicts such as non-overlapping additions, import unions, formatting-only differences, or generated lockfile refreshes. For semantic conflicts, present the specific conflict and ask whether to keep ours, keep theirs, manually edit, or abort. After resolution:127128 ```bash129 git add <file>130 git -c core.editor=true merge --continue131 ```1321338. Verify:134135 ```bash136 git rev-parse --git-path MERGE_HEAD137 git rev-parse --git-path rebase-merge138 git rev-parse --git-path rebase-apply139 git show -s --format=%P HEAD140 git rev-list --left-right --count "upstream/${upstream_branch}...HEAD"141 if [ "$origin_branch_exists" = true ]; then142 git merge-base --is-ancestor "origin/${current_branch}" HEAD143 fi144 ```145146 In default mode, verify HEAD has two parents: `previous_head` as first parent and `upstream_tip` as second parent. In `--ff-allow` fast-forward mode, verify HEAD equals `upstream_tip`.1471489. Push only with explicit approval:149150 ```bash151 GIT_PAGER=cat git log --oneline --graph --decorate -10152 git push origin "${current_branch}"153 ```154155 If the remote branch does not exist, use `git push -u origin "${current_branch}"`. If push is rejected as non-fast-forward, re-fetch and offer only non-destructive options: merge `origin/<branch>` into HEAD and retry, or stop.156157## Final Report158159Include:160161- branch and upstream target, including their full SHAs162- whether merge, fast-forward, or no-op happened163- for a no-op, confirmed ancestry and the empty `HEAD..upstream/<branch>` range164- fork commits preserved165- upstream commits integrated166- push status167- any conflicts and how they were resolved