How GitHub Fork Routing Works with Parent Repository and Fork URL Configuration
The no-mistakes CLI implements GitHub fork routing by storing both an upstream_url (parent repository) and an optional fork_url in a SQLite database, using resolveUpstreamURL to determine PR targets and resolvePushURL to determine push destinations.
The no-mistakes tool automates Git workflows while respecting GitHub's fork-based collaboration model. Understanding how it handles GitHub fork routing requires examining how it stores and resolves repository relationships in its internal database and pipeline steps. This architecture ensures contributors push to their forks while opening pull requests against the correct parent repository.
The Dual-URL Database Schema
At the core of the routing mechanism lies the SQLite schema defined in internal/db/schema.go. The repos table maintains two distinct URL columns for every repository:
upstream_url— The URL of the parent (original) repository.fork_url— An optional URL of a personal fork that a contributor pushes to.
These columns are defined in lines 8-11 of the schema file. When you run no-mistakes init --fork-url <url>, the CLI persists the fork URL in this column, while the upstream URL is automatically extracted from the current clone's origin remote.
Resolving Repository URLs at Runtime
The pipeline logic that determines where to push code or create pull requests resides in internal/pipeline/steps/common_git.go. This file implements two critical resolution functions that handle the routing decisions.
Determining the Upstream Parent
The resolveUpstreamURL function (lines 55-73) handles upstream resolution with credential safety in mind. It first attempts to read the origin remote from the local worktree, which may contain embedded credentials. If this live remote matches the redacted upstream URL stored in the database, the function returns the credential-rich version from the worktree. Otherwise, it falls back to the database's upstream_url value.
This approach ensures that authentication tokens remain only in the local Git configuration while the database stores sanitized URLs.
Determining the Push Destination
The resolvePushURL function (lines 92-97) determines where commits are pushed. It returns the fork_url if the repository record contains a non-empty value. If no fork is configured, it falls back to the upstream URL resolved by resolveUpstreamURL.
// Example: Resolve the URL to push to
pushURL := resolvePushURL(stepCtx) // returns fork URL if set, otherwise upstream URL
fmt.Println("Push will be made to:", pushURL)
Pull Request Creation and Routing
When creating pull requests, the tool must distinguish between the target repository (parent) and the source repository (fork). The logic documented in internal/pipeline/steps/host.go (lines 52-66) clarifies that while the push step may use fork_url, the PR creation step must target the parent repository.
The GitHub adapter implements this by creating a PR from <fork_owner>:<branch> to the parent repository. The code uses resolveUpstreamURL to identify the target repository and resolvePushURL to identify the head repository:
// Example: Creating a PR on GitHub (simplified)
parent := resolveUpstreamURL(stepCtx) // target repository (parent)
head := resolvePushURL(stepCtx) // fork repository (head)
gh.CreatePR(parent, head, branchName) // PR created from fork → parent
Initializing Fork Configuration
To configure the fork routing, use the initialization command defined in internal/cli/init.go:
no-mistakes init --fork-url https://github.com/your-username/forked-repo.git
This command stores the provided URL in the fork_url column of the repos table. If omitted, the system operates in "direct mode," pushing to and creating PRs against the upstream repository exclusively.
Summary
The no-mistakes fork routing system guarantees three critical behaviors:
- Correct push destination — When a fork URL is configured, pushes go to the fork; otherwise, they go to the upstream parent.
- Accurate PR routing — Pull requests are always opened against the parent repository, with the head reference pointing to the fork (or the same repo when no fork exists).
- Credential safety — Embedded credentials live only in the local
originremote; the database stores redacted URLs, andresolveUpstreamURLprefers the live remote when it matches the stored value.
Frequently Asked Questions
What happens if no fork URL is configured?
If the fork_url column is empty, resolvePushURL returns the upstream URL, causing both pushes and pull requests to target the parent repository directly. This effectively disables the fork workflow and treats the repository as the canonical source.
How does no-mistakes handle credentials in repository URLs?
The system stores redacted URLs in the SQLite database (lacking embedded tokens) while reading the actual remote configuration from the local Git worktree. The resolveUpstreamURL function compares the worktree's origin remote against the stored redacted URL, returning the credential-rich version only when they match, ensuring tokens never persist in the database.
Can I change the fork URL after initialization?
Yes. Running no-mistakes init --fork-url <new-url> updates the fork_url column in the database for the current repository. The next pipeline execution will use the new URL for push operations and pull request head references.
Why does the PR creation step use the parent repository as the target?
According to the implementation in internal/pipeline/steps/host.go, the PR must target the upstream (parent) repository because that is where code review and merging occurs. The fork serves only as the source of changes. This aligns with GitHub's standard fork-and-pull workflow, where contributors push to their fork but request changes be merged into the original repository.
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 →