Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 82 additions & 4 deletions apps/server/src/vcs/GitVcsDriverCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1579,8 +1579,8 @@ it.effect("cancels a slow submodule checkout without reporting best-effort succe
const spawner = ChildProcessSpawner.make((command) => {
if (
ChildProcess.isStandardCommand(command) &&
command.args[0] === "submodule" &&
command.args[1] === "update"
command.args.includes("submodule") &&
command.args.includes("update")
) {
return Effect.acquireRelease(
Effect.succeed(
Expand Down Expand Up @@ -4560,6 +4560,84 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {
}),
);

it.effect.each([false, true])(
"checks out submodules in a new worktree with borrowing failure %s",
(failBorrowing) =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const pathService = yield* Path.Path;

// Git blocks file submodule transports (CVE-2022-39253) despite repo config; local fixtures need env permission.
const previousAllowedProtocol = process.env.GIT_ALLOW_PROTOCOL;
process.env.GIT_ALLOW_PROTOCOL = "file";
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
if (previousAllowedProtocol === undefined) {
delete process.env.GIT_ALLOW_PROTOCOL;
} else {
process.env.GIT_ALLOW_PROTOCOL = previousAllowedProtocol;
}
}),
);

const submoduleRepo = yield* makeTmpDir("git-submodule-");
yield* initRepoWithCommit(submoduleRepo);
yield* writeTextFile(submoduleRepo, "SHARED.md", "# shared\n");
yield* git(submoduleRepo, ["add", "."]);
yield* git(submoduleRepo, ["commit", "-m", "shared"]);

const cwd = yield* makeTmpDir();
const { initialBranch } = yield* initRepoWithCommit(cwd);
yield* git(cwd, ["submodule", "add", submoduleRepo, "shared"]);
yield* git(cwd, ["commit", "-m", "add submodule"]);
// The remote still advertises its refs but can no longer serve objects, so the
// worktree can only populate the submodule from the source's `.git/modules`.
const remoteObjects = pathService.join(submoduleRepo, ".git", "objects");
if (!failBorrowing) {
yield* fileSystem.remove(remoteObjects, { recursive: true });
yield* fileSystem.makeDirectory(pathService.join(remoteObjects, "pack"), {
recursive: true,
});
}

const worktreePath = pathService.join(
yield* makeTmpDir("git-worktrees-"),
"submodule-worktree",
);
const delegate = yield* ChildProcessSpawner.ChildProcessSpawner;
let borrowingFailed = false;
const spawner = ChildProcessSpawner.make((command) => {
if (
failBorrowing &&
ChildProcess.isStandardCommand(command) &&
command.args.includes("--dissociate")
) {
borrowingFailed = true;
return Effect.succeed(makeFailedHandle("error: unknown option 'dissociate'"));
}
return delegate.spawn(command);
});
const driver = yield* makeGitVcsDriverCore().pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
);
yield* driver.createWorktree({
cwd,
path: worktreePath,
refName: initialBranch,
newRefName: "feature/submodules",
});

assert.equal(
yield* fileSystem.exists(pathService.join(worktreePath, "shared", "SHARED.md")),
true,
);
assert.equal(borrowingFailed, failBorrowing);
// The borrowed objects were copied, so the worktree survives the source losing them.
yield* fileSystem.remove(pathService.join(cwd, ".git", "modules"), { recursive: true });
yield* git(pathService.join(worktreePath, "shared"), ["fsck", "--no-progress"]);
}),
);

it.effect.each([
["recursive", false],
["recursive", true],
Expand Down Expand Up @@ -4605,8 +4683,8 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {
Effect.gen(function* () {
if (
ChildProcess.isStandardCommand(command) &&
command.args[0] === "submodule" &&
command.args[1] === "update"
command.args.includes("submodule") &&
command.args.includes("update")
) {
yield* Deferred.succeed(updateStarted, undefined);
yield* Deferred.await(releaseUpdate);
Expand Down
61 changes: 42 additions & 19 deletions apps/server/src/vcs/GitVcsDriverCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4285,32 +4285,55 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
yield* progress.onSubmodulesStarted();
}
const onSubmoduleLine = progress?.onSubmoduleLine;
const updateArgs = ["submodule", "update", "--init"];
if (submoduleMode.value === "recursive") updateArgs.push("--recursive");
const updateSubmodules = (args: ReadonlyArray<string>, env?: NodeJS.ProcessEnv) =>
runGit("GitVcsDriver.createWorktree.updateSubmodules", worktreePath, args, {
// Cloning scales with remote size; let it finish or be cancelled, like worktree add.
timeoutMs: null,
env: onSubmoduleLine ? { ...env, LANGUAGE: "C" } : env,
...(onSubmoduleLine
? { progress: { onStdoutLine: onSubmoduleLine, onStderrLine: onSubmoduleLine } }
: {}),
});
const borrowSubmodules = Effect.gen(function* () {
// Linked worktrees share the source object store; expose it as an alternate so Git
// can borrow from its modules. An empty primary store makes Git inspect the alternate;
// --dissociate copies borrowed objects before that temporary store is removed.
const sharedObjects = yield* runGitStdout(
"GitVcsDriver.createWorktree.objectsPath",
worktreePath,
["rev-parse", "--git-path", "objects"],
);
const emptyObjects = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3code-submodule-objects-",
});
yield* updateSubmodules(
[
"-c",
"submodule.alternateLocation=superproject",
"-c",
"submodule.alternateErrorStrategy=ignore",
...updateArgs,
"--dissociate",
],
{
GIT_OBJECT_DIRECTORY: emptyObjects,
GIT_ALTERNATE_OBJECT_DIRECTORIES: path.resolve(worktreePath, sharedObjects.trim()),
},
);
}).pipe(
Effect.scoped,
Effect.catch(() => updateSubmodules(updateArgs)),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
);
// Registering submodule URLs writes the shared config; the clones that follow do not.
yield* runGit(
"GitVcsDriver.createWorktree.initSubmodules",
worktreePath,
["submodule", "init"],
{ writesSharedConfig: true },
).pipe(
Effect.andThen(
runGit(
"GitVcsDriver.createWorktree.updateSubmodules",
worktreePath,
submoduleMode.value === "recursive"
? ["submodule", "update", "--init", "--recursive"]
: ["submodule", "update", "--init"],
{
// Cloning scales with remote size; let it finish or be cancelled, like worktree add.
timeoutMs: null,
...(onSubmoduleLine
? {
env: { LANGUAGE: "C" },
progress: { onStdoutLine: onSubmoduleLine, onStderrLine: onSubmoduleLine },
}
: {}),
},
),
),
Effect.andThen(borrowSubmodules),
Effect.matchEffect({
onFailure: (cause) =>
Effect.logWarning("worktree submodule checkout failed; submodules may be incomplete", {
Expand Down
Loading