How GitHub Fork Routing Works for Pull Requests with Different Upstream and Fork URLs
Fork routing in no-mistakes separates the push destination (your fork) from the PR target (upstream), allowing you to push branches to a personal fork while opening pull requests against the parent repository using --head <fork-owner>:<branch>.
When contributing to open-source projects, developers typically work with two distinct repository URLs: the upstream original and their personal fork. The no-mistakes CLI tool implements sophisticated GitHub fork routing that keeps these URLs distinct during the contribution lifecycle, ensuring pushes go to your fork while pull requests target the upstream repository.
Understanding Fork Routing Architecture
The Two-URL Model
When you initialize a repository with no-mistakes init --fork-url <url>, the system stores two critical values in its internal database (internal/db/repo.go):
upstream_url: The parent repository URL (where youroriginremote points)fork_url: Your personal fork URL where you have write access
This separation enables a workflow where the upstream repository remains read-only for direct pushes, while your fork serves as the writable staging area for branches.
Database Storage Structure
The repository metadata is persisted with distinct fields for each URL type. According to the source code analysis, the initialization process explicitly records both endpoints to enable later routing decisions in the pipeline steps.
How the Push Step Selects the Target Remote
The routing logic lives in internal/pipeline/steps/host.go within the buildHost function. When constructing the GitHub host configuration, the code checks for the presence of a fork URL:
// internal/pipeline/steps/host.go – buildHost for GitHub
if sctx.Repo.ForkURL != "" {
// forkRepo is only used to extract the fork owner for --head owner:branch;
// the plain slug (without host prefix) is correct here.
forkRepo = github.RepoSlug(sctx.Repo.ForkURL)
}
return github.NewWithFork(..., host, repo, forkRepo), ""
When fork_url is configured:
- Push target: Becomes
repos.fork_url(your fork) - PR base repository: Remains
repos.upstream_url(the parent)
Without fork routing, the push target defaults to repos.upstream_url, which requires direct write access to the parent repository.
How the PR Step Maintains Upstream as the Base
The PR creation workflow handles the split URL architecture through specific gh CLI flag combinations. As documented in docs/src/content/docs/reference/pipeline-steps.md, the PR step:
- Keeps
--repopointed at the parent repository (derived fromupstream_url) - Checks for existing PRs using the bare branch name
- Filters matching PRs by head owner to avoid collisions
- Creates new PRs with
--head <fork-owner>:<branch>
This approach ensures the pull request's base is the upstream repository while the head points to the specific branch in your fork. The gate-model.md documentation clarifies that origin remains the parent reference throughout the process, while fork_url is used exclusively for branch pushes.
Practical Implementation Walkthrough
Initialize a repository with fork routing enabled:
# 1️⃣ Initialise a repo with a fork URL
no-mistakes init --fork-url https://github.com/alice/no-mistakes-fork.git
# → stores upstream_url = https://github.com/kunchenguid/no-mistakes.git
# and fork_url = https://github.com/alice/no-mistakes-fork.git
Push your feature branch to the fork:
# 2️⃣ Push a branch (the push target is the fork)
git push no-mistakes my-feature
# → the Push step uses repos.fork_url as the remote target
# (see internal/pipeline/steps/host.go lines 41‑46)
Create or update the pull request against upstream:
# 3️⃣ Create or update a PR (base stays upstream, head is the fork)
no-mistakes axi pr create
# Internally the PR step runs:
# gh --repo kunchenguid/no-mistakes pr create \
# --head alice:my-feature # fork owner + branch
# (see docs/reference/pipeline-steps.md, PR section)
Verify your configuration:
# 4️⃣ Verify the stored URLs (optional)
no-mistakes repo list
# Output includes:
# upstream_url: https://github.com/kunchenguid/no-mistakes.git
# fork_url: https://github.com/alice/no-mistakes-fork.git
Summary
- Fork routing requires configuring both
upstream_urlandfork_urlduring repository initialization. - The Push step in
internal/pipeline/steps/host.goroutes branches tofork_urlwhen present, falling back toupstream_urlotherwise. - The PR step maintains the upstream repository as the base while formatting the head reference as
<fork-owner>:<branch>. - This architecture prevents accidental writes to upstream while preserving the standard GitHub fork contribution workflow.
Frequently Asked Questions
What happens if I don't specify a fork URL during initialization?
Without a configured fork_url, no-mistakes defaults to using upstream_url for both pushes and pull requests. This requires direct write access to the upstream repository and follows the standard single-remote workflow.
How does the PR step differentiate between my fork and other contributors' forks?
The PR step filters existing pull requests by matching the head owner against your configured fork_url. This ensures you only interact with PRs originating from your specific fork when checking for existing branches, preventing collision with similarly named branches from other contributors.
Can I change the fork URL after initial setup?
Yes, you can update the fork_url stored in the database by re-running the initialization command with the --fork-url flag. The internal/db/repo.go implementation supports updating these configuration values to accommodate repository transfers or fork recreation scenarios.
Why does the push target change but the PR base stays the same?
This separation enforces the principle of least privilege. The upstream repository (origin) often requires maintainer-level permissions to push directly, while your fork grants you full write access. By pushing to the fork but opening PRs against upstream, you preserve the code review workflow while ensuring the upstream repository remains protected from accidental direct pushes.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →