Skip to content

Commit b0b8b62

Browse files
Jami CogswellJami Cogswell
authored andcommitted
Download source root
1 parent 2bcf42e commit b0b8b62

File tree

1 file changed

+130
-1
lines changed

1 file changed

+130
-1
lines changed

extensions/ql-vscode/src/variant-analysis/view-autofixes.ts

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { Credentials } from "../common/authentication";
1111
import type { NotificationLogger } from "../common/logging";
1212
import type { App } from "../common/app";
1313
import type { CodeQLCliServer } from "../codeql-cli/cli";
14-
import { pathExists, ensureDir } from "fs-extra";
14+
import { pathExists, ensureDir, readdir, move, remove } from "fs-extra";
1515
import { withProgress, progressUpdate } from "../common/vscode/progress";
1616
import type { ProgressCallback } from "../common/vscode/progress";
1717
import { join, dirname, parse } from "path";
@@ -20,6 +20,9 @@ import { window as Window } from "vscode";
2020
import { pluralize } from "../common/word";
2121
import { glob } from "glob";
2222
import { readRepoTask } from "./repo-tasks-store";
23+
import { unlink, mkdtemp } from "fs/promises";
24+
import { tmpdir } from "os";
25+
import { spawn } from "child_process";
2326

2427
// Limit to three repos when generating autofixes so not sending
2528
// too many requests to autofix. Since we only need to validate
@@ -297,6 +300,18 @@ async function processSelectedRepositories(
297300
if (!repoTask.resultCount) {
298301
throw new Error(`Missing variant analysis result count for ${nwo}`);
299302
}
303+
304+
// Download the source root.
305+
// Using `0` as the progress step to force a dynamic vs static progress bar.
306+
// Consider using `reportStreamProgress` as a future enhancement.
307+
progressForRepo(progressUpdate(0, 3, `Downloading source root`));
308+
const srcRootPath = await downloadPublicCommitSource(
309+
nwo,
310+
repoTask.databaseCommitSha,
311+
sourceRootsStoragePath,
312+
credentials,
313+
logger,
314+
);
300315
},
301316
{
302317
title: `Processing ${nwo}`,
@@ -327,3 +342,117 @@ async function getSarifFile(
327342
}
328343
return sarifFiles[0];
329344
}
345+
346+
/**
347+
* Downloads the source code of a public commit from a GitHub repository.
348+
*/
349+
async function downloadPublicCommitSource(
350+
nwo: string,
351+
sha: string,
352+
outputPath: string,
353+
credentials: Credentials,
354+
logger: NotificationLogger,
355+
): Promise<string> {
356+
const [owner, repo] = nwo.split("/");
357+
if (!owner || !repo) {
358+
throw new Error(`Invalid repository name: ${nwo}`);
359+
}
360+
361+
// Create output directory if it doesn't exist
362+
await ensureDir(outputPath);
363+
364+
// Define the final checkout directory
365+
const checkoutDir = join(
366+
outputPath,
367+
`${owner}-${repo}-${sha.substring(0, 7)}`,
368+
);
369+
370+
// Check if directory already exists to avoid re-downloading
371+
if (await pathExists(checkoutDir)) {
372+
void logger.log(
373+
`Source for ${nwo} at ${sha} already exists at ${checkoutDir}.`,
374+
);
375+
return checkoutDir;
376+
}
377+
378+
void logger.log(`Fetching source of repository ${nwo} at ${sha}...`);
379+
380+
try {
381+
// Create a temporary directory for downloading
382+
const downloadDir = await mkdtemp(join(tmpdir(), "download-source-"));
383+
const tarballPath = join(downloadDir, "source.tar.gz");
384+
385+
const octokit = await credentials.getOctokit();
386+
387+
// Get the tarball URL
388+
const { url } = await octokit.rest.repos.downloadTarballArchive({
389+
owner,
390+
repo,
391+
ref: sha,
392+
});
393+
394+
// Download the tarball using spawn
395+
await new Promise<void>((resolve, reject) => {
396+
const curlArgs = [
397+
"-H",
398+
"Accept: application/octet-stream",
399+
"--user-agent",
400+
"GitHub-CodeQL-Extension",
401+
"-L", // Follow redirects
402+
"-o",
403+
tarballPath,
404+
url,
405+
];
406+
407+
const process = spawn("curl", curlArgs, { cwd: downloadDir });
408+
409+
process.on("error", reject);
410+
process.on("exit", (code) =>
411+
code === 0
412+
? resolve()
413+
: reject(new Error(`curl exited with code ${code}`)),
414+
);
415+
});
416+
417+
void logger.log(`Download complete, extracting source...`);
418+
419+
// Extract the tarball
420+
await new Promise<void>((resolve, reject) => {
421+
const process = spawn("tar", ["-xzf", tarballPath], { cwd: downloadDir });
422+
423+
process.on("error", reject);
424+
process.on("exit", (code) =>
425+
code === 0
426+
? resolve()
427+
: reject(new Error(`tar extraction failed with code ${code}`)),
428+
);
429+
});
430+
431+
// Remove the tarball to save space
432+
await unlink(tarballPath);
433+
434+
// Find the extracted directory (GitHub tarballs extract to a single directory)
435+
const extractedFiles = await readdir(downloadDir);
436+
const sourceDir = extractedFiles.filter((f) => f !== "source.tar.gz")[0];
437+
438+
if (!sourceDir) {
439+
throw new Error("Failed to find extracted source directory");
440+
}
441+
442+
const extractedSourcePath = join(downloadDir, sourceDir);
443+
444+
// Ensure the destination directory's parent exists
445+
await ensureDir(dirname(checkoutDir));
446+
447+
// Move the extracted source to the final location
448+
await move(extractedSourcePath, checkoutDir);
449+
450+
// Clean up the temporary directory
451+
await remove(downloadDir);
452+
453+
return checkoutDir;
454+
} catch (error) {
455+
const errorMessage = error instanceof Error ? error.message : String(error);
456+
throw new Error(`Failed to download ${nwo} at ${sha}: ${errorMessage}`);
457+
}
458+
}

0 commit comments

Comments
 (0)