History

fix(server): check out submodules in a new worktree (#7674)

Rodrigo Brechard committed 2 months ago

Commit be3da501 parent f9a726e2 files · +110 −0Browse files

Unified · Split

2 files
  1. apps/server/src/vcs/GitVcsDriverCore.test.ts +86 −0
  2. apps/server/src/vcs/GitVcsDriverCore.ts +24 −0
apps/server/src/vcs/GitVcsDriverCore.test.ts+86−0
@@ -1374,6 +1374,92 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { · 1373 unmodified lines
13741374 }),
13751375 );
13761376
1377+ it.effect("checks out submodules in a new worktree", () =>
1378+ Effect.gen(function* () {
1379+ const fileSystem = yield* FileSystem.FileSystem;
1380+ const pathService = yield* Path.Path;
1381+
1382+ // Git refuses `file:` submodule transports by default (CVE-2022-39253)
1383+ // and ignores repo-level config for it, so a local fixture needs the
1384+ // env allowance. Real submodules are https/ssh and need none of this.
1385+ const previousAllowedProtocol = process.env.GIT_ALLOW_PROTOCOL;
1386+ process.env.GIT_ALLOW_PROTOCOL = "file";
1387+ yield* Effect.addFinalizer(() =>
1388+ Effect.sync(() => {
1389+ if (previousAllowedProtocol === undefined) {
1390+ delete process.env.GIT_ALLOW_PROTOCOL;
1391+ } else {
1392+ process.env.GIT_ALLOW_PROTOCOL = previousAllowedProtocol;
1393+ }
1394+ }),
1395+ );
1396+
1397+ // A real submodule: `git worktree add` leaves these empty, which is
1398+ // what silently strips shared tooling out of every new worktree.
1399+ const submoduleRepo = yield* makeTmpDir("git-submodule-");
1400+ yield* initRepoWithCommit(submoduleRepo);
1401+ yield* writeTextFile(submoduleRepo, "SHARED.md", "# shared\n");
1402+ yield* git(submoduleRepo, ["add", "."]);
1403+ yield* git(submoduleRepo, ["commit", "-m", "shared"]);
1404+
1405+ const cwd = yield* makeTmpDir();
1406+ const { initialBranch } = yield* initRepoWithCommit(cwd);
1407+ yield* git(cwd, ["submodule", "add", submoduleRepo, "shared"]);
1408+ yield* git(cwd, ["commit", "-m", "add submodule"]);
1409+
1410+ const worktreePath = pathService.join(
1411+ yield* makeTmpDir("git-worktrees-"),
1412+ "submodule-worktree",
1413+ );
1414+ const driver = yield* GitVcsDriver.GitVcsDriver;
1415+ yield* driver.createWorktree({
1416+ cwd,
1417+ path: worktreePath,
1418+ refName: initialBranch,
1419+ newRefName: "feature/submodules",
1420+ });
1421+
1422+ assert.equal(
1423+ yield* fileSystem.exists(pathService.join(worktreePath, "shared", "SHARED.md")),
1424+ true,
1425+ );
1426+ }),
1427+ );
1428+
1429+ it.effect("still creates the worktree when submodule checkout fails", () =>
1430+ Effect.gen(function* () {
1431+ const fileSystem = yield* FileSystem.FileSystem;
1432+ const pathService = yield* Path.Path;
1433+
1434+ const cwd = yield* makeTmpDir();
1435+ const { initialBranch } = yield* initRepoWithCommit(cwd);
1436+ // Points at a repository that does not exist, so the checkout fails the
1437+ // way an unreachable private remote would. Creation must still succeed.
1438+ yield* writeTextFile(
1439+ cwd,
1440+ ".gitmodules",
1441+ '[submodule "missing"]\n\tpath = missing\n\turl = /nonexistent/repo.git\n',
1442+ );
1443+ yield* git(cwd, ["add", "."]);
1444+ yield* git(cwd, ["commit", "-m", "add unreachable submodule"]);
1445+
1446+ const worktreePath = pathService.join(
1447+ yield* makeTmpDir("git-worktrees-"),
1448+ "broken-submodule-worktree",
1449+ );
1450+ const driver = yield* GitVcsDriver.GitVcsDriver;
1451+ const created = yield* driver.createWorktree({
1452+ cwd,
1453+ path: worktreePath,
1454+ refName: initialBranch,
1455+ newRefName: "feature/broken-submodules",
1456+ });
1457+
1458+ assert.equal(created.worktree.path, worktreePath);
1459+ assert.equal(yield* fileSystem.exists(worktreePath), true);
1460+ }),
1461+ );
1462+
13771463 it.effect("creates and removes a worktree for a new refName", () =>
13781464 Effect.gen(function* () {
13791465 const cwd = yield* makeTmpDir();
apps/server/src/vcs/GitVcsDriverCore.ts+24−0
@@ -2776,6 +2776,30 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* · 2775 unmodified lines
27762776 timeoutMs: WORKTREE_ADD_TIMEOUT_MS,
27772777 });
27782778
2779+ // `git worktree add` leaves submodules empty, so a repo that keeps agent
2780+ // skills, tooling or source in one gets a worktree that is quietly missing
2781+ // them. Best-effort: the objects are usually already in the parent's
2782+ // `.git/modules`, but a first-ever clone needs the network, and failing to
2783+ // populate a submodule must not roll back the caller's thread.
2784+ const hasSubmodules = yield* fileSystem
2785+ .exists(path.join(worktreePath, ".gitmodules"))
2786+ .pipe(Effect.orElseSucceed(() => false));
2787+ if (hasSubmodules) {
2788+ yield* runGit("GitVcsDriver.createWorktree.updateSubmodules", worktreePath, [
2789+ "submodule",
2790+ "update",
2791+ "--init",
2792+ "--recursive",
2793+ ]).pipe(
2794+ Effect.catch((cause) =>
2795+ Effect.logWarning("worktree submodule checkout failed; submodule paths are empty", {
2796+ worktreePath,
2797+ cause,
2798+ }),
2799+ ),
2800+ );
2801+ }
2802+
27792803 if (input.newRefName && input.baseRefName) {
27802804 const remoteNames = yield* listRemoteNames(input.cwd).pipe(Effect.orElseSucceed(() => []));
27812805 const parsedBaseRef = parseRemoteRefWithRemoteNames(