# How GitHub Fork Routing Works for Pull Requests with Different Upstream and Fork URLs

> Understand GitHub fork routing for pull requests. Learn how to push branches to your fork while targeting the upstream repository with --head. Master PR workflows with no-mistakes.

- Repository: [Kun Chen/no-mistakes](https://github.com/kunchenguid/no-mistakes)
- Tags: deep-dive
- Published: 2026-07-17

---

**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`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/db/repo.go)):

- **`upstream_url`**: The parent repository URL (where your `origin` remote 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`](https://github.com/kunchenguid/no-mistakes/blob/main/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:

```go
// 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`](https://github.com/kunchenguid/no-mistakes/blob/main/docs/src/content/docs/reference/pipeline-steps.md), the PR step:

1. Keeps `--repo` pointed at the parent repository (derived from `upstream_url`)
2. Checks for existing PRs using the bare branch name
3. Filters matching PRs by head owner to avoid collisions
4. 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`](https://github.com/kunchenguid/no-mistakes/blob/main/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:

```bash

# 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:

```bash

# 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:

```bash

# 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:

```bash

# 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_url` and `fork_url` during repository initialization.
- The **Push step** in [`internal/pipeline/steps/host.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/host.go) routes branches to `fork_url` when present, falling back to `upstream_url` otherwise.
- 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`](https://github.com/kunchenguid/no-mistakes/blob/main/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.