-
Notifications
You must be signed in to change notification settings - Fork 5.3k
feat(pull-requests): resolve ssh config host aliases on git remotes #6196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
16a7807
fix(pull-requests): resolve ssh config host aliases on git remotes
nyedle c19bc7f
Merge branch 'pingdotgg:main' into main
nyedle 6793f6c
Merge branch 'pingdotgg:main' into main
nyedle 73a0836
Merge branch 'pingdotgg:main' into main
nyedle 5078f76
fix(pull-requests): resolve ssh aliases on GitManager's remote reads too
nyedle 32708d8
fix: pass -- before the ssh host
nyedle 585cf49
Merge branch 'main' into main
nyedle b132a17
fix: stop caching failed ssh probes
nyedle c988d11
fix: bracket ipv6 hostnames in resolved ssh
nyedle 0fed439
Merge branch 'main' into main
nyedle File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| import { describe, expect, it } from "@effect/vitest"; | ||
| import * as Effect from "effect/Effect"; | ||
|
|
||
| import { canonicalizeSshRemoteUrl, SshHostnameCache } from "./sshRemoteUrl.ts"; | ||
|
|
||
| const sshConfig = (hostname: string) => (host: string) => | ||
| Effect.succeed(`host ${host}\r\nhostname ${hostname}\r\nport 22\r\nuser git\r\n`); | ||
|
|
||
| const unresolved = (host: string) => Effect.succeed(`hostname ${host}\n`); | ||
|
|
||
| describe("canonicalizeSshRemoteUrl", () => { | ||
| it.effect("resolves an scp-style alias to its configured hostname", () => | ||
| Effect.gen(function* () { | ||
| expect( | ||
| yield* canonicalizeSshRemoteUrl("git@alt:pingdotgg/t3chat.git", sshConfig("github.com")), | ||
| ).toBe("git@github.com:pingdotgg/t3chat.git"); | ||
| }), | ||
| ); | ||
|
|
||
| it.effect("resolves an alias in an ssh:// remote, port and path intact", () => | ||
| Effect.gen(function* () { | ||
| expect( | ||
| yield* canonicalizeSshRemoteUrl( | ||
| "ssh://git@work-main:2222/pingdotgg/t3chat.git", | ||
| sshConfig("gitlab.example.com"), | ||
| ), | ||
| ).toBe("ssh://git@gitlab.example.com:2222/pingdotgg/t3chat.git"); | ||
| }), | ||
| ); | ||
|
|
||
| it.effect("probes each host once", () => | ||
| Effect.gen(function* () { | ||
| let probes = 0; | ||
| const probe = (host: string) => { | ||
| probes += 1; | ||
| return sshConfig("github.com")(host); | ||
| }; | ||
| yield* canonicalizeSshRemoteUrl("git@cached-alias:pingdotgg/t3chat.git", probe); | ||
| yield* canonicalizeSshRemoteUrl("git@cached-alias:pingdotgg/t3code.git", probe); | ||
| expect(probes).toBe(1); | ||
| }).pipe(Effect.provideService(SshHostnameCache, new Map())), | ||
| ); | ||
|
|
||
| it.effect("retries after a failed probe instead of caching it", () => | ||
| Effect.gen(function* () { | ||
| const results = ["", "hostname github.com\n"]; | ||
| const probe = () => Effect.succeed(results.shift() ?? ""); | ||
| yield* canonicalizeSshRemoteUrl("git@flaky:pingdotgg/t3chat.git", probe); | ||
| expect(yield* canonicalizeSshRemoteUrl("git@flaky:pingdotgg/t3chat.git", probe)).toBe( | ||
| "git@github.com:pingdotgg/t3chat.git", | ||
| ); | ||
| }).pipe(Effect.provideService(SshHostnameCache, new Map())), | ||
| ); | ||
|
|
||
| it.effect("brackets an IPv6 hostname before substitution", () => | ||
| Effect.gen(function* () { | ||
| expect( | ||
| yield* canonicalizeSshRemoteUrl("git@v6:pingdotgg/t3chat.git", sshConfig("2001:db8::1")), | ||
| ).toBe("git@[2001:db8::1]:pingdotgg/t3chat.git"); | ||
| expect( | ||
| yield* canonicalizeSshRemoteUrl( | ||
| "ssh://git@v6:2222/pingdotgg/t3chat.git", | ||
| sshConfig("2001:db8::1"), | ||
| ), | ||
| ).toBe("ssh://git@[2001:db8::1]:2222/pingdotgg/t3chat.git"); | ||
| }).pipe(Effect.provideService(SshHostnameCache, new Map())), | ||
| ); | ||
|
|
||
| it.effect("leaves non-ssh remotes and local paths alone", () => | ||
| Effect.gen(function* () { | ||
| const failing = () => Effect.die("ssh must not be probed"); | ||
| for (const remoteUrl of [ | ||
| "https://github.com/pingdotgg/t3chat.git", | ||
| "git://github.com/pingdotgg/t3chat.git", | ||
| "/home/me/repo", | ||
| "C:/Users/me/repo", | ||
| "C:\\Users\\me\\repo", | ||
| "../sibling/repo", | ||
| ]) { | ||
| expect(yield* canonicalizeSshRemoteUrl(remoteUrl, failing)).toBe(remoteUrl); | ||
| } | ||
| }), | ||
| ); | ||
|
|
||
| it.effect("leaves a host that ssh does not rewrite alone", () => | ||
| Effect.gen(function* () { | ||
| expect( | ||
| yield* canonicalizeSshRemoteUrl("git@github.com:pingdotgg/t3chat.git", unresolved), | ||
| ).toBe("git@github.com:pingdotgg/t3chat.git"); | ||
| }), | ||
| ); | ||
|
|
||
| it.effect("keeps the remote when ssh cannot be run", () => | ||
| Effect.gen(function* () { | ||
| expect( | ||
| yield* canonicalizeSshRemoteUrl("git@no-ssh-here:pingdotgg/t3chat.git", () => | ||
| Effect.succeed(""), | ||
| ), | ||
| ).toBe("git@no-ssh-here:pingdotgg/t3chat.git"); | ||
| }), | ||
| ); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import * as Context from "effect/Context"; | ||
| import * as Effect from "effect/Effect"; | ||
|
|
||
| import type * as ProcessRunner from "../processRunner.ts"; | ||
|
|
||
| const SSH_URL_HOST = /^(ssh:\/\/(?:[^@/]*@)?)([^@:/]+)/iu; | ||
| const SCP_URL_HOST = /^((?:[^@:/]*@)?)([^@:/]{2,})(?=:(?!\/))/u; | ||
|
|
||
| const hostPattern = (remoteUrl: string): RegExp => | ||
| /^ssh:\/\//iu.test(remoteUrl) ? SSH_URL_HOST : SCP_URL_HOST; | ||
|
|
||
| export type SshConfigProbe = (host: string) => Effect.Effect<string>; | ||
|
|
||
| export const sshConfigProbe = | ||
| (processRunner: ProcessRunner.ProcessRunner["Service"]): SshConfigProbe => | ||
| (host) => | ||
| processRunner | ||
| .run({ command: "ssh", args: ["-G", "--", host], timeoutBehavior: "timedOutResult" }) | ||
| .pipe( | ||
| Effect.map((result) => result.stdout), | ||
| Effect.orElseSucceed(() => ""), | ||
| ); | ||
|
|
||
| const HOSTNAME_TTL_MS = 5 * 60_000; | ||
|
|
||
| export const SshHostnameCache = Context.Reference< | ||
| Map<string, { readonly at: number; readonly hostname: string }> | ||
| >("@t3tools/server/vcs/SshHostnameCache", { | ||
| defaultValue: () => new Map(), | ||
| }); | ||
|
|
||
| export const canonicalizeSshRemoteUrl = Effect.fnUntraced(function* ( | ||
| remoteUrl: string, | ||
| probe: SshConfigProbe, | ||
| ) { | ||
| const host = hostPattern(remoteUrl).exec(remoteUrl)?.[2]; | ||
| if (host === undefined) return remoteUrl; | ||
|
|
||
| const cache = yield* SshHostnameCache; | ||
| const now = Date.now(); | ||
| const cached = cache.get(host); | ||
| let hostname = | ||
| cached !== undefined && now - cached.at < HOSTNAME_TTL_MS ? cached.hostname : undefined; | ||
| if (hostname === undefined) { | ||
| // Only cache probes that yielded a hostname; a failed or empty ssh -G run | ||
| // must not pin the alias unresolved for the whole TTL. | ||
| const probed = /^hostname[ \t]+(\S+)/imu.exec(yield* probe(host))?.[1]; | ||
| if (probed !== undefined) cache.set(host, { at: now, hostname: probed }); | ||
| hostname = probed ?? host; | ||
| } | ||
|
|
||
| if (hostname === host) return remoteUrl; | ||
| const substituted = | ||
| hostname.includes(":") && !hostname.startsWith("[") ? `[${hostname}]` : hostname; | ||
| return remoteUrl.replace(hostPattern(remoteUrl), (_, prefix: string) => prefix + substituted); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This fixes interrupted
AsyncResultreads only in the web copy ofuseEnvironmentQuery, whileapps/mobile/src/state/query.tsstill formats everyFailureas an error. Any mobile screen that refreshes overlapping environment atoms can therefore continue showing the same spurious “environment request failed” state that this patch removes from web/desktop; move this logic to shared client code or update the mobile hook as well.AGENTS.md reference: AGENTS.md:L65-L75
Useful? React with 👍 / 👎.