@@ -11,7 +11,7 @@ import type { Credentials } from "../common/authentication";
1111import type { NotificationLogger } from "../common/logging" ;
1212import type { App } from "../common/app" ;
1313import type { CodeQLCliServer } from "../codeql-cli/cli" ;
14- import { pathExists , ensureDir } from "fs-extra" ;
14+ import { pathExists , ensureDir , readdir , move , remove } from "fs-extra" ;
1515import { withProgress , progressUpdate } from "../common/vscode/progress" ;
1616import type { ProgressCallback } from "../common/vscode/progress" ;
1717import { join , dirname , parse } from "path" ;
@@ -20,6 +20,9 @@ import { window as Window } from "vscode";
2020import { pluralize } from "../common/word" ;
2121import { glob } from "glob" ;
2222import { 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