@@ -50,9 +50,13 @@ export const enum ResourceGroupType {
5050 Merge ,
5151 Index ,
5252 WorkingTree ,
53- Untracked
53+ Untracked ,
54+ ParentChanges
5455}
5556
57+ export const PARENT_CHANGES_GROUP_ID = 'parentChanges' ;
58+ export const PARENT_CHANGE_CONTEXT_VALUE = 'parentChange' ;
59+
5660export class Resource implements SourceControlResourceState {
5761
5862 static getStatusLetter ( type : Status ) : string {
@@ -189,7 +193,12 @@ export class Resource implements SourceControlResourceState {
189193 get type ( ) : Status { return this . _type ; }
190194 get original ( ) : Uri { return this . _resourceUri ; }
191195 get renameResourceUri ( ) : Uri | undefined { return this . _renameResourceUri ; }
192- get contextValue ( ) : string | undefined { return this . _repositoryKind ; }
196+ get contextValue ( ) : string | undefined {
197+ if ( this . _resourceGroupType === ResourceGroupType . ParentChanges ) {
198+ return PARENT_CHANGE_CONTEXT_VALUE ;
199+ }
200+ return this . _repositoryKind ;
201+ }
193202
194203 private static Icons = {
195204 light : {
@@ -588,6 +597,14 @@ class ResourceCommandResolver {
588597 }
589598
590599 private getLeftResource ( resource : Resource ) : ModifiedOrOriginal {
600+ // Parent changes: left = file at origin/<default_branch>
601+ if ( resource . resourceGroupType === ResourceGroupType . ParentChanges ) {
602+ const ref = this . repository . parentBranchRef ;
603+ if ( ! ref ) { return { } ; }
604+ if ( resource . type === Status . INDEX_ADDED || resource . type === Status . UNTRACKED ) { return { } ; }
605+ return { original : toGitUri ( resource . original , ref ) } ;
606+ }
607+
591608 switch ( resource . type ) {
592609 case Status . INDEX_MODIFIED :
593610 case Status . INDEX_RENAMED :
@@ -606,6 +623,12 @@ class ResourceCommandResolver {
606623 }
607624
608625 private getRightResource ( resource : Resource ) : ModifiedOrOriginal {
626+ // Parent changes: right = working tree file
627+ if ( resource . resourceGroupType === ResourceGroupType . ParentChanges ) {
628+ if ( resource . type === Status . DELETED ) { return { } ; }
629+ return { modified : resource . resourceUri } ;
630+ }
631+
609632 switch ( resource . type ) {
610633 case Status . INDEX_MODIFIED :
611634 case Status . INDEX_ADDED :
@@ -649,6 +672,10 @@ class ResourceCommandResolver {
649672 private getTitle ( resource : Resource ) : string {
650673 const basename = path . basename ( resource . resourceUri . fsPath ) ;
651674
675+ if ( resource . resourceGroupType === ResourceGroupType . ParentChanges ) {
676+ return l10n . t ( '{0} (Parent)' , basename ) ;
677+ }
678+
652679 switch ( resource . type ) {
653680 case Status . INDEX_MODIFIED :
654681 case Status . INDEX_RENAMED :
@@ -748,6 +775,12 @@ export class Repository implements Disposable {
748775 private _untrackedGroup : SourceControlResourceGroup ;
749776 get untrackedGroup ( ) : GitResourceGroup { return this . _untrackedGroup as GitResourceGroup ; }
750777
778+ private _parentChangesGroup : SourceControlResourceGroup ;
779+ get parentChangesGroup ( ) : GitResourceGroup { return this . _parentChangesGroup as GitResourceGroup ; }
780+
781+ private _parentBranchRef : string | undefined ;
782+ get parentBranchRef ( ) : string | undefined { return this . _parentBranchRef ; }
783+
751784 private _EMPTY_TREE : string | undefined ;
752785
753786 private _HEAD : Branch | undefined ;
@@ -894,6 +927,9 @@ export class Repository implements Disposable {
894927 private branchProtection = new Map < string , BranchProtectionMatcher [ ] > ( ) ;
895928 private commitCommandCenter : CommitCommandsCenter ;
896929 private resourceCommandResolver = new ResourceCommandResolver ( this ) ;
930+ private static readonly PARENT_FETCH_THROTTLE_MS = 60_000 ;
931+ private _lastParentFetchTime = 0 ;
932+ private _parentChangesGeneration = 0 ;
897933 private updateModelStateCancellationTokenSource : CancellationTokenSource | undefined ;
898934 private disposables : Disposable [ ] = [ ] ;
899935
@@ -997,6 +1033,7 @@ export class Repository implements Disposable {
9971033 this . _indexGroup = this . _sourceControl . createResourceGroup ( 'index' , l10n . t ( 'Staged Changes' ) , { multiDiffEditorEnableViewChanges : true } ) ;
9981034 this . _workingTreeGroup = this . _sourceControl . createResourceGroup ( 'workingTree' , l10n . t ( 'Changes' ) , { multiDiffEditorEnableViewChanges : true } ) ;
9991035 this . _untrackedGroup = this . _sourceControl . createResourceGroup ( 'untracked' , l10n . t ( 'Untracked Changes' ) , { multiDiffEditorEnableViewChanges : true } ) ;
1036+ this . _parentChangesGroup = this . _sourceControl . createResourceGroup ( PARENT_CHANGES_GROUP_ID , l10n . t ( 'Changes in Parent' ) ) ;
10001037
10011038 const updateIndexGroupVisibility = ( ) => {
10021039 const config = workspace . getConfiguration ( 'git' , root ) ;
@@ -1033,11 +1070,13 @@ export class Repository implements Disposable {
10331070
10341071 this . mergeGroup . hideWhenEmpty = true ;
10351072 this . untrackedGroup . hideWhenEmpty = true ;
1073+ this . parentChangesGroup . hideWhenEmpty = true ;
10361074
10371075 this . disposables . push ( this . mergeGroup ) ;
10381076 this . disposables . push ( this . indexGroup ) ;
10391077 this . disposables . push ( this . workingTreeGroup ) ;
10401078 this . disposables . push ( this . untrackedGroup ) ;
1079+ this . disposables . push ( this . parentChangesGroup ) ;
10411080
10421081 // Don't allow auto-fetch in untrusted workspaces
10431082 if ( workspace . isTrusted ) {
@@ -2846,6 +2885,9 @@ export class Repository implements Disposable {
28462885 this . _refs = refs ;
28472886 this . _updateResourceGroupsState ( resourceGroups ) ;
28482887
2888+ // Update parent branch changes (non-blocking)
2889+ this . _updateParentChanges ( ) . catch ( ( ) => { /* silently ignore */ } ) ;
2890+
28492891 this . _onDidChangeStatus . fire ( ) ;
28502892 }
28512893 catch ( err ) {
@@ -2873,6 +2915,96 @@ export class Repository implements Disposable {
28732915 this . setCountBadge ( ) ;
28742916 }
28752917
2918+ private async _updateParentChanges ( ) : Promise < void > {
2919+ const generation = ++ this . _parentChangesGeneration ;
2920+
2921+ try {
2922+ const defaultBranch = await this . getDefaultBranch ( ) ;
2923+ if ( ! defaultBranch || generation !== this . _parentChangesGeneration ) {
2924+ if ( generation === this . _parentChangesGeneration ) {
2925+ this . parentChangesGroup . resourceStates = [ ] ;
2926+ }
2927+ return ;
2928+ }
2929+
2930+ const parentRef = `${ defaultBranch . remote } /${ defaultBranch . name } ` ;
2931+
2932+ // Fetch latest from the remote default branch (throttled to once per 60s)
2933+ const now = Date . now ( ) ;
2934+ if ( now - this . _lastParentFetchTime > Repository . PARENT_FETCH_THROTTLE_MS ) {
2935+ this . _lastParentFetchTime = now ;
2936+ try {
2937+ await this . fetch ( { remote : defaultBranch . remote ! , ref : defaultBranch . name } ) ;
2938+ } catch {
2939+ // Fetch may fail (offline, no remote, etc.) — continue with stale ref
2940+ }
2941+ }
2942+
2943+ if ( generation !== this . _parentChangesGeneration ) { return ; }
2944+
2945+ // Use merge-base so the diff matches what a PR would show:
2946+ // only files changed on THIS branch since it diverged from origin/main
2947+ const mergeBase = await this . getMergeBase ( parentRef , 'HEAD' ) ;
2948+ if ( ! mergeBase || generation !== this . _parentChangesGeneration ) {
2949+ if ( generation === this . _parentChangesGeneration ) {
2950+ this . parentChangesGroup . resourceStates = [ ] ;
2951+ }
2952+ return ;
2953+ }
2954+ this . _parentBranchRef = mergeBase ;
2955+
2956+ // Diff from merge-base to working tree (committed + staged + unstaged)
2957+ const changes = await this . diffBetweenWithStats2 ( mergeBase ) ;
2958+
2959+ if ( generation !== this . _parentChangesGeneration ) { return ; }
2960+
2961+ const config = workspace . getConfiguration ( 'git' ) ;
2962+ const useIcons = ! config . get < boolean > ( 'decorations.enabled' , true ) ;
2963+
2964+ // Build a set of tracked paths so we can add untracked files without duplicates
2965+ const trackedPaths = new Set ( changes . map ( c => c . uri . fsPath ) ) ;
2966+
2967+ const resources : Resource [ ] = changes . map ( change =>
2968+ new Resource (
2969+ this . resourceCommandResolver ,
2970+ ResourceGroupType . ParentChanges ,
2971+ change . originalUri ,
2972+ change . status ,
2973+ useIcons ,
2974+ change . renameUri ,
2975+ this . kind ,
2976+ )
2977+ ) ;
2978+
2979+ // Add untracked files (from existing groups, already populated by getStatus)
2980+ const untrackedResources = [
2981+ ...this . untrackedGroup . resourceStates ,
2982+ ...this . workingTreeGroup . resourceStates . filter ( r => r . type === Status . UNTRACKED ) ,
2983+ ] ;
2984+ for ( const r of untrackedResources ) {
2985+ if ( ! trackedPaths . has ( r . resourceUri . fsPath ) ) {
2986+ trackedPaths . add ( r . resourceUri . fsPath ) ;
2987+ resources . push ( new Resource (
2988+ this . resourceCommandResolver ,
2989+ ResourceGroupType . ParentChanges ,
2990+ r . resourceUri ,
2991+ Status . UNTRACKED ,
2992+ useIcons ,
2993+ undefined ,
2994+ this . kind ,
2995+ ) ) ;
2996+ }
2997+ }
2998+
2999+ this . parentChangesGroup . resourceStates = resources ;
3000+ this . _onDidChangeStatus . fire ( ) ;
3001+ } catch {
3002+ if ( generation === this . _parentChangesGeneration ) {
3003+ this . parentChangesGroup . resourceStates = [ ] ;
3004+ }
3005+ }
3006+ }
3007+
28763008 private async getStatus ( cancellationToken ?: CancellationToken ) : Promise < GitResourceGroups > {
28773009 if ( cancellationToken && cancellationToken . isCancellationRequested ) {
28783010 throw new CancellationError ( ) ;
0 commit comments