fix(forge): preserve non-default port in forge web URLs (#2478)

"Open in browser" links for a self-hosted forge served on a non-standard
port (e.g. Forgejo/Gitea on :60443) dropped the port, producing
https://host/owner/repo/... instead of https://host:60443/owner/repo/...,
which 404s or hits the wrong service.

parseGitRemoteLocation discarded parsed.port (GitRemoteLocation had no
port field), and buildForgeBranchTreeUrl / buildForgeBlobUrl rebuilt the
origin from the portless host. Preserve the port on GitRemoteLocation and
reattach it in the web-URL builders, only for self-hosted http(s) origins
(an SSH port isn't the web port; a canonicalized cloud host uses the
default port). Host-identity matching (forge detection, cloud-host checks)
stays port-agnostic.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Li Mu Zhi
2026-07-28 04:00:46 +08:00
committed by GitHub
parent c596e058cd
commit 869edcbf11
4 changed files with 80 additions and 9 deletions

View File

@@ -27,3 +27,25 @@ describe("isCompleteGitRemote", () => {
}
});
});
describe("parseGitRemoteLocation port", () => {
it("preserves an explicit non-default port from an https remote", () => {
expect(parseGitRemoteLocation("https://home-git.example.com:60443/team/repo.git")?.port).toBe(
"60443",
);
});
it("preserves a port from a plain http remote", () => {
expect(parseGitRemoteLocation("http://internal.example.com:3000/team/repo.git")?.port).toBe(
"3000",
);
});
it("omits the port for a default-port remote", () => {
expect(parseGitRemoteLocation("https://github.com/acme/repo.git")?.port).toBeUndefined();
});
it("has no port for an scp-form remote", () => {
expect(parseGitRemoteLocation("git@host.example.com:team/repo.git")?.port).toBeUndefined();
});
});

View File

@@ -11,6 +11,13 @@ const TRANSPORT_BY_PROTOCOL: Record<string, GitRemoteLocation["transport"]> = {
export interface GitRemoteLocation {
transport: "scp" | "ssh" | "http" | "https";
host: string;
/**
* Explicit non-default port from the remote (e.g. a self-hosted forge on
* `:60443`), or undefined for a default-port or scp-form remote. Kept separate
* from `host` so host-identity matching (forge detection, cloud-host checks)
* stays port-agnostic; only consumers that reconstruct a URL (web links) use it.
*/
port?: string;
path: string;
}
@@ -71,7 +78,7 @@ export function parseGitRemoteLocation(remoteUrl: string): GitRemoteLocation | n
const normalizedPath = normalizeRemotePath(path);
if (!isValidRemoteHost(host) || !normalizedPath) return null;
return { transport, host, path: normalizedPath };
return { transport, host, port: parsed.port || undefined, path: normalizedPath };
}
export function parseGitHubRemoteIdentity(path: string): GitHubRemoteIdentity | null {