@@ -12,6 +12,7 @@ public sealed class DevelopViewModel : ObservableObject
1212{
1313 private readonly MountService _mount ;
1414 private readonly ContributeService _contribute ;
15+ private readonly CacheDriftService _drift ;
1516 private readonly GitHubAuthService _auth ;
1617 private readonly GitHubApiService _api ;
1718 private readonly GitService _git ;
@@ -26,6 +27,7 @@ public sealed class DevelopViewModel : ObservableObject
2627
2728 public ObservableCollection < GitPackageRow > GitPackages { get ; } = new ( ) ;
2829 public ObservableCollection < MountedRow > Mounted { get ; } = new ( ) ;
30+ public ObservableCollection < CacheDriftRow > DriftPackages { get ; } = new ( ) ;
2931
3032 public string InstallName => _install ? . DisplayName ?? L . Tr ( "develop.install.none" ) ;
3133 public bool HasInstall => _install is not null && _install . HasUnityProject ;
@@ -35,6 +37,11 @@ public sealed class DevelopViewModel : ObservableObject
3537 public bool NoGitPackages => HasInstall && GitPackages . Count == 0 ;
3638 public bool HasMounted => Mounted . Count > 0 ;
3739
40+ private bool _driftScanned ;
41+ public bool DriftScanned { get => _driftScanned ; private set { if ( SetField ( ref _driftScanned , value ) ) OnPropertyChanged ( nameof ( NoDrift ) ) ; } }
42+ public bool HasDrift => DriftPackages . Count > 0 ;
43+ public bool NoDrift => DriftScanned && DriftPackages . Count == 0 ;
44+
3845 public bool GitAvailable => _git . IsAvailable ;
3946 public bool GitMissing => ! _git . IsAvailable ;
4047 public bool GhAvailable => _auth . GhAvailable ;
@@ -58,23 +65,29 @@ public bool AuthOk
5865 public RelayCommand RecheckCommand { get ; }
5966 public RelayCommand ApplyPatCommand { get ; }
6067 public RelayCommand < GitPackageRow > MountCommand { get ; }
68+ public RelayCommand MountAllCommand { get ; }
6169 public RelayCommand < MountedRow > SubmitPrCommand { get ; }
6270 public RelayCommand < MountedRow > SwapBackCommand { get ; }
6371 public RelayCommand < MountedRow > OpenInUnityCommand { get ; }
72+ public RelayCommand ScanDriftCommand { get ; }
73+ public RelayCommand < CacheDriftRow > OpenDriftPrCommand { get ; }
6474
65- public DevelopViewModel ( MountService mount , ContributeService contribute , GitHubAuthService auth ,
75+ public DevelopViewModel ( MountService mount , ContributeService contribute , CacheDriftService drift , GitHubAuthService auth ,
6676 GitHubApiService api , GitService git , MountRegistry registry , MainWindowViewModel shell )
6777 {
68- _mount = mount ; _contribute = contribute ; _auth = auth ; _api = api ; _git = git ; _registry = registry ; _shell = shell ;
78+ _mount = mount ; _contribute = contribute ; _drift = drift ; _auth = auth ; _api = api ; _git = git ; _registry = registry ; _shell = shell ;
6979
7080 RefreshCommand = new RelayCommand ( RefreshAsync ) ;
7181 InstallGitCommand = new RelayCommand ( ( ) => OpenUrl ( "https://git-scm.com/downloads" ) ) ;
7282 RecheckCommand = new RelayCommand ( RefreshAsync ) ;
7383 ApplyPatCommand = new RelayCommand ( ApplyPatAsync ) ;
7484 MountCommand = new RelayCommand < GitPackageRow > ( MountAsync ) ;
85+ MountAllCommand = new RelayCommand ( MountAllAsync ) ;
7586 SubmitPrCommand = new RelayCommand < MountedRow > ( SubmitPrAsync ) ;
7687 SwapBackCommand = new RelayCommand < MountedRow > ( SwapBackAsync ) ;
7788 OpenInUnityCommand = new RelayCommand < MountedRow > ( row => { if ( _install is not null ) _ = _shell . OpenProjectInUnityAsync ( _install ) ; } ) ;
89+ ScanDriftCommand = new RelayCommand ( ScanDriftAsync ) ;
90+ OpenDriftPrCommand = new RelayCommand < CacheDriftRow > ( OpenDriftPrAsync ) ;
7891 }
7992
8093 public void SetActiveInstall ( BasisInstall install )
@@ -99,6 +112,8 @@ private void Refresh()
99112
100113 GitPackages . Clear ( ) ;
101114 Mounted . Clear ( ) ;
115+ DriftPackages . Clear ( ) ;
116+ DriftScanned = false ;
102117
103118 if ( _install is not null && _install . HasUnityProject )
104119 {
@@ -119,6 +134,8 @@ private void Refresh()
119134 OnPropertyChanged ( nameof ( HasGitPackages ) ) ;
120135 OnPropertyChanged ( nameof ( NoGitPackages ) ) ;
121136 OnPropertyChanged ( nameof ( HasMounted ) ) ;
137+ OnPropertyChanged ( nameof ( HasDrift ) ) ;
138+ OnPropertyChanged ( nameof ( NoDrift ) ) ;
122139 }
123140
124141 private async Task CheckAuthAsync ( )
@@ -176,6 +193,97 @@ private async Task MountAsync(GitPackageRow? row)
176193 finally { IsBusy = false ; }
177194 }
178195
196+ // Mount every git package that isn't mounted yet, skipping any whose folder is already present.
197+ private async Task MountAllAsync ( )
198+ {
199+ if ( _install is null ) return ;
200+ if ( ! _git . IsAvailable ) { _shell . SetStatus ( L . Tr ( "develop.status.gitRequiredMount" ) , StatusKind . Error ) ; return ; }
201+
202+ var rows = GitPackages . ToList ( ) ;
203+ if ( rows . Count == 0 ) return ;
204+
205+ IsBusy = true ;
206+ int ok = 0 , skipped = 0 , failed = 0 ;
207+ try
208+ {
209+ foreach ( var row in rows )
210+ {
211+ _shell . SetStatus ( L . Tr ( "develop.status.mounting" , row . Id ) ) ;
212+ var result = await _mount . MountAsync ( _install , row . Id , row . GitUrl , line => Dispatcher . UIThread . Post ( ( ) => _shell . SetStatus ( line ) ) ) ;
213+ if ( result . Ok ) ok ++ ;
214+ else if ( result . Error is not null && result . Error . Contains ( "already exists" , StringComparison . OrdinalIgnoreCase ) ) skipped ++ ;
215+ else failed ++ ;
216+ }
217+ Refresh ( ) ;
218+ _shell . SetStatus ( L . Tr ( "develop.status.mountedAll" , ok , skipped , failed ) , failed > 0 ? StatusKind . Error : StatusKind . Success ) ;
219+ }
220+ catch ( Exception ex ) { _shell . SetStatus ( L . Tr ( "develop.status.mountError" , ex . Message ) , StatusKind . Error ) ; }
221+ finally { IsBusy = false ; }
222+ }
223+
224+ // Scan the project's git packages for accidental edits made directly in Library/PackageCache.
225+ private async Task ScanDriftAsync ( )
226+ {
227+ if ( _install is null ) return ;
228+ if ( ! _git . IsAvailable ) { _shell . SetStatus ( L . Tr ( "develop.status.gitRequiredMount" ) , StatusKind . Error ) ; return ; }
229+
230+ IsBusy = true ;
231+ DriftPackages . Clear ( ) ;
232+ OnPropertyChanged ( nameof ( HasDrift ) ) ;
233+ _shell . SetStatus ( L . Tr ( "develop.status.scanningDrift" ) ) ;
234+ try
235+ {
236+ var drifts = await _drift . ScanAsync ( _install , line => Dispatcher . UIThread . Post ( ( ) => _shell . SetStatus ( line ) ) ) ;
237+ foreach ( var d in drifts )
238+ {
239+ var summary = string . Join ( ", " , d . ChangedFiles . Take ( 6 ) ) ;
240+ if ( d . ChangedFiles . Count > 6 ) summary += ", …" ;
241+ DriftPackages . Add ( new CacheDriftRow ( d . PackageId , d . WorkClonePath , d . GitUrl , d . ChangedFiles . Count , summary ) ) ;
242+ }
243+ DriftScanned = true ;
244+ OnPropertyChanged ( nameof ( HasDrift ) ) ;
245+ OnPropertyChanged ( nameof ( NoDrift ) ) ;
246+ _shell . SetStatus (
247+ drifts . Count == 0 ? L . Tr ( "develop.status.noDrift" ) : L . Tr ( "develop.status.driftFound" , drifts . Count ) ,
248+ drifts . Count == 0 ? StatusKind . Success : StatusKind . Info ) ;
249+ }
250+ catch ( Exception ex ) { _shell . SetStatus ( L . Tr ( "develop.status.driftError" , ex . Message ) , StatusKind . Error ) ; }
251+ finally { IsBusy = false ; }
252+ }
253+
254+ // Turn an accidental cache edit into a pull request, reusing the mounted-package PR flow on the work clone.
255+ private async Task OpenDriftPrAsync ( CacheDriftRow ? row )
256+ {
257+ if ( row is null ) return ;
258+ if ( ! _git . IsAvailable ) { _shell . SetStatus ( L . Tr ( "develop.status.gitRequired" ) , StatusKind . Error ) ; return ; }
259+
260+ var token = await _auth . GetTokenAsync ( ) ;
261+ if ( string . IsNullOrEmpty ( token ) ) { _shell . SetStatus ( L . Tr ( "develop.status.signInFirst" ) , StatusKind . Error ) ; return ; }
262+ var user = await _api . GetUserAsync ( token ) ;
263+ if ( user is null ) { _shell . SetStatus ( L . Tr ( "develop.status.loginUnverified" ) , StatusKind . Error ) ; return ; }
264+
265+ var draft = await Dialogs . SubmitPrAsync ( row . PackageId ) ;
266+ if ( draft is null ) return ;
267+
268+ IsBusy = true ;
269+ _shell . SetStatus ( L . Tr ( "develop.status.submittingPr" , row . PackageId ) ) ;
270+ try
271+ {
272+ var result = await _contribute . SubmitPrAsync ( row . WorkClonePath , token , user , draft , line => Dispatcher . UIThread . Post ( ( ) => _shell . SetStatus ( line ) ) ) ;
273+ if ( result . Ok )
274+ {
275+ _shell . SetStatus ( L . Tr ( "develop.status.prOpened" , result . Forked ? L . Tr ( "develop.status.viaFork" ) : "" , result . PrUrl ) , StatusKind . Success ) ;
276+ if ( ! string . IsNullOrEmpty ( result . PrUrl ) ) OpenUrl ( result . PrUrl ! ) ;
277+ DriftPackages . Remove ( row ) ;
278+ OnPropertyChanged ( nameof ( HasDrift ) ) ;
279+ OnPropertyChanged ( nameof ( NoDrift ) ) ;
280+ }
281+ else _shell . SetStatus ( result . Error ?? L . Tr ( "develop.status.prFailed" ) , StatusKind . Error ) ;
282+ }
283+ catch ( Exception ex ) { _shell . SetStatus ( L . Tr ( "develop.status.prError" , ex . Message ) , StatusKind . Error ) ; }
284+ finally { IsBusy = false ; }
285+ }
286+
179287 private async Task SwapBackAsync ( MountedRow ? row )
180288 {
181289 if ( row is null || _install is null ) return ;
@@ -237,3 +345,5 @@ private static void TryForceDelete(string path)
237345public sealed record GitPackageRow ( string Id , string GitUrl , string Host , string Slug , bool InSubfolder ) ;
238346
239347public sealed record MountedRow ( string Id , string FolderPath , string OriginalManifestValue ) ;
348+
349+ public sealed record CacheDriftRow ( string PackageId , string WorkClonePath , string GitUrl , int ChangedCount , string ChangedSummary ) ;
0 commit comments