diff --git a/src/git/remote_ref/azure.rs b/src/git/remote_ref/azure.rs index cee7dade80..2e7d4935f7 100644 --- a/src/git/remote_ref/azure.rs +++ b/src/git/remote_ref/azure.rs @@ -122,22 +122,30 @@ struct AzForkRepository { /// Prefers the primary remote (typically `origin` or whatever the user pushed /// with) so fork workflows hit the right tenant. Falls back to the first /// Azure remote found if the primary isn't Azure DevOps. +/// +/// The host is a web host, the same shape [`parse_web_url`] returns: an +/// `ssh.dev.azure.com` remote reports `dev.azure.com`, since the URL builders +/// that consume it put the host into an HTTPS URL. fn detect_azure_target(repo: &Repository) -> Option<(String, String)> { + let target = |parsed: &GitRemoteUrl| { + let org = parsed.azure_organization()?; + let host = if host_is_within(parsed.host(), "visualstudio.com") { + parsed.host() + } else { + "dev.azure.com" + }; + Some((host.to_string(), org.to_string())) + }; if let Ok(remote) = repo.primary_remote() && let Some(url) = repo.effective_remote_url(&remote) && let Some(parsed) = GitRemoteUrl::parse(&url) - && let Some(org) = parsed.azure_organization() + && let Some(found) = target(&parsed) { - return Some((parsed.host().to_string(), org.to_string())); - } - for (_, url) in repo.all_remote_urls() { - if let Some(parsed) = GitRemoteUrl::parse(&url) - && let Some(org) = parsed.azure_organization() - { - return Some((parsed.host().to_string(), org.to_string())); - } + return Some(found); } - None + repo.all_remote_urls() + .into_iter() + .find_map(|(_, url)| target(&GitRemoteUrl::parse(&url)?)) } /// Build the `--org` URL for the `az` CLI from a host and organization. diff --git a/tests/integration_tests/switch.rs b/tests/integration_tests/switch.rs index 0ae82f40e1..6a26438dcd 100644 --- a/tests/integration_tests/switch.rs +++ b/tests/integration_tests/switch.rs @@ -6787,6 +6787,41 @@ fn test_switch_pr_azure_fork(#[from(repo_with_remote)] repo: TestRepo) { }); } +/// With no `webUrl` in the response, the org and host come from the local +/// remote. An `ssh.dev.azure.com` remote must still suggest an HTTPS +/// `dev.azure.com` URL for the PR's repository, not one on the SSH host. +#[rstest] +fn test_switch_pr_azure_ssh_remote_suggests_web_host(#[from(repo_with_remote)] repo: TestRepo) { + repo.run_git(&[ + "remote", + "set-url", + "origin", + "git@ssh.dev.azure.com:v3/myorg/myproject/test-repo", + ]); + + let az_response = r#"{ + "title": "Fix in a sibling repository", + "createdBy": {"uniqueName": "alice@example.com"}, + "status": "active", + "isDraft": false, + "sourceRefName": "refs/heads/feature-auth", + "repository": { + "name": "other-repo", + "project": {"name": "myproject"} + }, + "forkSource": null + }"#; + + let mock_bin = setup_mock_az(&repo, az_response); + + let settings = setup_snapshot_settings(&repo); + settings.bind(|| { + let mut cmd = make_snapshot_cmd(&repo, "switch", &["pr:101"], None); + configure_mock_cli_env(&mut cmd, &mock_bin); + assert_cmd_snapshot!("switch_pr_azure_ssh_remote_suggests_web_host", cmd); + }); +} + /// A missing PR reaches the user as the `TF401174` line `az` printed. /// /// Once the extension question is settled, `azure::fetch_pr_info` classifies diff --git a/tests/snapshots/integration__integration_tests__switch__switch_pr_azure_ssh_remote_suggests_web_host.snap b/tests/snapshots/integration__integration_tests__switch__switch_pr_azure_ssh_remote_suggests_web_host.snap new file mode 100644 index 0000000000..cbafc68ec5 --- /dev/null +++ b/tests/snapshots/integration__integration_tests__switch__switch_pr_azure_ssh_remote_suggests_web_host.snap @@ -0,0 +1,66 @@ +--- +source: tests/integration_tests/switch.rs +info: + program: wt + args: + - switch + - "pr:101" + env: + APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" + CLICOLOR_FORCE: "1" + COLUMNS: "500" + GIT_ALLOW_PROTOCOL: file + GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" + GIT_AUTHOR_EMAIL: test@example.com + GIT_AUTHOR_NAME: Test User + GIT_COMMITTER_DATE: "2025-01-01T00:00:00Z" + GIT_COMMITTER_EMAIL: test@example.com + GIT_COMMITTER_NAME: Test User + GIT_CONFIG_COUNT: "2" + GIT_CONFIG_GLOBAL: /nonexistent/wt/gitconfig + GIT_CONFIG_KEY_0: user.useConfigOnly + GIT_CONFIG_KEY_1: rerere.enabled + GIT_CONFIG_SYSTEM: /nonexistent/wt/gitconfig + GIT_CONFIG_VALUE_0: "true" + GIT_CONFIG_VALUE_1: "false" + GIT_TERMINAL_PROMPT: "0" + HOME: "[TEST_HOME]" + LANG: C + LC_ALL: C + LLVM_PROFILE_FILE: "[LLVM_PROFILE_FILE]" + OPENCODE_CONFIG_DIR: "[TEST_OPENCODE_CONFIG]" + PATH: "[PATH]" + TERM: alacritty + USERPROFILE: "[TEST_HOME]" + WORKTRUNK_APPROVALS_PATH: "[TEST_APPROVALS]" + WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]" + WORKTRUNK_SYSTEM_CONFIG_PATH: "[TEST_SYSTEM_CONFIG]" + WORKTRUNK_TEST_BASH_INSTALLED: "0" + WORKTRUNK_TEST_CLAUDE_INSTALLED: "0" + WORKTRUNK_TEST_CODEX_INSTALLED: "0" + WORKTRUNK_TEST_DELAYED_STREAM_MS: "-1" + WORKTRUNK_TEST_EPOCH: "1735776000" + WORKTRUNK_TEST_FISH_INSTALLED: "0" + WORKTRUNK_TEST_GEMINI_INSTALLED: "0" + WORKTRUNK_TEST_MOCK_CONFIG_DIR: "[TEST_MOCK_CONFIG]" + WORKTRUNK_TEST_NUSHELL_ENV: "0" + WORKTRUNK_TEST_OPENCODE_INSTALLED: "0" + WORKTRUNK_TEST_PARENT_SHELL: "" + WORKTRUNK_TEST_POWERSHELL_ENV: "0" + WORKTRUNK_TEST_POWERSHELL_INSTALLED: "0" + WORKTRUNK_TEST_PROBE_TIMEOUT_MS: "60000" + WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1" + WORKTRUNK_TEST_ZSH_INSTALLED: "0" + XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]" +--- +success: false +exit_code: 1 +----- stdout ----- + +----- stderr ----- +◎ Fetching PR #101... +  Fix in a sibling repository (#101) +  by @alice@example.com · active · feature-auth · https://dev.azure.com/myorg/myproject/_git/other-repo/pullrequest/101 +✗ No remote found for myorg/other-repo +↳ Add the remote: git remote add upstream https://dev.azure.com/myorg/myproject/_git/other-repo