When managing multi-tiered Git architectures—such as an internal development environment, a local dispatch/staging repository, and an upstream GitHub host—handling pull requests (PRs) without introducing messy merge commits or web UI dependencies can be challenging.

A common setup involves applying an external PR locally as a patch, testing and validating it inside private branches, squashing it into a curated release branch, and then pushing back upstream. However, GitHub usually only marks a PR as merged (the coveted purple badge) under two conditions:

  • A merge commit containing the PR branch's tip SHA is pushed to GitHub's default branch.
  • A rebased or squashed commit exactly matches the tree and metadata of the PR commit via the GitHub API or UI.

If you need to close the PR automatically as merged using purely local Git commands while maintaining a single commit that matches your release branch commit ID, here is how you can achieve this.

The Core Challenge: GitHub PR Matching Logic

GitHub recognizes a pull request as "Merged" rather than "Closed" when the target branch (e.g., main) receives a commit history that directly references the PR branch head commit. If you export a patch and cherry-pick or commit it anew on top of newer commits, the Git tree changes and commit SHAs diverge. GitHub treats the new commit as unrelated and marks the PR as "Closed with unmerged commits" instead of "Merged".

To preserve your single-commit policy while still informing GitHub that the PR branch was incorporated, you have two primary native Git solutions: synthetic two-parent squashing or rebasing the PR branch locally before fast-forwarding.

Solution 1: Create a Two-Parent Squash Commit (Preserves Single Commit & PR Link)

If you want one single commit on your release/main branch that links your upstream base and the PR branch together, you can create a custom merge commit that acts as a squash merge using low-level Git plumbing commands (git commit-tree).

Suppose you are in your Dispatch repository:

# 1. Fetch the PR branch from GitHub into a local tracking branch
git fetch origin +refs/pull/123/head:refs/heads/pr/123

# 2. Pull your tested single-commit release from your development repo
git fetch dev release:refs/heads/dev-release

# 3. Create a synthetic merge commit that contains:
#    - Tree: The exact tree state of your new dev-release commit
#    - Parent 1: The current origin/main
#    - Parent 2: The tip of refs/heads/pr/123
TREE_ID=$(git rev-parse dev-release^{tree})
MSG=$(git log -1 --format=%B dev-release)

SYNTHETIC_COMMIT=$(git commit-tree $TREE_ID \
  -p origin/main \
  -p pr/123 \
  -m "$MSG")

# 4. Fast-forward your main branch to this new commit
git update-ref refs/heads/main $SYNTHETIC_COMMIT

# 5. Push to GitHub
git push origin main

Why this works:

  • GitHub examines main, sees pr/123 listed as the second parent of the latest commit, and detects that the PR head was merged into main. The PR automatically turns purple (Merged).
  • The commit snapshot contains the exact code from your release branch, without conflicts or multi-step rebase noise.

Solution 2: Rebase the PR Locally and Fast-Forward

If strict linear history (a single-parent commit) is preferred over a two-parent merge, you can adopt the local rebase-and-fast-forward strategy directly on the PR branch head.

Instead of manually creating a standalone patch and committing it in an orphan branch, pull the PR branch into your dispatch repo, rebase it on top of your local main, squash it down to one commit, and apply any necessary adjustments:

# 1. Fetch PR branch
git fetch origin +refs/pull/123/head:refs/heads/pr/123

# 2. Rebase onto current main
git checkout pr/123
git rebase origin/main

# 3. Squash PR changes into a single commit with your conventional commit message
git reset --soft origin/main
git commit -m "fix: resolve issue (#123)"

# 4. Fast-forward main to this commit and push
git checkout main
git merge --ff-only pr/123
git push origin main

GitHub will compare the tree contents and the PR commit subject, detect the squashed resolution or fast-forward lineage, and mark the PR as merged without requiring the browser.

Synchronizing the Development Repository

To ensure your development repository stays aligned and avoids generating "ghost commits":

  1. Do not fabricate the final commit SHA in the isolated development repo.
  2. Instead, execute the merge in the dispatch repository (which has access to both GitHub references and the development repo).
  3. Once pushed to GitHub, pull that final commit back down into your development repository's release tracking branch:
# In the development repo
git pull dispatch main:release

By treating the dispatch repository as the single point of truth where upstream references (GitHub PRs) and downstream updates converge, your commit IDs remain consistent across all three tiers.