@@ -11,7 +11,7 @@ import type { Credentials } from "../common/authentication";
11
11
import type { NotificationLogger } from "../common/logging" ;
12
12
import type { App } from "../common/app" ;
13
13
import type { CodeQLCliServer } from "../codeql-cli/cli" ;
14
- import { pathExists , ensureDir } from "fs-extra" ;
14
+ import { pathExists , ensureDir , readdir , move , remove } from "fs-extra" ;
15
15
import { withProgress , progressUpdate } from "../common/vscode/progress" ;
16
16
import type { ProgressCallback } from "../common/vscode/progress" ;
17
17
import { join , dirname , parse } from "path" ;
@@ -20,6 +20,9 @@ import { window as Window } from "vscode";
20
20
import { pluralize } from "../common/word" ;
21
21
import { glob } from "glob" ;
22
22
import { readRepoTask } from "./repo-tasks-store" ;
23
+ import { unlink , mkdtemp } from "fs/promises" ;
24
+ import { tmpdir } from "os" ;
25
+ import { spawn } from "child_process" ;
23
26
24
27
// Limit to three repos when generating autofixes so not sending
25
28
// too many requests to autofix. Since we only need to validate
@@ -297,6 +300,18 @@ async function processSelectedRepositories(
297
300
if ( ! repoTask . resultCount ) {
298
301
throw new Error ( `Missing variant analysis result count for ${ nwo } ` ) ;
299
302
}
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
+ ) ;
300
315
} ,
301
316
{
302
317
title : `Processing ${ nwo } ` ,
@@ -327,3 +342,117 @@ async function getSarifFile(
327
342
}
328
343
return sarifFiles [ 0 ] ;
329
344
}
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