Fall back to effective URL for forge detection with url.insteadOf - #1771
Conversation
When users configure `url.insteadOf` for multi-key SSH setups, the raw config URL may have a custom hostname (e.g., `work-ssh`) that isn't recognized as a known forge. This breaks forge detection, CI status, and `gh` API calls. **What:** - Add `GitRemoteUrl::is_known_forge()` — checks if hostname matches github or gitlab - Add `Repository::effective_remote_url()` — reads URL with insteadOf rewrites applied via `git remote get-url` - Add `Repository::forge_remote_url()` — tries raw URL first, falls back to effective URL when hostname isn't a known forge - Add `Repository::primary_forge_remote_url()` — forge-aware variant of `primary_remote_url()` - Update all forge detection consumers: `platform_for_repo()`, `github_owner_repo()`, `fetch_pr_info()`, `github_push_url()` **Why:** Users with SSH multi-key setups (work vs personal) configure custom hostnames in `~/.ssh/config` and map them via `url.insteadOf`. When the raw `.git/config` URL has the custom hostname, worktrunk can't identify the forge. The effective URL (via `git remote get-url`) may contain the real forge hostname after insteadOf rewrites are applied.
worktrunk-bot
left a comment
There was a problem hiding this comment.
CI is failing due to 6 collapsible_if clippy lints — nested if statements that can be collapsed using let chains. The code itself is correct; just needs the nesting flattened.
Locations:
src/git/repository/remotes.rs:98,:105,:106,:107— inforge_remote_url()src/git/repository/branch.rs:188,:189— ingithub_push_url()
For example in forge_remote_url(), the three nested ifs at lines 105-117 can become:
if let Some(effective_url) = self.effective_remote_url(remote)
&& effective_url != raw_url
&& let Some(parsed) = GitRemoteUrl::parse(&effective_url)
&& parsed.is_known_forge()
{
log::debug!(...);
return Some(effective_url);
}Same pattern applies to the other locations. There's also a formatting lint from pre-commit (collapsing a multi-line run_git call in the test).
Collapse nested `if` statements into `let` chains as required by clippy, and apply cargo fmt formatting.
codecov/patch failed — uncovered fallback paths in consumer code
worktrunk-bot
left a comment
There was a problem hiding this comment.
codecov/patch is failing — the core forge_remote_url() function is well-tested, but the consumer-side fallback paths that call it are uncovered:
github_owner_repo()fallback loop (src/commands/list/ci_status/github.rs:32-42) — only runs when all raw URLs are non-GitHub and an insteadOf rewrite resolves to GitHubplatform_for_repo()fallback loop (src/commands/list/ci_status/platform.rs:173-187) — same pattern for platform detectiongithub_push_url()fallback (src/git/repository/branch.rs:187-195) — requires custom push remote with insteadOfprimary_forge_remote_url()(src/git/repository/remotes.rs:214-218) — only called fromfetch_pr_info()which isn't integration tested
These are tricky to test since the consumers also need gh CLI or push remote configs. A few options:
- Unit tests with a helper repo that has custom hostname + insteadOf configured, calling
github_owner_repo()andplatform_for_repo()directly (they'repub/ module-visible) - Extract and test the fallback iteration — the two-pass "raw URLs then forge_remote_url fallback" pattern is duplicated across
github_owner_repoandplatform_for_repo. A shared helper likefind_forge_remote_url(repo, |parsed| parsed.is_github())would be testable independently and reduce duplication - Accept partial coverage if the consumer paths are considered sufficiently covered by the
forge_remote_urlunit tests (the consumer logic is just iteration + delegation)
|
is setting the type of forge in the configs an alternative? does this require an additional network request, and if so, when? @amodelaweb if we did go ahead, we would need to get the test coverage much higher, potentially by consolidating some of the code |
Extract the two-pass "raw URLs then insteadOf fallback" iteration into Repository::find_forge_remote(predicate). This eliminates duplication between github_owner_repo() and platform_for_repo(), and makes the fallback logic directly testable. Add 5 new integration tests covering find_forge_remote (both fast path and insteadOf fallback), primary_forge_remote_url, effective_remote_url, and github_push_url with insteadOf.
|
Good questions — answering each: Config alternative: There's already Network requests: None. Test coverage: Agreed — the codecov failures are real gaps. Consolidating the two-pass "raw URLs then effective URL fallback" iteration into |
|
Thanks for the review @max-sixty ! Config alternative: Yeah, I think both approaches have merit. The config approach ( Network requests: Zero — Test coverage: Agreed. I've already consolidated the two-pass iteration into Let me know which direction you'd like to go on the config vs insteadOf question — happy to adjust. |
|
@worktrunk-bot please make those changes to improve the coverage @amodelaweb thanks for the PR; please don't paste AI output from your handle. we have a bot for that! |
Cover the uncovered paths in github_push_url (non-GitHub forge, insteadOf to non-GitHub), find_forge_remote (no remotes), forge_remote_url (insteadOf to unknown forge, nonexistent remote), and effective_remote_url (nonexistent remote). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Added 6 tests covering the uncovered edge cases in the forge URL fallback code:
|
|
@worktrunk-bot please do a big consolidation, I see a bunch of repetition. run /simplify |
- find_forge_remote: call effective_remote_url directly instead of going through forge_remote_url (avoids redundant re-parsing and re-checking of raw URLs already examined in the fast path) - github_push_url: simplify by delegating to forge_remote_url instead of manually reimplementing the fallback logic - Tests: extract setup_insteadof/setup_push_tracking helpers, merge the two nonexistent-remote tests, trim comments that restate code Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Consolidated the forge URL fallback logic (-72 lines net): Source code:
Tests:
All 492 unit + 48 default_branch integration tests pass. Waiting on CI. |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@worktrunk-bot can we write a spec at the top of a module of exactly how we resolve what forge to use? |
Documents the two-pass resolution algorithm (raw URL → insteadOf fallback → best effort) and the entry points with their consumers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Added a module-level spec to
Kept it concise — the spec describes the contract, individual doc comments on each function cover the details. |
| //! 3. **Best effort** — if neither URL resolves to a known forge, return the | ||
| //! raw URL so callers can still attempt to parse owner/repo from it. |
There was a problem hiding this comment.
Why have logic in the caller? why not have all the logic in this one function, and otherwise fail?
There was a problem hiding this comment.
Good point — changed forge_remote_url to return None when neither the raw nor effective URL resolves to a known forge, instead of falling back to the raw URL. This lets all forge-resolution logic live inside that one function.
This also simplified github_push_url — it was manually doing a raw-URL check then falling back to forge_remote_url, but forge_remote_url already does both passes internally. Removed the duplicated logic and the now-unused push_remote_url method.
|
@worktrunk-bot note my question above |
Instead of returning the raw URL as a best-effort fallback when neither the raw nor effective URL resolves to a known forge, return None. This keeps all forge-resolution logic inside forge_remote_url and eliminates redundant is_github/is_known_forge checks in callers. Simplifies github_push_url by removing its manual raw-URL-then-fallback pattern — forge_remote_url already does both passes internally. Also removes the now-unused push_remote_url method. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CI failed — snapshot mismatch + fmt lint
worktrunk-bot
left a comment
There was a problem hiding this comment.
CI is failing with two issues:
1. cargo fmt lint failure — a multi-line run_git call in test_forge_remote_url_known_forge_no_fallback needs to be collapsed to one line.
2. Snapshot mismatch in test_list_full_with_url_based_pushremote — this is a real regression from removing push_remote_url(). The test simulates gh pr checkout behavior where branch.feature.pushremote is set to a URL (https://github.com/fork-owner/test-repo.git), not a remote name.
The old push_remote_url() handled this via %(push:remotename) — if the result was already a URL (contains :// or starts with git@), it returned it directly. The new github_push_url() delegates to push_remote() which uses @{push} and extracts the remote name from the result. But @{push} fails when pushremote is a URL because there's no corresponding remote-tracking ref.
Fix options:
- Restore the URL-detection logic from
push_remote_url()insidegithub_push_url()— check ifpush_remote()returnsNone, then fall back to reading%(push:remotename)and checking if it's a URL - Or add a
push_remote_url()method back that handles the URL-as-pushremote case, and callforge_remote_urlonly when it returns a remote name
gh pr checkout sets branch.<name>.pushRemote to a URL rather than a remote name. Restore push_remote_url() (now private) to handle this case via %(push:remotename), then fall back to forge_remote_url for insteadOf alias resolution. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
worktrunk-bot
left a comment
There was a problem hiding this comment.
CI is failing — all three jobs (lint, test linux, test macos) fail on the same cargo fmt issue: the multi-line run_git call at tests/integration_tests/default_branch.rs:375-379 needs collapsing to a single line. This was flagged in a previous review but the latest push didn't include the fix.
The new push_remote_url() method and github_push_url() restructuring look correct — restoring URL-based pushremote support (for gh pr checkout fork PRs) while keeping insteadOf fallback via forge_remote_url().
Re @max-sixty's question about a config alternative (forge = "github"): the PR discussion surfaced this but it's still an open design question. The author offered to go either direction. If the preference is config-only, this PR's scope would shrink significantly. Worth deciding before further iteration on coverage/consolidation.
Co-authored-by: worktrunk-bot <w@worktrunk.dev>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
worktrunk-bot
left a comment
There was a problem hiding this comment.
Re the open design question — config vs insteadOf inference:
The two approaches aren't mutually exclusive, but I'd lean toward config-first (forge = "github" or extending the existing [ci] platform):
- Explicit config is more predictable — insteadOf inference works when the rewrite points to a known forge, but silently gives up when it doesn't. A config value always works.
- The existing
[ci] platformconfig already solves half the problem — CI status detection already has a config path. Extending it (or addingforge) to coverwt switch pr:and push remote detection would be consistent. - insteadOf detection is a nice fallback — zero-config is genuinely better UX for the happy path. But it adds ~100 lines of code and subprocess spawns for what a one-line config would solve.
A reasonable path: add a forge config option as the primary mechanism, and keep the insteadOf inference as an optional fallback (or defer it to a follow-up if it's wanted later). That would shrink this PR significantly.
This is a design call — flagging for human decision.
…s logic Instead of a two-pass strategy (raw URL fast path → insteadOf fallback), always use `git remote get-url` (which applies insteadOf rewrites) and cache results per-remote in RepoCache. This eliminates the need for forge_remote_url, is_known_forge, and per-caller fallback logic. - Cache effective_remote_url in RepoCache via DashMap - Remove forge_remote_url, is_known_forge, primary_forge_remote_url - Simplify find_forge_remote from two-pass to single-pass - Simplify github_push_url from 7-line fallback to 3-line check - Fix detect_github using raw URL for remote branch owner lookup - Rename primary_forge_remote_url → primary_effective_remote_url Co-Authored-By: Claude <noreply@anthropic.com>
Refactor find_remote_for_repo to check both raw config URLs and effective URLs (with url.insteadOf rewrites applied). This enables matching remotes when the raw URL contains a custom hostname that gets rewritten to a real forge hostname, supporting both directions: detecting a known forge from a custom hostname, and matching a real forge URL to a remote configured with a custom hostname. Add integration tests covering insteadOf scenarios with single/multiple remotes and case-insensitive matching.
The convenience method had exactly one caller (fetch_pr_info). Inlining it reduces the API surface without changing behavior. Co-Authored-By: Claude <noreply@anthropic.com>
Adds Azure DevOps as a third forge alongside GitHub and GitLab: - `wt switch pr:N` dispatches to GitHub or Azure DevOps automatically based on the repo's configured remotes (GitHub still wins in mixed-remote setups). - `wt list --full` surfaces Azure DevOps PR and pipeline status via the `az` CLI. - `forge.platform` accepts `azure-devops` as an override. - `wt config show --full` reports `az` install/auth state when the platform is detected as Azure DevOps. Implementation notes: - New `AzureDevOpsProvider` implements `RemoteRefProvider`; auto-detects the organisation from any Azure DevOps remote URL so contributors don't have to pass `--org`. - `GitRemoteUrl` learns `is_azure_devops()`, `azure_organization()`, and `azure_project()` to parse `dev.azure.com`, `ssh.dev.azure.com`, and legacy `*.visualstudio.com` URLs (which don't fit the standard host/owner/repo shape). - Pipeline URLs are constructed from org/project/build ID rather than the API's `url` field (which is a REST endpoint, not a browser URL). - Requires the `azure-devops` extension (`az extension add --name azure-devops`). Originally proposed by @mikeyroush in max-sixty#1256; reimplemented against current `main` to pick up the `[forge]` config section (max-sixty#1826), `url.insteadOf` fallback (max-sixty#1771), and `handle_switch` consolidation (max-sixty#2597) that landed after the original branch. Co-Authored-By: mroush <me@mikeyroush.com> Co-Authored-By: Claude <noreply@anthropic.com>
When users configure
url.insteadOffor multi-key SSH setups (e.g.,git@github-work:org/repo.gitrewritten togit@github.com:org/repo.git), the raw remote URL has a custom hostname that doesn't match any known forge. This breaks CI status, PR detection, and push-remote features.The fix: add
effective_remote_urlwhich usesgit remote get-url(appliesinsteadOfrewrites) and cache results per-remote inRepoCache. This is a local operation (no network I/O). When noinsteadOfrules are configured, the effective URL equals the raw URL — no behavioral change. Bothremote_url(raw) andeffective_remote_url(rewritten) are needed becauseinsteadOfcan be configured in either orientation relative to forge URLs — forward (custom hostname → real forge, e.g. multi-key SSH) or reverse (real forge → internal destination, e.g. corporate mirrors).Key changes:
effective_remote_urlcached per-remote viaDashMap— free after first callfind_forge_remote,platform_for_repo,github_push_url,detect_github,fetch_pr_info) useeffective_remote_urlfind_remote_for_repochecks both raw and effective URLs (needs both for forward and reverseinsteadOfscenarios)forge_remote_url,is_known_forge,primary_forge_remote_url— superseded by the cachedeffective_remote_urldetect_githubusing rawremote_urlfor remote branch owner lookup