# How no-mistakes Handles Fork Routing for GitHub PRs: A Technical Deep Dive

> Learn how no-mistakes handles fork routing for GitHub PRs by routing branch pushes and targeting upstream for PRs using a dual-URL configuration in its repos table.

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

---

**no-mistakes routes branch pushes to a contributor's fork while always targeting the upstream repository for pull request creation, using a dual-URL configuration stored in the `repos` table.**

The open-source tool **no-mistakes** (available at `kunchenguid/no-mistakes`) implements a sophisticated fork routing system that separates where code is pushed from where pull requests are opened. This architecture allows contributors to work on forks without write access to the parent repository while ensuring all GitHub PRs are created against the canonical upstream project.

## Dual-URL Configuration for Fork Routing

The system maintains two distinct remote URLs for every repository entry in its database. The **`upstream_url`** field stores the canonical GitHub repository (the parent), while the optional **`fork_url`** field contains the contributor's personal fork address.

These values are persisted in the `repos` table and drive all subsequent routing decisions. When `fork_url` is populated, the tool assumes the contributor intends to push changes to their own fork rather than the upstream repository.

## The PushURL Method: Routing Commits to the Correct Remote

The core routing logic resides in [`internal/db/repo.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/db/repo.go) within the `PushURL()` method. This function determines which remote receives branch updates by checking the presence of a configured fork:

```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
}

```

When the push step executes in [`internal/pipeline/steps/push.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/push.go), it invokes `sctx.Repo.PushURL()` to determine the destination for `git push` operations. This ensures that contributors without upstream write permissions can still publish branches to their own forks.

## GitHub PR Creation: Always Targeting Upstream

For **GitHub PR creation**, no-mistakes employs a different routing strategy than it uses for pushes. The workflow enforces that the pull request target must always be the parent repository, regardless of whether a fork is configured.

The implementation in [`internal/pipeline/steps/host.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/host.go) constructs the `gh pr create` command with the following behavior:

- The `--repo` flag always points to the upstream repository (derived from `upstream_url`)
- When `fork_url` is present, the command adds `--head <fork_owner>:<branch>` to specify the fork as the PR head
- The `fork_url` itself is never passed directly to the `gh` command; only the fork owner name is extracted from the URL

This split-routing approach ensures that while branch updates land on the contributor's fork, the pull request itself is opened against the trusted upstream repository.

## Security and Workflow Benefits

This dual-routing architecture provides critical security and convenience advantages:

- **Permission isolation**: Contributors can open PRs without requiring write access to the upstream default branch
- **Trusted targets**: Maintainers control the target branch configuration on the upstream repository, preventing accidental PRs against incorrect branches
- **Clean history**: Branch updates occur on the fork, keeping the upstream repository's branch list uncluttered

## Summary

- **no-mistakes** stores two URLs per repository: `upstream_url` for the parent repo and `fork_url` for contributor forks
- The `PushURL()` method in [`internal/db/repo.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/db/repo.go) prioritizes the fork URL when determining where to push branches
- Push operations in [`internal/pipeline/steps/push.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/push.go) use the fork when available, enabling contributions without upstream write access
- PR creation always targets the upstream repository via `--repo`, using `--head <fork_owner>:<branch>` when a fork is configured
- The `fork_url` is never used directly for PR creation; only the owner name is extracted in [`internal/pipeline/steps/host.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/host.go)

## Frequently Asked Questions

### What is fork routing in no-mistakes?

Fork routing is the system's mechanism for directing Git operations to different remotes based on context. It pushes code to a contributor's fork (when configured) while ensuring GitHub pull requests are always created against the upstream parent repository.

### How does no-mistakes decide whether to push to a fork or upstream?

The decision occurs in the `PushURL()` method inside [`internal/db/repo.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/db/repo.go). If the `ForkURL` field contains a non-empty string, that URL is returned and used for push operations. Otherwise, the method falls back to `UpstreamURL`.

### Why doesn't no-mistakes use the fork URL directly when creating PRs?

The tool intentionally avoids passing the fork URL to `gh pr create` because GitHub CLI requires the `--repo` flag to specify the target repository for the PR (which must be the upstream), while the fork is referenced only as the head source via `--head <owner>:<branch>`. This maintains the security model where the upstream repository controls the merge target.

### Which database fields control fork routing behavior?

The routing behavior is controlled by two columns in the `repos` table: `upstream_url` (the canonical parent repository) and `fork_url` (the optional contributor fork). The presence of a value in `fork_url` triggers fork-specific routing for pushes and modifies the PR creation command to include the fork owner prefix.