Cmdlets/src/XpandPosh.Cmdlets/Nuget/UpdateNugetProjectVersion/Update-NugetProjectVersion.cs

using System;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Net;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Reactive.Threading.Tasks;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Octokit;
using XpandPosh.Cmdlets.GitHub;
using XpandPosh.CmdLets;
 
namespace XpandPosh.Cmdlets.Nuget.UpdateNugetProjectVersion{
    [CmdletBinding(SupportsShouldProcess = true)]
    [Cmdlet(VerbsData.Update,"NugetProjectVersion",SupportsShouldProcess = true)]
    public class UpdateNugetProjectVersion:GitHubCmdlet,IParameter{
        [Parameter(Mandatory = true)]
        public string Repository{ get; set; }
        [Parameter(Mandatory = true)]
        public string Branch{ get; set; }
        [Parameter(Mandatory = true)]
        public string SourcePath{ get; set; }
        [Parameter(Mandatory=true)]
        public PSObject[] Packages{ get; set; }
 
        protected override async Task ProcessRecordAsync(){
            var lastTagedDate = (GitHubClient.Repository.GetForOrg(Organization, Repository)
                .Select(repository => GitHubClient.Repository
                    .LastTag(repository)
                    .Select(tag => GitHubClient.Repository.Commit.Get(repository.Id, tag.Commit.Sha))).Concat()
                .Concat()
                .Select(tag => tag.Commit.Committer.Date.AddSeconds(1)));
            var dateTimeOffset = await lastTagedDate;
            WriteVerbose($"lastTaggedDate={dateTimeOffset}");
            var commits = GitHubClient.Commits(Organization, Repository,
                dateTimeOffset, Branch).Replay().RefCount();
             
            await commits.WriteVerboseObject(this,commit => commit.Commit.Message);
            var changedPackages = ExistingPackages(this).ToObservable()
                .WriteVerboseObject(this,_ => $"Existing: {_.name}, {_.version} ")
                .SelectMany(tuple => commits.Where(commit => commit.Files.Any(file => file.Filename.Contains(tuple.directory.Name))).Select(_=>tuple)).Distinct()
                .Publish().RefCount();
 
            var subject = new Subject<string>();
            subject.WriteObject(this).Subscribe();
            var synchronizationContext = SynchronizationContext.Current;
            await changedPackages.SelectMany(tuple => GitHubClient.Repository
                    .GetForOrg(Organization, Repository)
                    .SelectMany(_ => CreateTagReference(this, GitHubClient, _, tuple, subject,synchronizationContext))
                    .Select(tag => tuple))
                .Select(UpdateAssemblyInfo)
                .HandleErrors(this)
                .WriteObject(this)
                .ToTask();
        }
 
        internal async Task Test(){
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
            Packages = PsObjects();
            await ProcessRecordAsync();
        }
 
        private string UpdateAssemblyInfo((string name, string version, DirectoryInfo directory) info){
            var newVersion = GetVersion(info);
            if (ShouldProcess($"Update version {newVersion}")){
                var directoryName = info.directory.FullName;
                var path = $@"{directoryName}\Properties\AssemblyInfo.cs";
                var text = File.ReadAllText(path);
                text = Regex.Replace(text, @"Version\(""([^""]*)", $"Version(\"{newVersion}");
                File.WriteAllText(path, text);
                return $"{info.name} version raised from {info.version} to {newVersion} ";
            }
 
            return null;
        }
 
 
        private IObservable<Reference> CreateTagReference(IParameter parameter, GitHubClient appClient,
            Repository repository, (string name, string version, DirectoryInfo directory) tuple,
            IObserver<string> observer, SynchronizationContext synchronizationContext){
            observer.OnNext($"Lookup {tuple.name} heads");
            return appClient.Git.Reference.Get(repository.Id, $"heads/{parameter.Branch}")
                .ToObservable()
                .ObserveOn(synchronizationContext)
                .SelectMany(reference => {
                    var tag = $"{tuple.directory.Name}_{GetVersion(tuple)}";
                    observer.OnNext($"Tagging {repository.Name} with {tag}");
                    if (ShouldProcess($"Creating tag on repo {repository.Name}")){
                        return appClient.Git.Reference.Create(repository.Id,new NewReference($@"refs/tags/{tag}",reference.Object.Sha))
                            .ToObservable().Catch<Reference, ApiValidationException>(ex =>
                                ex.ApiError.Message=="Reference already exists"? Observable.Return<Reference>(null): Observable.Throw<Reference>(ex));
                    }
 
                    return Observable.Return(default(Reference));
 
                });
        }
        private static Version GetVersion((string name, string version, DirectoryInfo directory) info){
            var directoryName = info.directory.FullName;
            var version = new Version(info.version);
            var path = $@"{directoryName}\Properties\AssemblyInfo.cs";
            var text = File.ReadAllText(path);
            var regex = new Regex(@"Version\(""([^""]*)""");
            var newVersion = new Version(version.Major, version.Minor, version.Build, version.Revision + 1);
            var fileVersion = new Version(regex.Match(text).Groups[1].Value);
            if (fileVersion.Build != version.Build){
                newVersion = new Version(fileVersion.Major, fileVersion.Minor, fileVersion.Build, fileVersion.Revision );
            }
 
            return newVersion;
        }
 
