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
37 changes: 28 additions & 9 deletions packages/junior-github/src/tools/clone-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ const inputSchema = z
.describe(
"Set true to keep an intentional ad-hoc checkout when matching Workspaces exist. Prefer switchWorkspace; the checkout is already present after a successful switch.",
),
blobless: z
.boolean()
.optional()
.describe(
"Set true to perform a blobless shallow clone (--filter=blob:none) to reduce bandwidth and clone times on large repositories.",
),
timeoutSeconds: z
.number()
.int()
.positive()
.max(300)
.optional()
.describe(
"Optional timeout in seconds for the clone operation. Defaults to 300 seconds (5 minutes), which is the sandbox executor cap.",
),
Comment thread
cursor[bot] marked this conversation as resolved.
})
.strict();
const cloneSchema = z.object({
Expand Down Expand Up @@ -206,20 +221,24 @@ export function createGitHubCloneRepositoryTool(ctx: {
// ls-remote, so egress policy cannot distinguish clone without also
// blocking normal repository workflows. This tool is the bounded,
// preferred clone path rather than an enforceable network boundary.
const timeoutMs = (input.timeoutSeconds ?? 300) * 1000;
const cloneArgs = ["clone", "--quiet", "--depth=1"];
if (input.blobless === true) {
cloneArgs.push("--filter=blob:none");
}
cloneArgs.push(
"--",
`https://github.com/${repo.owner}/${repo.name}.git`,
directory,
);

let clone;
try {
clone = await ctx.sandbox.run({
cmd: "git",
args: [
"clone",
"--quiet",
"--depth=1",
"--",
`https://github.com/${repo.owner}/${repo.name}.git`,
directory,
],
args: cloneArgs,
cwd: ctx.sandbox.root,
signal: commandSignal(options.signal, 2 * 60_000),
signal: commandSignal(options.signal, timeoutMs),
});
} catch (error) {
await removePartialClone(ctx, path);
Expand Down
65 changes: 65 additions & 0 deletions packages/junior-github/tests/clone-repository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,4 +265,69 @@ describe("cloneRepository", () => {
);
expect(run).toHaveBeenCalledTimes(3);
});

it("supports blobless shallow clone with --filter=blob:none", async () => {
const run = vi
.fn()
.mockResolvedValueOnce({ exitCode: 0, stdout: "", stderr: "" })
.mockResolvedValueOnce({ exitCode: 1, stdout: "", stderr: "" })
.mockResolvedValueOnce({ exitCode: 0, stdout: "", stderr: "" });
const tool = createGitHubCloneRepositoryTool(context(run));

const result = await tool.execute!(
{ repo: "getsentry/junior", blobless: true },
{} as never,
);

expect(run).toHaveBeenNthCalledWith(3, {
cmd: "git",
args: [
"clone",
"--quiet",
"--depth=1",
"--filter=blob:none",
"--",
"https://github.com/getsentry/junior.git",
"repos/junior",
],
cwd: "/vercel/sandbox",
signal: expect.any(AbortSignal),
});
expect(result).toMatchObject({
path: "/vercel/sandbox/repos/junior",
repo: "getsentry/junior",
});
});

it("accepts custom timeoutSeconds for clone operations", async () => {
const run = vi
.fn()
.mockResolvedValueOnce({ exitCode: 0, stdout: "", stderr: "" })
.mockResolvedValueOnce({ exitCode: 1, stdout: "", stderr: "" })
.mockResolvedValueOnce({ exitCode: 0, stdout: "", stderr: "" });
const tool = createGitHubCloneRepositoryTool(context(run));

const result = await tool.execute!(
{ repo: "getsentry/junior", timeoutSeconds: 250 },
{} as never,
);

expect(run).toHaveBeenNthCalledWith(3, {
cmd: "git",
args: [
"clone",
"--quiet",
"--depth=1",
"--",
"https://github.com/getsentry/junior.git",
"repos/junior",
],
cwd: "/vercel/sandbox",
signal: expect.any(AbortSignal),
});
expect(result).toMatchObject({
path: "/vercel/sandbox/repos/junior",
repo: "getsentry/junior",
});
});
});
Loading