|
3 | 3 | using System.Reflection; |
4 | 4 | using System.Text.Json; |
5 | 5 |
|
| 6 | +using LibGit2Sharp; |
| 7 | + |
6 | 8 | using Microsoft.Extensions.Logging; |
7 | 9 |
|
8 | 10 | using Semver; |
@@ -95,6 +97,11 @@ public RockInstallation( string rockPath, RockData data, IFileSystem fileSystem, |
95 | 97 | /// <returns>A <see cref="Task"/> that indicates when the operation has completed.</returns> |
96 | 98 | public Task InstallRockAsync() |
97 | 99 | { |
| 100 | + if ( !string.IsNullOrEmpty( _data.Url ) && !string.IsNullOrEmpty( _data.Branch ) ) |
| 101 | + { |
| 102 | + return InstallOrUpdateRockFromGitAsync(); |
| 103 | + } |
| 104 | + |
98 | 105 | if ( !SemVersion.TryParse( _data.Version, SemVersionStyles.Strict, out var version ) ) |
99 | 106 | { |
100 | 107 | throw new Exception( "Invalid Rock version specified in configuration." ); |
@@ -159,6 +166,95 @@ await progress.StartAsync( async ctx => |
159 | 166 | _console.WriteLine(); |
160 | 167 | } |
161 | 168 |
|
| 169 | + /// <summary> |
| 170 | + /// Installs or updates Rock from a git repository. If Rock is not yet installed then |
| 171 | + /// it will be installed. Otherwise it will be updated. |
| 172 | + /// </summary> |
| 173 | + /// <param name="context">The context used to report progress.</param> |
| 174 | + public async Task InstallOrUpdateRockFromGitAsync() |
| 175 | + { |
| 176 | + _console.MarkupLineInterpolated( $"Installing Rock from [cyan]{_data.Url}[/]" ); |
| 177 | + |
| 178 | + var progress = _console.Progress(); |
| 179 | + |
| 180 | + await progress.StartAsync( async ctx => |
| 181 | + { |
| 182 | + if ( !_fs.Directory.Exists( _rockPath ) || !Repository.IsValid( _rockPath ) ) |
| 183 | + { |
| 184 | + var progress = ctx.AddTask( $"Installing {_rockPath}", true, 1 ); |
| 185 | + InstallRockFromGit( progress ); |
| 186 | + } |
| 187 | + else |
| 188 | + { |
| 189 | + var progress = ctx.AddTask( $"Updating {_rockPath}", true, 1 ); |
| 190 | + UpdateRockFromGit( progress ); |
| 191 | + } |
| 192 | + }); |
| 193 | + |
| 194 | + _console.MarkupLineInterpolated( $"Installed Rock [cyan]{_data.Branch}[/] into [cyan]{_rockPath.Replace( '/', Path.DirectorySeparatorChar )}[/]" ); |
| 195 | + } |
| 196 | + |
| 197 | + /// <summary> |
| 198 | + /// Installs Rock from a git repository into the environment. |
| 199 | + /// </summary> |
| 200 | + /// <param name="progress">The progress reporter.</param> |
| 201 | + private void InstallRockFromGit( IProgress<double>? progress ) |
| 202 | + { |
| 203 | + if ( string.IsNullOrWhiteSpace( _data.Url ) || string.IsNullOrWhiteSpace( _data.Branch ) ) |
| 204 | + { |
| 205 | + throw new InvalidOperationException( "Can't install Rock without repository url and branch name." ); |
| 206 | + } |
| 207 | + |
| 208 | + PluginInstallation.Clone( _data.Url, |
| 209 | + _rockPath, |
| 210 | + _data.Branch, |
| 211 | + progress ); |
| 212 | + } |
| 213 | + |
| 214 | + /// <summary> |
| 215 | + /// Update Rock by ensuring it is on the correct branch and also |
| 216 | + /// pulls any changes from the remote. |
| 217 | + /// </summary> |
| 218 | + /// <param name="progress">An optional progress reporter.</param> |
| 219 | + private void UpdateRockFromGit( IProgress<double>? progress ) |
| 220 | + { |
| 221 | + if ( string.IsNullOrWhiteSpace( _data.Url ) || string.IsNullOrWhiteSpace( _data.Branch ) ) |
| 222 | + { |
| 223 | + throw new InvalidOperationException( "Can't update Rock without repository url and branch name." ); |
| 224 | + } |
| 225 | + |
| 226 | + var repo = new Repository( _rockPath ); |
| 227 | + var signature = repo.Config.BuildSignature( DateTimeOffset.Now ); |
| 228 | + var currentBranch = PluginInstallation.GetCurrentBranch( repo ); |
| 229 | + |
| 230 | + if ( currentBranch != _data.Branch ) |
| 231 | + { |
| 232 | + LibGit2Sharp.Commands.Checkout( repo, _data.Branch ); |
| 233 | + } |
| 234 | + |
| 235 | + var pullOptions = new PullOptions |
| 236 | + { |
| 237 | + FetchOptions = new FetchOptions |
| 238 | + { |
| 239 | + CredentialsProvider = PluginInstallation.GetCredentials, |
| 240 | + OnTransferProgress = ( transferProgress ) => |
| 241 | + { |
| 242 | + progress?.Report( transferProgress.ReceivedObjects / ( double ) transferProgress.TotalObjects ); |
| 243 | + return true; |
| 244 | + } |
| 245 | + }, |
| 246 | + MergeOptions = new MergeOptions |
| 247 | + { |
| 248 | + FailOnConflict = true, |
| 249 | + FastForwardStrategy = FastForwardStrategy.FastForwardOnly |
| 250 | + } |
| 251 | + }; |
| 252 | + |
| 253 | + LibGit2Sharp.Commands.Pull( repo, signature, pullOptions ); |
| 254 | + |
| 255 | + progress?.Report( 1 ); |
| 256 | + } |
| 257 | + |
162 | 258 | /// <summary> |
163 | 259 | /// Extract all the files in the archive into the destination path on disk. |
164 | 260 | /// </summary> |
@@ -363,7 +459,12 @@ bool RemoveDirectory( string directory ) |
363 | 459 | /// <returns>An instance of <see cref="EnvironmentStatusItem"/> that describes the status.</returns> |
364 | 460 | public RockStatusItem GetRockStatus() |
365 | 461 | { |
366 | | - if ( _data.Version == "custom" ) |
| 462 | + if ( !string.IsNullOrEmpty( _data.Url ) && !string.IsNullOrEmpty( _data.Branch ) ) |
| 463 | + { |
| 464 | + return GetRockGitStatus(); |
| 465 | + } |
| 466 | + |
| 467 | + if ( _data.Version == "custom" || string.IsNullOrEmpty( _data.Version ) ) |
367 | 468 | { |
368 | 469 | return new RockStatusItem( [] ); |
369 | 470 | } |
@@ -428,6 +529,63 @@ public RockStatusItem GetRockStatus() |
428 | 529 | return new RockStatusItem( fileStatuses ); |
429 | 530 | } |
430 | 531 |
|
| 532 | + /// <summary> |
| 533 | + /// Gets the status of the Rock installation based on git information. This |
| 534 | + /// will be used when the Rock installation is being managed via git instead of |
| 535 | + /// a binary installation. |
| 536 | + /// </summary> |
| 537 | + /// <returns>An instance of <see cref="EnvironmentStatusItem"/> that describes the status.</returns> |
| 538 | + private RockStatusItem GetRockGitStatus() |
| 539 | + { |
| 540 | + if ( !Repository.IsValid( _rockPath ) ) |
| 541 | + { |
| 542 | + _logger.LogError( "Rock {path} is not a git repository.", _rockPath ); |
| 543 | + return new RockStatusItem( "is not a git repository.", null ); |
| 544 | + } |
| 545 | + |
| 546 | + var repository = new Repository( _rockPath ); |
| 547 | + var currentBranch = PluginInstallation.GetCurrentBranch( repository ); |
| 548 | + |
| 549 | + if ( currentBranch == null ) |
| 550 | + { |
| 551 | + _logger.LogInformation( "Rock {path} is not on a branch.", _rockPath ); |
| 552 | + return new RockStatusItem( "is not on a branch.", null ); |
| 553 | + } |
| 554 | + |
| 555 | + if ( _data.Branch != currentBranch ) |
| 556 | + { |
| 557 | + _logger.LogInformation( "Rock {path} is on branch {repoBranch} instead of {expectedBranch}.", _rockPath, currentBranch, _data.Branch ); |
| 558 | + return new RockStatusItem( $"is on branch {currentBranch} but should be {_data.Branch}.", null ); |
| 559 | + } |
| 560 | + |
| 561 | + var remote = repository.Network.Remotes[repository.Head.RemoteName]; |
| 562 | + |
| 563 | + if ( remote.Url != _data.Url ) |
| 564 | + { |
| 565 | + _logger.LogInformation( "Rock {path} is using remote URL {repoUrl} instead of {expectedUrl}.", _rockPath, remote.Url, _data.Url ); |
| 566 | + return new RockStatusItem( $"is using remote URL {remote.Url} but should be {_data.Url}.", null ); |
| 567 | + } |
| 568 | + |
| 569 | + var refSpecs = remote.FetchRefSpecs.Select( r => r.Specification ); |
| 570 | + |
| 571 | + if ( !repository.Head.TrackingDetails.BehindBy.HasValue ) |
| 572 | + { |
| 573 | + return new RockStatusItem( "has no upstream remote configured.", null ); |
| 574 | + } |
| 575 | + |
| 576 | + LibGit2Sharp.Commands.Fetch( repository, remote.Name, refSpecs, new FetchOptions |
| 577 | + { |
| 578 | + CredentialsProvider = PluginInstallation.GetCredentials, |
| 579 | + }, "Fetching remote" ); |
| 580 | + |
| 581 | + if ( repository.Head.TrackingDetails.BehindBy.Value > 0 ) |
| 582 | + { |
| 583 | + return new RockStatusItem( $"is behind by {repository.Head.TrackingDetails.BehindBy} commits.", null ); |
| 584 | + } |
| 585 | + |
| 586 | + return new RockStatusItem( [] ); |
| 587 | + } |
| 588 | + |
431 | 589 | /// <summary> |
432 | 590 | /// Gets a list of status items that reflect the Rock installation status |
433 | 591 | /// for each individual file. |
@@ -491,6 +649,32 @@ public RockStatusItem GetRockStatus() |
491 | 649 | /// <returns><c>true</c> if the Rock installation is in a clean state; otherwise <c>false</c>.</returns> |
492 | 650 | public bool IsRockClean() |
493 | 651 | { |
| 652 | + if ( !string.IsNullOrEmpty( _data.Url ) && !string.IsNullOrEmpty( _data.Branch ) ) |
| 653 | + { |
| 654 | + // If the directory does not exist, it is considered clean so that |
| 655 | + // an update command can execute. |
| 656 | + if ( !_fs.Directory.Exists( _rockPath ) ) |
| 657 | + { |
| 658 | + return true; |
| 659 | + } |
| 660 | + |
| 661 | + // If the directory exists but is empty it is considered clean. |
| 662 | + if ( _fs.Directory.GetFiles( _rockPath ).Length == 0 && _fs.Directory.GetDirectories( _rockPath ).Length == 0 ) |
| 663 | + { |
| 664 | + return true; |
| 665 | + } |
| 666 | + |
| 667 | + if ( !Repository.IsValid( _rockPath ) ) |
| 668 | + { |
| 669 | + _logger.LogError( "Rock {path} is not a git repository.", _rockPath ); |
| 670 | + return false; |
| 671 | + } |
| 672 | + |
| 673 | + using var repository = new Repository( _rockPath ); |
| 674 | + |
| 675 | + return !repository.RetrieveStatus().IsDirty; |
| 676 | + } |
| 677 | + |
494 | 678 | var items = GetRockFileStatuses(); |
495 | 679 |
|
496 | 680 | if ( items == null ) |
|
0 commit comments