Skip to content

Commit 939b419

Browse files
committed
Implemented initial support for Rock installation to come from git repository.
1 parent 7dedc8d commit 939b419

3 files changed

Lines changed: 203 additions & 6 deletions

File tree

src/SparkDevNetwork.Rock.DevTool/Data/RockData.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,24 @@ class RockData
1010
{
1111
/// <summary>
1212
/// The version number to install in the environment. An empty string or
13-
/// missing value indicates that the Rock instance will be handled manually.
13+
/// missing value indicates that the Rock instance will be handled manually
14+
/// or via git reference.
1415
/// </summary>
1516
[JsonPropertyName( "version" )]
1617
public string? Version { get; set; }
1718

19+
/// <summary>
20+
/// The URL to the git repository to install Rock from.
21+
/// </summary>
22+
[JsonPropertyName( "url" )]
23+
public string? Url { get; set; }
24+
25+
/// <summary>
26+
/// The branch name to checkout in the Rock repository.
27+
/// </summary>
28+
[JsonPropertyName( "branch" )]
29+
public string? Branch { get; set; }
30+
1831
/// <summary>
1932
/// Additional data in the JSON stream that we don't know about.
2033
/// </summary>

src/SparkDevNetwork.Rock.DevTool/DevEnvironment/PluginInstallation.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ public bool IsClean()
319319
return true;
320320
}
321321

322-
// If the directory exists but is empty iti s considered clean.
322+
// If the directory exists but is empty it is considered clean.
323323
if ( _fs.Directory.GetFiles( _pluginPath ).Length == 0 && _fs.Directory.GetDirectories( _pluginPath ).Length == 0 )
324324
{
325325
return true;
@@ -343,7 +343,7 @@ public bool IsClean()
343343
/// <param name="destinationDirectory">The path to the directory to clone the repository into.</param>
344344
/// <param name="branch">If specified the name of the remote branch to clone; otherwise the default branch will be cloned.</param>
345345
/// <param name="progress">An optional progress reporter for the clone progress.</param>
346-
private static void Clone( string remoteUrl, string destinationDirectory, string? branch, IProgress<double>? progress )
346+
internal static void Clone( string remoteUrl, string destinationDirectory, string? branch, IProgress<double>? progress )
347347
{
348348
Repository.Clone( remoteUrl, destinationDirectory, new CloneOptions
349349
{
@@ -372,7 +372,7 @@ private static void Clone( string remoteUrl, string destinationDirectory, string
372372
/// <param name="supportedTypes">The supported authentication types.</param>
373373
/// <returns>A set of credentials to authenticate with.</returns>
374374
/// <exception cref="NoCredentialsException">Thrown if no credentials are available.</exception>
375-
private static UsernamePasswordCredentials GetCredentials( string repoUrl, string usernameFromUrl, SupportedCredentialTypes supportedTypes )
375+
internal static UsernamePasswordCredentials GetCredentials( string repoUrl, string usernameFromUrl, SupportedCredentialTypes supportedTypes )
376376
{
377377
var uri = new Uri( repoUrl );
378378
string? username = null;
@@ -444,7 +444,7 @@ private static UsernamePasswordCredentials GetCredentials( string repoUrl, strin
444444
/// </summary>
445445
/// <param name="repository">The repository.</param>
446446
/// <returns>The name of the branch or <c>null</c> if not on any branch.</returns>
447-
private static string? GetCurrentBranch( Repository repository )
447+
internal static string? GetCurrentBranch( Repository repository )
448448
{
449449
var reference = repository.Head.Reference.TargetIdentifier;
450450

src/SparkDevNetwork.Rock.DevTool/DevEnvironment/RockInstallation.cs

Lines changed: 185 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
using System.Reflection;
44
using System.Text.Json;
55

6+
using LibGit2Sharp;
7+
68
using Microsoft.Extensions.Logging;
79

810
using Semver;
@@ -95,6 +97,11 @@ public RockInstallation( string rockPath, RockData data, IFileSystem fileSystem,
9597
/// <returns>A <see cref="Task"/> that indicates when the operation has completed.</returns>
9698
public Task InstallRockAsync()
9799
{
100+
if ( !string.IsNullOrEmpty( _data.Url ) && !string.IsNullOrEmpty( _data.Branch ) )
101+
{
102+
return InstallOrUpdateRockFromGitAsync();
103+
}
104+
98105
if ( !SemVersion.TryParse( _data.Version, SemVersionStyles.Strict, out var version ) )
99106
{
100107
throw new Exception( "Invalid Rock version specified in configuration." );
@@ -159,6 +166,95 @@ await progress.StartAsync( async ctx =>
159166
_console.WriteLine();
160167
}
161168

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+
162258
/// <summary>
163259
/// Extract all the files in the archive into the destination path on disk.
164260
/// </summary>
@@ -363,7 +459,12 @@ bool RemoveDirectory( string directory )
363459
/// <returns>An instance of <see cref="EnvironmentStatusItem"/> that describes the status.</returns>
364460
public RockStatusItem GetRockStatus()
365461
{
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 ) )
367468
{
368469
return new RockStatusItem( [] );
369470
}
@@ -428,6 +529,63 @@ public RockStatusItem GetRockStatus()
428529
return new RockStatusItem( fileStatuses );
429530
}
430531

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+
431589
/// <summary>
432590
/// Gets a list of status items that reflect the Rock installation status
433591
/// for each individual file.
@@ -491,6 +649,32 @@ public RockStatusItem GetRockStatus()
491649
/// <returns><c>true</c> if the Rock installation is in a clean state; otherwise <c>false</c>.</returns>
492650
public bool IsRockClean()
493651
{
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+
494678
var items = GetRockFileStatuses();
495679

496680
if ( items == null )

0 commit comments

Comments
 (0)