        private static (string name, string version, DirectoryInfo directory)[] ExistingPackages(IParameter parameter){
            var packageArgs = parameter.Packages.Select(_ => (name: $"{_.Properties["Name"].Value}", version: $"{_.Properties["Version"].Value}", directory: (DirectoryInfo) null)).ToArray();
 
            var existingPackages = Directory.GetFiles(parameter.SourcePath, "*.csproj", SearchOption.AllDirectories)
                .Where(s => packageArgs.Select(_ => _.name).Any(s.Contains)).ToArray()
                .Select(s => {
                    var valueTuple = packageArgs.First(_ => _.name == Path.GetFileNameWithoutExtension(s));
                    valueTuple.directory = new DirectoryInfo($"{Path.GetDirectoryName(s)}");
                    return valueTuple;
                }).ToArray();
 
            return existingPackages;
        }
 
 
        private static PSObject[] PsObjects(){
            return new[]{PSObject.AsPSObject(new{Name = "Xpand.XAF.Modules.ModelViewInheritance", Version = "1.0.8"})};
        }
    }
 
    static class Updater{
        public static IObservable<string> Update(this IParameter parameter){
             
 
            return Observable.Create<string>(observer => {
                var appClient = new GitHubClient(new ProductHeaderValue(parameter.Organization)){
                    Credentials = new Credentials(parameter.Owner, parameter.Pass)
                };
 
                var commits = (appClient.Repository.GetForOrg(parameter.Organization, parameter.Repository)
                        .Select(repository => appClient.Repository
                            .LastTag(repository)
                            .Select(tag => appClient.Repository.Commit.Get(repository.Id, tag.Commit.Sha))).Concat()
                        .Concat()
                        .Select(tag => tag.Commit.Committer.Date.AddSeconds(1)))
                    .Do(datetime => observer.OnNext($"lastTaggedDate={datetime}"))
                    .SelectMany(lastTaggedDate => appClient.Commits(parameter.Organization, parameter.Repository,
                        lastTaggedDate, parameter.Branch)).Publish().RefCount();
 
                var changedPackages = ExistingPackages(parameter).ToObservable()
                    .SelectMany(tuple => commits.Where(commit => commit.Files.Any(file => file.Filename.Contains(tuple.directory.Name))).Select(_=>tuple)).Distinct()
                    .Publish().RefCount();
 
                return changedPackages.SelectMany(tuple => appClient.Repository
                        .GetForOrg(parameter.Organization, parameter.Repository)
                        .SelectMany(_ => CreateTagReference(parameter, appClient, _, tuple,observer))
                        .Select(tag => tuple))
                    .Select(UpdateAssemblyInfo)
                    .Subscribe(observer);
            });
        }
 
        private static string UpdateAssemblyInfo((string name, string version, DirectoryInfo directory) info){
            var newVersion = GetVersion(info);
            var directoryName = info.directory.FullName;
            var path = $@"{directoryName}\Properties\AssemblyInfo.cs";
            var text = File.ReadAllText(path);
            text = Regex.Replace(text, @"Version\(""([^""]*)", $"Version(\"{newVersion}");
            File.WriteAllText(path, text);
            return $"{info.name} version raised from {info.version} to {newVersion} ";
        }
 
        private static Version GetVersion((string name, string version, DirectoryInfo directory) info){
            var directoryName = info.directory.FullName;
            var version = new Version(info.version);
            var path = $@"{directoryName}\Properties\AssemblyInfo.cs";
            var text = File.ReadAllText(path);
            var regex = new Regex(@"Version\(""([^""]*)""");
            var newVersion = new Version(version.Major, version.Minor, version.Build, version.Revision + 1);
            var fileVersion = new Version(regex.Match(text).Groups[1].Value);
            if (fileVersion.Build != version.Build){
                newVersion = new Version(fileVersion.Major, fileVersion.Minor, fileVersion.Build, fileVersion.Revision );
            }
 
            return newVersion;
        }
 
        private static (string name, string version, DirectoryInfo directory)[] ExistingPackages(IParameter parameter){
            var packageArgs = parameter.Packages.Select(_ => (name: $"{_.Properties["Name"].Value}", version: $"{_.Properties["Version"].Value}", directory: (DirectoryInfo) null)).ToArray();
 
            var existingPackages = Directory.GetFiles(parameter.SourcePath, "*.csproj", SearchOption.AllDirectories)
                .Where(s => packageArgs.Select(_ => _.name).Any(s.Contains)).ToArray()
                .Select(s => {
                    var valueTuple = packageArgs.First(_ => _.name == Path.GetFileNameWithoutExtension(s));
                    valueTuple.directory = new DirectoryInfo($"{Path.GetDirectoryName(s)}");
                    return valueTuple;
                }).ToArray();
 
            return existingPackages;
        }
 
        private static IObservable<Reference> CreateTagReference(IParameter parameter, GitHubClient appClient,
            Repository repository, (string name, string version, DirectoryInfo directory) tuple,
            IObserver<string> observer){
            observer.OnNext($"Lookup {tuple.name} heads");
            return appClient.Git.Reference.Get(repository.Id, $"heads/{parameter.Branch}")
                .ToObservable().SelectMany(reference => {
                    var tag = $"{tuple.directory.Name}_{GetVersion(tuple)}";
                    observer.OnNext($"Tagging {repository.Name} with {tag}");
                    return appClient.Git.Reference.Create(repository.Id,new NewReference($@"refs/tags/{tag}",reference.Object.Sha))
                        .ToObservable().Catch<Reference, ApiValidationException>(ex =>
                            ex.ApiError.Message=="Reference already exists"? Observable.Return<Reference>(null): Observable.Throw<Reference>(ex));
 
                });
        }
 
    }
    public interface IParameter{
        PSObject[] Packages{ get; set; }
        string GitHubApp{ get; set; }
        string Owner{ get; set; }
        string Organization{ get; set; }
        string Repository{ get; set; }
        string Branch{ get; set; }
        string Pass{ get; set; }
        string SourcePath{ get; set; }
    }
 
}