#157: convert forge worktree origins to SSH
This commit is contained in:
@@ -7,6 +7,8 @@ import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -64,6 +66,9 @@ public final class GitWorktrees implements Worktrees {
|
||||
/** What {@link #isolateToolSurface} writes for {@code .autoenv}: a valid, empty env file. */
|
||||
private static final String NEUTRAL_AUTOENV_CONFIG = "";
|
||||
|
||||
private static final String FORGE_HOST = "git.ltms.dev";
|
||||
private static final int FORGE_SSH_PORT = 2224;
|
||||
|
||||
/**
|
||||
* A tracked project config that is hostile in a provisioned worktree, and what to replace it
|
||||
* with. {@link #file} is the repo-relative path; {@link #stub} is a neutral but VALID payload for
|
||||
@@ -107,11 +112,40 @@ public final class GitWorktrees implements Worktrees {
|
||||
}
|
||||
String wt = path.toAbsolutePath().toString();
|
||||
log.info("adding worktree branch={} path={} base={}", branch, wt, base);
|
||||
convertForgeHttpsOriginToSsh(repoRoot);
|
||||
exec("git", "-C", repoRoot, "worktree", "add", wt, "-b", branch, base);
|
||||
isolateToolSurface(wt);
|
||||
return wt;
|
||||
}
|
||||
|
||||
/**
|
||||
* A linked worktree shares its primary checkout's git config. Convert this forge's HTTPS origin
|
||||
* before adding the worktree, so a credential accidentally embedded in that config cannot reach
|
||||
* the member. SSH uses the host user's key at push time and stores no forge token in git config.
|
||||
*/
|
||||
private void convertForgeHttpsOriginToSsh(String repoRoot) {
|
||||
if (exitCode("git", "-C", repoRoot, "config", "--get", "remote.origin.url") != 0) {
|
||||
return;
|
||||
}
|
||||
String origin = exec("git", "-C", repoRoot, "config", "--get", "remote.origin.url").trim();
|
||||
URI uri;
|
||||
try {
|
||||
uri = new URI(origin);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new WorktreeException("origin URL is invalid; cannot provision a safe worktree", e);
|
||||
}
|
||||
if (!"https".equalsIgnoreCase(uri.getScheme()) || !FORGE_HOST.equalsIgnoreCase(uri.getHost())) {
|
||||
return;
|
||||
}
|
||||
if (uri.getRawPath() == null || uri.getRawPath().isBlank() || uri.getRawQuery() != null
|
||||
|| uri.getRawFragment() != null) {
|
||||
throw new WorktreeException("origin URL cannot be converted to the forge SSH form safely");
|
||||
}
|
||||
String sshOrigin = "ssh://git@" + FORGE_HOST + ":" + FORGE_SSH_PORT + uri.getRawPath();
|
||||
exec("git", "-C", repoRoot, "remote", "set-url", "origin", sshOrigin);
|
||||
log.info("converted forge origin to SSH before provisioning worktree");
|
||||
}
|
||||
|
||||
/**
|
||||
* Neutralize the worktree's worktree-hostile project configs so a worker inherits only the tools
|
||||
* and environment its launcher mounts (the bridge via {@code --mcp-config}, the opencode config
|
||||
|
||||
@@ -205,6 +205,27 @@ class GitWorktreesTest {
|
||||
"expected an explicitly empty server map, got:\n" + body);
|
||||
}
|
||||
|
||||
/**
|
||||
* The worktree command shares the primary checkout's config, so this checks the URL git actually
|
||||
* reads after {@link GitWorktrees#add}, rather than checking only a URL formatting helper.
|
||||
*/
|
||||
@Test
|
||||
void aProvisionedWorktreeUsesTheForgeSshOrigin(@TempDir Path tmp) throws Exception {
|
||||
Path repo = initRepo(tmp.resolve("repo"));
|
||||
git(repo, "remote", "add", "origin", "https://synthetic-test-token@git.ltms.dev/akb/kb.git");
|
||||
|
||||
String wt = new GitWorktrees(tmp.resolve("wts").toString())
|
||||
.add(repo.toString(), "fleetd-157-safe-origin", "HEAD");
|
||||
|
||||
Process p = new ProcessBuilder("git", "-C", wt, "config", "--get", "remote.origin.url")
|
||||
.redirectErrorStream(true).start();
|
||||
String origin = new String(p.getInputStream().readAllBytes()).trim();
|
||||
assertTrue(p.waitFor(30, TimeUnit.SECONDS), "git config timed out");
|
||||
assertEquals(0, p.exitValue(), "git config failed");
|
||||
assertEquals("ssh://git@git.ltms.dev:2224/akb/kb.git", origin);
|
||||
assertFalse(origin.contains("synthetic-test-token"), "provisioned worktree kept user info");
|
||||
}
|
||||
|
||||
/** Neutralizing must not look like work in progress, or a worker would commit it into its PR. */
|
||||
@Test
|
||||
void theNeutralizedConfigIsNotAPendingLocalModification(@TempDir Path tmp) throws Exception {
|
||||
|
||||
Reference in New Issue
Block a user