# How no-mistakes Handles Fork Routing for GitHub PRs

> Learn how no-mistakes reliably routes GitHub PRs using separate upstream and fork URLs. Ensure your pull requests always target the parent repository with this smart solution.

- Repository: [Kun Chen/no-mistakes](https://github.com/kunchenguid/no-mistakes)
- Tags: how-to-guide
- Published: 2026-07-02

---

**no-mistakes maintains separate `upstream_url` and `fork_url` fields to route commits to a contributor's fork while ensuring pull requests always target the parent repository.**

The `no-mistakes` automation tool streamlines GitHub workflows by solving a critical routing challenge: pushing code to a personal fork while opening PRs against the upstream repository. Understanding **fork routing for GitHub PRs** in this codebase requires examining how the system distinguishes between write targets and PR base repositories using a dual-URL architecture.

## The Dual-URL Architecture

At the core of the routing logic lies a simple but effective data model that tracks two distinct remote URLs for every repository.

### Upstream vs. Fork Remotes

The `repos` table stores repository metadata using two key fields:

- **`upstream_url`**: The canonical GitHub repository (the parent repo) that serves as the official source of truth.
- **`fork_url`**: An optional URL pointing to a contributor's personal fork, stored in the `fork_url` column.

This separation enables the system to make intelligent decisions about where to send changes based on the operation type.

## Push Operations: Routing Commits to the Fork

When propagating branch updates, `no-mistakes` prioritizes the contributor's fork to avoid requiring write permissions on the upstream repository.

### The PushURL Method Implementation

In [`internal/db/repo.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/db/repo.go) (lines 19–27), the `PushURL` method implements the precedence logic:

```go
// PushURL returns the remote URL that should receive branch updates.
func (r *Repo) PushURL() string {
    if r == nil {
        return ""
    }
    if strings.TrimSpace(r.ForkURL) != "" {
        return r.ForkURL        // fork takes precedence
    }
    return r.UpstreamURL          // otherwise push to the parent
}

```

The method returns `r.ForkURL` when configured, falling back to `r.UpstreamURL` only when no fork exists.

### Execution in the Push Pipeline

The push step located in [`internal/pipeline/steps/push.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/push.go) invokes this logic through `sctx.Repo.PushURL()`. This ensures that when a contributor runs a push command, the commits land on their fork rather than attempting direct writes to the upstream repository.

## Pull Request Creation: Targeting the Upstream Repository

Despite routing pushes to forks, PR creation follows a different security model that always uses the parent repository as the base.

### Excluding Fork URLs from PR Commands

The [`internal/pipeline/steps/host.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/host.go) file contains guards that prevent direct use of `fork_url` during PR creation. According to the source code, the `fork_url` itself is **never** passed to the `gh pr create` command. Instead, the tool derives only the fork owner name from the URL.

This design preserves the security model where the maintainer's trusted default-branch configuration supplies the PR target, while the contributor's fork merely supplies the head reference.

### Constructing the Head Reference

When a fork is configured, `no-mistakes` constructs the PR head using the `--head <fork_owner>:<branch>` syntax. The command structure becomes:

```bash
gh pr create \
  --repo parent_owner/parent_repo \
  --head fork_owner:feature_branch \
  --title "Commit message" \
  --body "PR description"

```

This approach directs GitHub to compare the fork's branch against the upstream repository's default branch without requiring the fork URL to be specified as the repository target.

## Security and Workflow Implications

The dual-routing strategy provides two critical guarantees:

- **Contributor autonomy**: Branch updates go to the fork when `fork_url` is set, eliminating the need for write permissions on the upstream repo.
- **Repository integrity**: Pull-request creation always targets the upstream repository, ensuring that maintainers control the merge target and base branch configuration.

## Practical Workflow Examples

The following commands demonstrate the fork routing behavior in practice:

```bash

# Push a feature branch – it lands on the fork if fork_url is set

$ no-mistakes push --branch feature/foo

# Internally: git push <fork_url> feature/foo

# Open a PR using the upstream repository as the base

$ no-mistakes pr create --title "Add foo" --body "Fixes #123"

# Internally (fork present):

#   gh pr create \

#     --repo parent_owner/parent_repo \

#     --head fork_owner:feature/foo \

#     --title "Add foo" \

#     --body "Fixes #123"

```

## Summary

- `no-mistakes` stores repository metadata in the `repos` table using `upstream_url` and `fork_url` fields.
- The `PushURL()` method in [`internal/db/repo.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/db/repo.go) prioritizes `fork_url` for push operations, falling back to `upstream_url` when empty.
- The push step in [`internal/pipeline/steps/push.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/push.go) calls `sctx.Repo.PushURL()` to determine the remote destination.
- PR creation always targets the upstream repository, with [`internal/pipeline/steps/host.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/host.go) preventing direct `fork_url` usage.
- Fork branches are referenced using `--head <fork_owner>:<branch>` syntax rather than specifying the fork URL as the repository target.

## Frequently Asked Questions

### How does no-mistakes decide which remote receives my commits?

The `PushURL()` method in [`internal/db/repo.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/db/repo.go) checks the `fork_url` field first. If a non-empty fork URL exists in the repository configuration, commits route there; otherwise, they push to the `upstream_url`. This logic executes during the push step in [`internal/pipeline/steps/push.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/push.go).

### Why does no-mistakes ignore the fork_url when creating pull requests?

The tool intentionally excludes the `fork_url` from `gh pr create` commands to maintain security and consistency. Pull requests must target the upstream repository to ensure maintainers control the base branch and merge settings. The fork only contributes the head reference via the `--head` flag, not the repository target.

### What happens if I configure only an upstream_url without a fork_url?

Without a configured `fork_url`, the `PushURL()` method returns the `upstream_url` for all operations. Both pushes and PR creation will reference the parent repository directly, requiring appropriate write permissions for branch creation.

### Where is the fork routing logic documented in the source code?

Key implementations reside in three files: [`internal/db/repo.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/db/repo.go) defines the `PushURL` method; [`internal/pipeline/steps/push.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/push.go) executes pushes using that URL; and [`internal/pipeline/steps/host.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/host.go) handles the PR creation logic that excludes fork URLs from the target repository parameter.