@@ -12,8 +12,29 @@ import {
12
12
PullRequestChanges ,
13
13
PullRequestCommentRequest ,
14
14
PullRequestCommentResponse ,
15
+ PullRequestFileContent ,
15
16
} from './types.js' ;
16
17
18
+ /**
19
+ * Helper function to safely stringify objects with circular references
20
+ */
21
+ function safeStringify ( obj : any ) : string {
22
+ const seen = new WeakSet ( ) ;
23
+ return JSON . stringify ( obj , ( key , value ) => {
24
+ // Skip _httpMessage, socket and similar properties that cause circular references
25
+ if ( key === '_httpMessage' || key === 'socket' || key === 'connection' || key === 'agent' ) {
26
+ return '[Circular]' ;
27
+ }
28
+ if ( typeof value === 'object' && value !== null ) {
29
+ if ( seen . has ( value ) ) {
30
+ return '[Circular]' ;
31
+ }
32
+ seen . add ( value ) ;
33
+ }
34
+ return value ;
35
+ } ) ;
36
+ }
37
+
17
38
/**
18
39
* Service for interacting with Azure DevOps API
19
40
*/
@@ -462,7 +483,7 @@ class AzureDevOpsService {
462
483
startPosition : number ,
463
484
length : number ,
464
485
project ?: string
465
- ) : Promise < { content : string ; size : number ; position : number ; length : number } > {
486
+ ) : Promise < PullRequestFileContent > {
466
487
await this . initialize ( ) ;
467
488
468
489
if ( ! this . gitClient ) {
@@ -477,20 +498,110 @@ class AzureDevOpsService {
477
498
}
478
499
479
500
try {
501
+ // Check if the file is binary first
502
+ if ( this . isBinaryFile ( filePath ) ) {
503
+ try {
504
+ // Get metadata about the file to know its full size
505
+ const item = await this . gitClient . getItem ( repositoryId , filePath , projectName , objectId ) ;
506
+
507
+ const fileSize = item ?. contentMetadata ?. contentLength || 0 ;
508
+
509
+ return {
510
+ content : `[Binary file not displayed - ${ Math . round ( fileSize / 1024 ) } KB]` ,
511
+ size : fileSize ,
512
+ position : startPosition ,
513
+ length : 0 ,
514
+ } ;
515
+ } catch ( error ) {
516
+ console . error ( `Error getting binary file info: ${ error } ` ) ;
517
+ return {
518
+ content : '[Binary file - Unable to retrieve size information]' ,
519
+ size : 0 ,
520
+ position : startPosition ,
521
+ length : 0 ,
522
+ error : `Failed to get binary file info: ${ ( error as Error ) . message } ` ,
523
+ } ;
524
+ }
525
+ }
526
+
480
527
// Get metadata about the file to know its full size
481
528
const item = await this . gitClient . getItem ( repositoryId , filePath , projectName , objectId ) ;
482
529
483
530
const fileSize = item ?. contentMetadata ?. contentLength || 0 ;
484
531
485
- // Get the specified chunk of content
486
- const content = await this . gitClient . getItemText (
487
- repositoryId ,
488
- filePath ,
489
- projectName ,
490
- objectId ,
491
- startPosition ,
492
- length
493
- ) ;
532
+ // Get the content - handle potential errors and circular references
533
+ let rawContent ;
534
+ try {
535
+ rawContent = await this . gitClient . getItemText (
536
+ repositoryId ,
537
+ filePath ,
538
+ projectName ,
539
+ objectId ,
540
+ startPosition ,
541
+ length
542
+ ) ;
543
+ } catch ( textError ) {
544
+ console . error ( `Error fetching file text: ${ textError } ` ) ;
545
+ // If direct text access fails, try using the branch approach
546
+ try {
547
+ // Get the PR details to find the source branch
548
+ const pullRequest = await this . getPullRequestById (
549
+ repositoryId ,
550
+ pullRequestId ,
551
+ projectName
552
+ ) ;
553
+
554
+ // Try the source branch first
555
+ if ( pullRequest . sourceRefName ) {
556
+ const branchResult = await this . getFileFromBranch (
557
+ repositoryId ,
558
+ filePath ,
559
+ pullRequest . sourceRefName ,
560
+ startPosition ,
561
+ length ,
562
+ projectName
563
+ ) ;
564
+
565
+ if ( ! branchResult . error ) {
566
+ return branchResult ;
567
+ }
568
+ }
569
+
570
+ // If source branch fails, try target branch
571
+ if ( pullRequest . targetRefName ) {
572
+ const branchResult = await this . getFileFromBranch (
573
+ repositoryId ,
574
+ filePath ,
575
+ pullRequest . targetRefName ,
576
+ startPosition ,
577
+ length ,
578
+ projectName
579
+ ) ;
580
+
581
+ if ( ! branchResult . error ) {
582
+ return branchResult ;
583
+ }
584
+ }
585
+
586
+ throw new Error ( 'Failed to retrieve content using branch approach' ) ;
587
+ } catch ( branchError ) {
588
+ throw new Error ( `Failed to retrieve content: ${ ( branchError as Error ) . message } ` ) ;
589
+ }
590
+ }
591
+
592
+ // Ensure content is a proper string
593
+ let content = '' ;
594
+ if ( typeof rawContent === 'string' ) {
595
+ content = rawContent ;
596
+ } else if ( rawContent && typeof rawContent === 'object' ) {
597
+ // If it's an object but not a string, try to convert it safely
598
+ try {
599
+ content = safeStringify ( rawContent ) ;
600
+ } catch ( stringifyError ) {
601
+ console . error ( `Error stringifying content: ${ stringifyError } ` ) ;
602
+ content = '[Content could not be displayed due to format issues]' ;
603
+ }
604
+ }
494
605
495
606
return {
496
607
content,
@@ -500,7 +611,13 @@ class AzureDevOpsService {
500
611
} ;
501
612
} catch ( error ) {
502
613
console . error ( `Error getting file content for ${ filePath } :` , error ) ;
503
- throw new Error ( `Failed to retrieve content for file: ${ filePath } ` ) ;
614
+ return {
615
+ content : '' ,
616
+ size : 0 ,
617
+ position : startPosition ,
618
+ length : 0 ,
619
+ error : `Failed to retrieve content for file: ${ filePath } . Error: ${ ( error as Error ) . message } ` ,
620
+ } ;
504
621
}
505
622
}
506
623
@@ -552,7 +669,7 @@ class AzureDevOpsService {
552
669
startPosition = 0 ,
553
670
length = 100000 ,
554
671
project ?: string
555
- ) : Promise < { content : string ; size : number ; position : number ; length : number ; error ?: string } > {
672
+ ) : Promise < PullRequestFileContent > {
556
673
await this . initialize ( ) ;
557
674
558
675
if ( ! this . gitClient ) {
@@ -601,22 +718,72 @@ class AzureDevOpsService {
601
718
602
719
const fileSize = item ?. contentMetadata ?. contentLength || 0 ;
603
720
604
- // Get the content
605
- const content = await this . gitClient . getItemText (
606
- repositoryId ,
607
- filePath ,
608
- projectName ,
609
- undefined ,
610
- startPosition ,
611
- length ,
612
- {
613
- versionDescriptor : {
614
- version : commitId ,
615
- versionOptions : 0 , // Use exact version
616
- versionType : 1 , // Commit
617
- } ,
721
+ // Handle binary files
722
+ if ( this . isBinaryFile ( filePath ) ) {
723
+ return {
724
+ content : `[Binary file not displayed - ${ Math . round ( fileSize / 1024 ) } KB]` ,
725
+ size : fileSize ,
726
+ position : startPosition ,
727
+ length : 0 ,
728
+ error : undefined ,
729
+ } ;
730
+ }
731
+
732
+ // Get the content - carefully handle the response to prevent circular references
733
+ let rawContent ;
734
+ try {
735
+ rawContent = await this . gitClient . getItemText (
736
+ repositoryId ,
737
+ filePath ,
738
+ projectName ,
739
+ undefined ,
740
+ startPosition ,
741
+ length ,
742
+ {
743
+ versionDescriptor : {
744
+ version : commitId ,
745
+ versionOptions : 0 , // Use exact version
746
+ versionType : 1 , // Commit
747
+ } ,
748
+ }
749
+ ) ;
750
+ } catch ( textError ) {
751
+ console . error ( `Error fetching file text: ${ textError } ` ) ;
752
+ // If getItemText fails, try to get content as Buffer and convert it
753
+ const contentBuffer = await this . gitClient . getItemContent (
754
+ repositoryId ,
755
+ filePath ,
756
+ projectName ,
757
+ undefined ,
758
+ {
759
+ versionDescriptor : {
760
+ version : commitId ,
761
+ versionOptions : 0 ,
762
+ versionType : 1 ,
763
+ } ,
764
+ }
765
+ ) ;
766
+
767
+ if ( Buffer . isBuffer ( contentBuffer ) ) {
768
+ rawContent = contentBuffer . toString ( 'utf8' ) ;
769
+ } else {
770
+ throw new Error ( 'Failed to retrieve file content in any format' ) ;
618
771
}
619
- ) ;
772
+ }
773
+
774
+ // Ensure content is a proper string
775
+ let content = '' ;
776
+ if ( typeof rawContent === 'string' ) {
777
+ content = rawContent ;
778
+ } else if ( rawContent && typeof rawContent === 'object' ) {
779
+ // If it's an object but not a string, try to convert it safely
780
+ try {
781
+ content = safeStringify ( rawContent ) ;
782
+ } catch ( stringifyError ) {
783
+ console . error ( `Error stringifying content: ${ stringifyError } ` ) ;
784
+ content = '[Content could not be displayed due to format issues]' ;
785
+ }
786
+ }
620
787
621
788
return {
622
789
content,
@@ -625,6 +792,7 @@ class AzureDevOpsService {
625
792
length : content . length ,
626
793
} ;
627
794
} catch ( error ) {
795
+ console . error ( `Error getting file from branch: ${ error } ` ) ;
628
796
return {
629
797
content : '' ,
630
798
size : 0 ,
0 commit comments