diff --git a/src/GitVersion.Cli/Commands/CalculateCommand.cs b/src/GitVersion.Cli/Commands/CalculateCommand.cs new file mode 100644 index 0000000000..aa38b6502f --- /dev/null +++ b/src/GitVersion.Cli/Commands/CalculateCommand.cs @@ -0,0 +1,96 @@ +using GitVersion.Logging; +using LibGit2Sharp; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.Threading.Tasks; + +namespace GitVersion.Cli +{ + public class CalculateCommand : Command + { + private readonly Logging.IConsole console; + private readonly IGitVersionCalculator gitversionCalculator; + private readonly CommandWrapper executor; + + public CalculateCommand(Logging.IConsole console, IGitVersionCalculator gitversionCalculator, CommandWrapper executor) : base("calculate", "Calculates version information from your git repository") + { + this.console = console; + this.gitversionCalculator = gitversionCalculator; + this.executor = executor; + AddOptions(); + + this.Handler = CommandHandler.Create(ExecuteAsync); + } + + private void AddOptions() + { + this.AddOption(new Option( + "--normalize", + "Attempt to mutate your git repository so gitversion has enough information (local branches, commit history etc) to calculate.")); + this.AddOption(new Option( + "--dot-git-dir", + "The path of the .git folder. If not specified, will be discovered from the envrionment current working directory.")); + this.AddOption(new Option( + "--no-cache", + "Whether to disable caching of the version variables. If true is specified, any previous cached calculation will be ignored and the new calculation won't be cached either.")); + + this.AddOption(new Option( + "--branch", + "The target branch to calculate a version against.")); + + } + + private async Task ExecuteAsync(GlobalOptions globalOptions, CalculateOptions options) + { + // The executor wraps execution of the command logic inside somethng that + // will do error handling according to the old behaviour. + return await executor.Execute(globalOptions, async () => + { + // if .git directory not specified, discover it from current environment working directory. + if (string.IsNullOrWhiteSpace(options.DotGitDir)) + { + options.DotGitDir = Repository.Discover(globalOptions.WorkingDirectory); + } + + if (options.Normalize ?? false) + { + Normalize(); + } + + //TODO: support the config override param, and also inside the CalculateVersionVariables it does a prepare - we should be able + // to pass args to control that as well. i.e using the above (options.Normalize) arg. + var variables = this.gitversionCalculator.CalculateVersionVariables(options.NoCache, null); + console.WriteLine(variables.ToString()); + return 0; + }); + } + + private void Normalize() + { + executor.Log.Info("TODO: Implement --normalize"); + // throw new NotImplementedException(); + } + } + + public class CalculateOptions + { + public CalculateOptions() + { + + } + public CalculateOptions(bool? normalize) + { + //GlobalOptions = globalOptions; + Normalize = normalize; + } + public bool? Normalize { get; } + public bool? NoCache { get; } + /// + /// The path of the .git folder. If not specified, will be discovered from the envrionment current working directory. + /// + public string DotGitDir { get; set; } + + public string Branch { get; set; } + } + +} diff --git a/src/GitVersion.Cli/Commands/LogToArgument.cs b/src/GitVersion.Cli/Commands/LogToArgument.cs new file mode 100644 index 0000000000..3f0e523bcd --- /dev/null +++ b/src/GitVersion.Cli/Commands/LogToArgument.cs @@ -0,0 +1,39 @@ +using System; +using System.CommandLine; +using System.Linq; + +namespace GitVersion.Cli +{ + /// + /// This is a custom argument that parses `--log-to Console` and `--log-to File "c:/foo.txt" + /// + public class LogToArgument : Argument + { + public LogToArgument() : base(result => + { + var options = new LoggingOptions(); + var logTargetToken = result.Tokens.First(); + + options.LogTo = Enum.Parse(logTargetToken.Value); + if (options.LogTo == LogToTarget.File) + { + var logFilePathToken = result.Tokens[1]; + options.LogFilePath = logFilePathToken.Value; + } + else + { + // only --log-to File can have an additional symbol for the file path. + if (result.Tokens.Count > 1) + { + throw new ArgumentException(); + } + } + + return options; + }) + { + Arity = new ArgumentArity(1, 2); + } + } + +} diff --git a/src/GitVersion.Cli/Commands/RootCommand.cs b/src/GitVersion.Cli/Commands/RootCommand.cs new file mode 100644 index 0000000000..a3d2a0eebf --- /dev/null +++ b/src/GitVersion.Cli/Commands/RootCommand.cs @@ -0,0 +1,20 @@ +using System.CommandLine; + +namespace GitVersion.Cli +{ + + public class RootCommand : System.CommandLine.RootCommand + { + public RootCommand(CalculateCommand calculateCommand) : base("Versioning for your git repository, solved!") + { + AddGlobalOptions(); + this.AddCommand(calculateCommand); + } + + private void AddGlobalOptions() + { + var option = new Option("--log-to") { Argument = new LogToArgument() }; + this.AddGlobalOption(option); + } + } +} diff --git a/src/GitVersion.Cli/GitVersion.Cli.csproj b/src/GitVersion.Cli/GitVersion.Cli.csproj new file mode 100644 index 0000000000..33ea01f01a --- /dev/null +++ b/src/GitVersion.Cli/GitVersion.Cli.csproj @@ -0,0 +1,18 @@ + + + + Exe + netcoreapp3.1 + AnyCPU + + + + + + + + + + + + diff --git a/src/GitVersion.Cli/GlobalOptions/GlobalOptions.cs b/src/GitVersion.Cli/GlobalOptions/GlobalOptions.cs new file mode 100644 index 0000000000..da244790a3 --- /dev/null +++ b/src/GitVersion.Cli/GlobalOptions/GlobalOptions.cs @@ -0,0 +1,19 @@ +using System; + +namespace GitVersion.Cli +{ + + public class GlobalOptions + { + + public GlobalOptions(LoggingOptions logTo = null) + { + LogTo = logTo; + } + + public string WorkingDirectory { get; set; } = System.Environment.CurrentDirectory; + + public LoggingOptions LogTo { get; set; } + } + +} diff --git a/src/GitVersion.Cli/GlobalOptions/LogToTarget.cs b/src/GitVersion.Cli/GlobalOptions/LogToTarget.cs new file mode 100644 index 0000000000..0ccfbf7bca --- /dev/null +++ b/src/GitVersion.Cli/GlobalOptions/LogToTarget.cs @@ -0,0 +1,9 @@ +namespace GitVersion.Cli +{ + public enum LogToTarget + { + Console = 1, + File = 2, + } + +} diff --git a/src/GitVersion.Cli/GlobalOptions/LoggingOptions.cs b/src/GitVersion.Cli/GlobalOptions/LoggingOptions.cs new file mode 100644 index 0000000000..fe499dd7c9 --- /dev/null +++ b/src/GitVersion.Cli/GlobalOptions/LoggingOptions.cs @@ -0,0 +1,10 @@ +namespace GitVersion.Cli +{ + public class LoggingOptions + { + public LogToTarget LogTo { get; set; } + + public string LogFilePath { get; set; } + } + +} diff --git a/src/GitVersion.Cli/HostedService.cs b/src/GitVersion.Cli/HostedService.cs new file mode 100644 index 0000000000..af6b9001ed --- /dev/null +++ b/src/GitVersion.Cli/HostedService.cs @@ -0,0 +1,48 @@ +using GitVersion.Logging; +using Microsoft.Extensions.Hosting; +using System; +using System.CommandLine; +using System.Threading; +using System.Threading.Tasks; + +namespace GitVersion.Cli +{ + internal class HostedService : IHostedService + { + private readonly IHostApplicationLifetime applicationLifetime; + private readonly RootCommand command; + // private readonly IGitVersionExecutor gitVersionExecutor; + private readonly ILog log; + private readonly string[] arguments; + + public HostedService(IHostApplicationLifetime applicationLifetime, RootCommand command, ILog log, string[] arguments) + { + + this.applicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime)); + this.command = command; + // this.gitVersionExecutor = gitVersionExecutor ?? throw new ArgumentNullException(nameof(gitVersionExecutor)); + this.log = log ?? throw new ArgumentNullException(nameof(log)); + this.arguments = arguments; + } + public async Task StartAsync(CancellationToken cancellationToken) + { + try + { + System.Environment.ExitCode = await command.InvokeAsync(arguments); + // log.Verbosity = gitVersionOptions.Verbosity; // not sure how to get verbosity now. + } + catch (Exception exception) + { + Console.Error.WriteLine(exception.Message); + System.Environment.ExitCode = 1; + } + + applicationLifetime.StopApplication(); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } +} diff --git a/src/GitVersion.Cli/Internal/CommandWrapper.cs b/src/GitVersion.Cli/Internal/CommandWrapper.cs new file mode 100644 index 0000000000..ff1fbd8c4f --- /dev/null +++ b/src/GitVersion.Cli/Internal/CommandWrapper.cs @@ -0,0 +1,77 @@ +using GitVersion.Extensions; +using GitVersion.Logging; +using System; +using System.Threading.Tasks; + +namespace GitVersion.Cli +{ + /// + /// The purpose of this class is to wrap execution of each subcommand's logic, + /// to have a centralised location to handle exceptions, and log those exceptions, + /// based on the global option arguments used to control log output. + /// + /// This is evolving in a similar way to but for executing the new commands, and using the GlobalOptions for the new cli instead. + /// Allowing to serve as a reference until the port is finished. + /// + public class CommandWrapper + { + public CommandWrapper(ILog log) + { + Log = log; + } + + public async Task Execute(GlobalOptions globalOptions, Func> execute) + { + var loggingOptions = globalOptions.LogTo; + + if (loggingOptions.LogTo == LogToTarget.Console) + { + Log.AddLogAppender(new ConsoleAppender()); + } + else if (loggingOptions.LogTo == LogToTarget.File && loggingOptions.LogFilePath != "console") + { + Log.AddLogAppender(new FileAppender(loggingOptions.LogFilePath)); + } + + try + { + //gitVersionTool.OutputVariables(variables); + //gitVersionTool.UpdateAssemblyInfo(variables); + //gitVersionTool.UpdateWixVersionFile(variables); + var result = await execute.Invoke(); + return result; + } + catch (WarningException exception) + { + var error = $"An error occurred:{System.Environment.NewLine}{exception.Message}"; + Log.Warning(error); + return 1; + } + catch (Exception exception) + { + var error = $"An unexpected error occurred:{System.Environment.NewLine}{exception}"; + Log.Error(error); + + Log.Info("Attempting to show the current git graph (please include in issue): "); + Log.Info("Showing max of 100 commits"); + + try + { + // I am not sure here if we should be passing in working directory, or the dot git directory? + // current behaviour was to pass in working directory (environment current directory) so sticking with that. + LibGitExtensions.DumpGraph(globalOptions.WorkingDirectory, mess => Log.Info(mess), 100); + } + catch (Exception dumpGraphException) + { + Log.Error("Couldn't dump the git graph due to the following error: " + dumpGraphException); + } + return 1; + } + + + } + + public ILog Log { get; set; } + } + +} diff --git a/src/GitVersion.Cli/Internal/GitPreparer.cs b/src/GitVersion.Cli/Internal/GitPreparer.cs new file mode 100644 index 0000000000..8425731e30 --- /dev/null +++ b/src/GitVersion.Cli/Internal/GitPreparer.cs @@ -0,0 +1,310 @@ +using GitVersion.Extensions; +using GitVersion.Logging; +using LibGit2Sharp; +using System; +using System.Linq; + +namespace GitVersion.Cli +{ + + public class GitPreparer : IGitPreparer + { + private readonly ILog log; + private readonly IEnvironment environment; + private readonly GlobalOptions options; + private readonly CalculateOptions calculateOptions; + private readonly ICurrentBuildAgent buildAgent; + + private const string DefaultRemoteName = "origin"; + + public GitPreparer( + ILog log, + IEnvironment environment, + ICurrentBuildAgent buildAgent, + GlobalOptions options, + CalculateOptions calculateOptions) + { + this.log = log ?? throw new ArgumentNullException(nameof(log)); + this.environment = environment ?? throw new ArgumentNullException(nameof(environment)); + this.options = options ?? throw new ArgumentNullException(nameof(options)); + this.calculateOptions = calculateOptions; + this.buildAgent = buildAgent; + } + + public void Prepare() + { + var gitVersionOptions = options; + + // Normalize if we are running on build server + var normalizeGitDirectory = (calculateOptions.Normalize ?? false) && buildAgent != null; + var shouldCleanUpRemotes = buildAgent != null && buildAgent.ShouldCleanUpRemotes(); + + var targetBranchArg = calculateOptions.Branch; + var currentBranch = ResolveCurrentBranch(targetBranchArg); + + // This isn't nice. Perhaps we should pass in as param to this method, but that involves interface changes. + var repoDirectoryInfo = GitRepositoryDirectoryInfo.Get(calculateOptions.DotGitDir, options.WorkingDirectory); + + var dotGitDirectory = repoDirectoryInfo.DotGitDirectory; + log.Info($"DotGit directory is: {dotGitDirectory}"); + + var projectRoot = repoDirectoryInfo.WorkingDirectory; + log.Info($"Git repository working directory is: {projectRoot}"); + + if (string.IsNullOrEmpty(dotGitDirectory) || string.IsNullOrEmpty(projectRoot)) + { + throw new Exception($"Failed to prepare or find the .git directory in path '{gitVersionOptions.WorkingDirectory}'."); + } + + PrepareInternal(dotGitDirectory, normalizeGitDirectory, currentBranch, shouldCleanUpRemotes); + } + + private void PrepareInternal(string dotGitDirectory, bool normalizeGitDirectory, string currentBranch, bool shouldCleanUpRemotes = false) + { + + if (normalizeGitDirectory) + { + if (shouldCleanUpRemotes) + { + CleanupDuplicateOrigin(dotGitDirectory); + } + + NormalizeGitDirectory(currentBranch, dotGitDirectory, false); + } + + } + + /// + /// Uses build environment to work out which branch is being targeted for version calculation, but falls back to using the branch arg passed via commadn line... + /// + /// + /// + private string ResolveCurrentBranch(string targetBranch) + { + + if (buildAgent == null) + { + return targetBranch; + } + + var currentBranch = buildAgent.GetCurrentBranch(false) ?? targetBranch; + log.Info("Branch from build environment: " + currentBranch); + + return currentBranch; + } + + private void CleanupDuplicateOrigin(string dotGitDirectory) + { + var remoteToKeep = DefaultRemoteName; + + using var repo = new Repository(dotGitDirectory); + + // check that we have a remote that matches defaultRemoteName if not take the first remote + if (!repo.Network.Remotes.Any(remote => remote.Name.Equals(DefaultRemoteName, StringComparison.InvariantCultureIgnoreCase))) + { + remoteToKeep = repo.Network.Remotes.First().Name; + } + + var duplicateRepos = repo.Network + .Remotes + .Where(remote => !remote.Name.Equals(remoteToKeep, StringComparison.InvariantCultureIgnoreCase)) + .Select(remote => remote.Name); + + // remove all remotes that are considered duplicates + foreach (var repoName in duplicateRepos) + { + repo.Network.Remotes.Remove(repoName); + } + } + + private void NormalizeGitDirectory(string targetBranch, string gitDirectory, bool noFetch) + { + using (log.IndentLog($"Normalizing git directory for branch '{targetBranch}'")) + { + // Normalize (download branches) before using the branch + NormalizeGitDirectory(gitDirectory, noFetch, targetBranch); + } + } + + private void CloneRepository(string repositoryUrl, string gitDirectory, AuthenticationInfo auth) + { + Credentials credentials = null; + + if (auth != null) + { + if (!string.IsNullOrWhiteSpace(auth.Username)) + { + log.Info($"Setting up credentials using name '{auth.Username}'"); + + credentials = new UsernamePasswordCredentials + { + Username = auth.Username, + Password = auth.Password ?? string.Empty + }; + } + } + + try + { + using (log.IndentLog($"Cloning repository from url '{repositoryUrl}'")) + { + var cloneOptions = new CloneOptions + { + Checkout = false, + CredentialsProvider = (url, usernameFromUrl, types) => credentials + }; + + var returnedPath = Repository.Clone(repositoryUrl, gitDirectory, cloneOptions); + log.Info($"Returned path after repository clone: {returnedPath}"); + } + } + catch (LibGit2SharpException ex) + { + var message = ex.Message; + if (message.Contains("401")) + { + throw new Exception("Unauthorized: Incorrect username/password"); + } + if (message.Contains("403")) + { + throw new Exception("Forbidden: Possibly Incorrect username/password"); + } + if (message.Contains("404")) + { + throw new Exception("Not found: The repository was not found"); + } + + throw new Exception("There was an unknown problem with the Git repository you provided", ex); + } + } + + /// + /// Normalization of a git directory turns all remote branches into local branches, turns pull request refs into a real branch and a few other things. This is designed to be run *only on the build server* which checks out repositories in different ways. + /// It is not recommended to run normalization against a local repository + /// + private void NormalizeGitDirectory(string gitDirectory, bool noFetch, string currentBranch) + { + throw new NotImplementedException(); + +// var authentication = options.Value.Authentication; +// using var repository = new Repository(gitDirectory); +// // Need to ensure the HEAD does not move, this is essentially a BugCheck +// var expectedSha = repository.Head.Tip.Sha; +// var expectedBranchName = repository.Head.CanonicalName; + +// try +// { +// var remote = repository.EnsureOnlyOneRemoteIsDefined(log); + +// repository.AddMissingRefSpecs(log, remote); + +// //If noFetch is enabled, then GitVersion will assume that the git repository is normalized before execution, so that fetching from remotes is not required. +// if (noFetch) +// { +// log.Info("Skipping fetching, if GitVersion does not calculate your version as expected you might need to allow fetching."); +// } +// else +// { +// log.Info($"Fetching from remote '{remote.Name}' using the following refspecs: {string.Join(", ", remote.FetchRefSpecs.Select(r => r.Specification))}."); +// Commands.Fetch(repository, remote.Name, new string[0], authentication.ToFetchOptions(), null); +// } + +// repository.EnsureLocalBranchExistsForCurrentBranch(log, remote, currentBranch); +// repository.CreateOrUpdateLocalBranchesFromRemoteTrackingOnes(log, remote.Name); + +// // Bug fix for https://github.com/GitTools/GitVersion/issues/1754, head maybe have been changed +// // if this is a dynamic repository. But only allow this in case the branches are different (branch switch) +// if (expectedSha != repository.Head.Tip.Sha && +// (isDynamicRepository || !expectedBranchName.IsBranch(currentBranch))) +// { +// var newExpectedSha = repository.Head.Tip.Sha; +// var newExpectedBranchName = repository.Head.CanonicalName; + +// log.Info($"Head has moved from '{expectedBranchName} | {expectedSha}' => '{newExpectedBranchName} | {newExpectedSha}', allowed since this is a dynamic repository"); + +// expectedSha = newExpectedSha; +// } + +// var headSha = repository.Refs.Head.TargetIdentifier; + +// if (!repository.Info.IsHeadDetached) +// { +// log.Info($"HEAD points at branch '{headSha}'."); +// return; +// } + +// log.Info($"HEAD is detached and points at commit '{headSha}'."); +// log.Info($"Local Refs:{System.Environment.NewLine}" + string.Join(System.Environment.NewLine, repository.Refs.FromGlob("*").Select(r => $"{r.CanonicalName} ({r.TargetIdentifier})"))); + +// // In order to decide whether a fake branch is required or not, first check to see if any local branches have the same commit SHA of the head SHA. +// // If they do, go ahead and checkout that branch +// // If no, go ahead and check out a new branch, using the known commit SHA as the pointer +// var localBranchesWhereCommitShaIsHead = repository.Branches.Where(b => !b.IsRemote && b.Tip.Sha == headSha).ToList(); + +// var matchingCurrentBranch = !string.IsNullOrEmpty(currentBranch) +// ? localBranchesWhereCommitShaIsHead.SingleOrDefault(b => b.CanonicalName.Replace("/heads/", "/") == currentBranch.Replace("/heads/", "/")) +// : null; +// if (matchingCurrentBranch != null) +// { +// log.Info($"Checking out local branch '{currentBranch}'."); +// Commands.Checkout(repository, matchingCurrentBranch); +// } +// else if (localBranchesWhereCommitShaIsHead.Count > 1) +// { +// var branchNames = localBranchesWhereCommitShaIsHead.Select(r => r.CanonicalName); +// var csvNames = string.Join(", ", branchNames); +// const string moveBranchMsg = "Move one of the branches along a commit to remove warning"; + +// log.Warning($"Found more than one local branch pointing at the commit '{headSha}' ({csvNames})."); +// var master = localBranchesWhereCommitShaIsHead.SingleOrDefault(n => n.FriendlyName == "master"); +// if (master != null) +// { +// log.Warning("Because one of the branches is 'master', will build master." + moveBranchMsg); +// Commands.Checkout(repository, master); +// } +// else +// { +// var branchesWithoutSeparators = localBranchesWhereCommitShaIsHead.Where(b => !b.FriendlyName.Contains('/') && !b.FriendlyName.Contains('-')).ToList(); +// if (branchesWithoutSeparators.Count == 1) +// { +// var branchWithoutSeparator = branchesWithoutSeparators[0]; +// log.Warning($"Choosing {branchWithoutSeparator.CanonicalName} as it is the only branch without / or - in it. " + moveBranchMsg); +// Commands.Checkout(repository, branchWithoutSeparator); +// } +// else +// { +// throw new WarningException("Failed to try and guess branch to use. " + moveBranchMsg); +// } +// } +// } +// else if (localBranchesWhereCommitShaIsHead.Count == 0) +// { +// log.Info($"No local branch pointing at the commit '{headSha}'. Fake branch needs to be created."); +// repository.CreateFakeBranchPointingAtThePullRequestTip(log, authentication); +// } +// else +// { +// log.Info($"Checking out local branch 'refs/heads/{localBranchesWhereCommitShaIsHead[0].FriendlyName}'."); +// Commands.Checkout(repository, repository.Branches[localBranchesWhereCommitShaIsHead[0].FriendlyName]); +// } +// } +// finally +// { +// if (repository.Head.Tip.Sha != expectedSha) +// { +// if (environment.GetEnvironmentVariable("IGNORE_NORMALISATION_GIT_HEAD_MOVE") != "1") +// { +// // Whoa, HEAD has moved, it shouldn't have. We need to blow up because there is a bug in normalisation +// throw new BugException($@"GitVersion has a bug, your HEAD has moved after repo normalisation. + +//To disable this error set an environmental variable called IGNORE_NORMALISATION_GIT_HEAD_MOVE to 1 + +//Please run `git {LibGitExtensions.CreateGitLogArgs(100)}` and submit it along with your build log (with personal info removed) in a new issue at https://github.com/GitTools/GitVersion"); +// } +// } +// } + } + } + + +} diff --git a/src/GitVersion.Cli/Internal/GitRepositoryDirectoryInfo.cs b/src/GitVersion.Cli/Internal/GitRepositoryDirectoryInfo.cs new file mode 100644 index 0000000000..efdedb7372 --- /dev/null +++ b/src/GitVersion.Cli/Internal/GitRepositoryDirectoryInfo.cs @@ -0,0 +1,52 @@ +using LibGit2Sharp; +using System; +using System.IO; + +namespace GitVersion.Cli +{ + public class GitRepositoryDirectoryInfo + { + private readonly Func getRepoWorkingDirectory; + private Lazy workingDirectory; + + public GitRepositoryDirectoryInfo(string dotGitDirectory, Func getRepoWorkingDirectory) + { + DotGitDirectory = dotGitDirectory; + this.getRepoWorkingDirectory = getRepoWorkingDirectory; + workingDirectory = new Lazy(() => + { + return getRepoWorkingDirectory(); + }); + } + + public string DotGitDirectory { get; } + + public string WorkingDirectory { get { return workingDirectory.Value; } } + + public static GitRepositoryDirectoryInfo Get(string dotGitDirectory, string environmentWorkingDirectory) + { + if (string.IsNullOrEmpty(dotGitDirectory)) + { + dotGitDirectory = Repository.Discover(environmentWorkingDirectory); + } + + dotGitDirectory = dotGitDirectory?.TrimEnd('/', '\\'); + + if (string.IsNullOrEmpty(dotGitDirectory)) + throw new DirectoryNotFoundException($"Can't find the .git directory in {dotGitDirectory}"); + + dotGitDirectory = dotGitDirectory.Contains(Path.Combine(".git", "worktrees")) + ? Directory.GetParent(Directory.GetParent(dotGitDirectory).FullName).FullName + : dotGitDirectory; + + using var repository = new Repository(dotGitDirectory); + return new GitRepositoryDirectoryInfo(dotGitDirectory, () => { + using var repository = new Repository(dotGitDirectory); + return repository.Info.WorkingDirectory; + }); + } + + } + + +} diff --git a/src/GitVersion.Cli/Internal/GitVersionCalculator.cs b/src/GitVersion.Cli/Internal/GitVersionCalculator.cs new file mode 100644 index 0000000000..57d6242b7c --- /dev/null +++ b/src/GitVersion.Cli/Internal/GitVersionCalculator.cs @@ -0,0 +1,81 @@ +using GitVersion.Logging; +using GitVersion.Model.Configuration; +using GitVersion.OutputVariables; +using GitVersion.VersionCalculation; +using GitVersion.VersionCalculation.Cache; +using GitVersion.VersionConverters.AssemblyInfo; +using GitVersion.VersionConverters.GitVersionInfo; +using GitVersion.VersionConverters.OutputGenerator; +using GitVersion.VersionConverters.WixUpdater; +using System; + +namespace GitVersion.Cli +{ + /// + /// A new implementation of IGitVersionCalculator that doesn't depend on IOptions + /// + public class GitVersionCalculator : IGitVersionCalculator + { + private readonly ILog log; + private readonly IGitVersionCache gitVersionCache; + private readonly INextVersionCalculator nextVersionCalculator; + private readonly IVariableProvider variableProvider; + // private readonly IGitPreparer gitPreparer; + private readonly IGitVersionCacheKeyFactory cacheKeyFactory; + + private readonly Lazy versionContext; + private GitVersionContext context => versionContext.Value; + + public GitVersionCalculator(ILog log, INextVersionCalculator nextVersionCalculator, IVariableProvider variableProvider, + // IGitPreparer gitPreparer, + IGitVersionCache gitVersionCache, IGitVersionCacheKeyFactory cacheKeyFactory, + IOutputGenerator outputGenerator, IWixVersionFileUpdater wixVersionFileUpdater, IGitVersionInfoGenerator gitVersionInfoGenerator, IAssemblyInfoFileUpdater assemblyInfoFileUpdater, + Lazy versionContext) + { + this.log = log ?? throw new ArgumentNullException(nameof(log)); + + this.nextVersionCalculator = nextVersionCalculator ?? throw new ArgumentNullException(nameof(nextVersionCalculator)); + this.variableProvider = variableProvider ?? throw new ArgumentNullException(nameof(variableProvider)); + // this.gitPreparer = gitPreparer ?? throw new ArgumentNullException(nameof(gitPreparer)); + + this.cacheKeyFactory = cacheKeyFactory ?? throw new ArgumentNullException(nameof(cacheKeyFactory)); + this.gitVersionCache = gitVersionCache ?? throw new ArgumentNullException(nameof(gitVersionCache)); + this.versionContext = versionContext ?? throw new ArgumentNullException(nameof(versionContext)); + } + + + public VersionVariables CalculateVersionVariables(bool? noCache, Config overrideConfig = null) + { + + //TODO: Need to figure out whether we run Prepare as a seperate command + //if(prepare) + //{ + // gitPreparer.Prepare(); //we need to prepare the repository before using it for version calculation + //} + + //var gitVersionOptions = options.Value; + + var cacheKey = cacheKeyFactory.Create(overrideConfig); + var versionVariables = noCache ?? false ? default : gitVersionCache.LoadVersionVariablesFromDiskCache(cacheKey); + + if (versionVariables != null) return versionVariables; + + var semanticVersion = nextVersionCalculator.FindVersion(); + versionVariables = variableProvider.GetVariablesFor(semanticVersion, context.Configuration, context.IsCurrentCommitTagged); + + if (noCache ?? false) return versionVariables; + try + { + gitVersionCache.WriteVariablesToDiskCache(cacheKey, versionVariables); + } + catch (AggregateException e) + { + log.Warning($"One or more exceptions during cache write:{System.Environment.NewLine}{e}"); + } + + return versionVariables; + } + + } + +} diff --git a/src/GitVersion.Cli/Program.cs b/src/GitVersion.Cli/Program.cs new file mode 100644 index 0000000000..f40400bcf5 --- /dev/null +++ b/src/GitVersion.Cli/Program.cs @@ -0,0 +1,52 @@ +using GitVersion.Extensions; +using GitVersion.Logging; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.Threading.Tasks; + +namespace GitVersion.Cli +{ + class Program + { + private static async Task Main(string[] args) + { + await new Program().RunAsync(args); + } + + internal Task RunAsync(string[] args) + { + return CreateHostBuilder(args).Build().RunAsync(); + } + + private IHostBuilder CreateHostBuilder(string[] args) => + new HostBuilder() + .ConfigureAppConfiguration((hostContext, configApp) => + { + configApp.AddCommandLine(args); + }) + .ConfigureServices((hostContext, services) => + { + services.AddModule(new GitVersionCoreModule()); + services.AddSingleton(); + + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddHostedService(sp => + { + var lifetime = sp.GetRequiredService(); + var rootCommand = sp.GetRequiredService(); + var log = sp.GetRequiredService(); + var hostedService = new HostedService(lifetime, rootCommand, log, args); + return hostedService; + // return ActivatorUtilities.CreateInstance(sp, args); + }); + }) + .UseConsoleLifetime(); + } + +} diff --git a/src/GitVersion.Cli/Properties/launchSettings.json b/src/GitVersion.Cli/Properties/launchSettings.json new file mode 100644 index 0000000000..5836734797 --- /dev/null +++ b/src/GitVersion.Cli/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "GitVersion.Cli": { + "commandName": "Project", + "commandLineArgs": "calculate --log-to File c:/temp/foo.txt" + } + } +} \ No newline at end of file diff --git a/src/GitVersion.Cli/README.md b/src/GitVersion.Cli/README.md new file mode 100644 index 0000000000..830ccd7247 --- /dev/null +++ b/src/GitVersion.Cli/README.md @@ -0,0 +1,6 @@ +## New CLI + +## Commands + + + diff --git a/src/GitVersion.sln b/src/GitVersion.sln index 13bb303084..34f97e36ea 100644 --- a/src/GitVersion.sln +++ b/src/GitVersion.sln @@ -33,6 +33,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GitVersionTask", "GitVersio EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GitVersionTask.MsBuild", "GitVersionTask.MsBuild\GitVersionTask.MsBuild.csproj", "{0F1AEC4E-E81D-4F84-B2E8-3415A1A4DBF4}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GitVersion.Cli", "GitVersion.Cli\GitVersion.Cli.csproj", "{5BABE11A-CC5D-45C6-8C39-4BBC376F81C0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -67,6 +69,10 @@ Global {0F1AEC4E-E81D-4F84-B2E8-3415A1A4DBF4}.Debug|Any CPU.Build.0 = Debug|Any CPU {0F1AEC4E-E81D-4F84-B2E8-3415A1A4DBF4}.Release|Any CPU.ActiveCfg = Release|Any CPU {0F1AEC4E-E81D-4F84-B2E8-3415A1A4DBF4}.Release|Any CPU.Build.0 = Release|Any CPU + {5BABE11A-CC5D-45C6-8C39-4BBC376F81C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5BABE11A-CC5D-45C6-8C39-4BBC376F81C0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5BABE11A-CC5D-45C6-8C39-4BBC376F81C0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5BABE11A-CC5D-45C6-8C39-4BBC376F81C0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/GitVersionCore/Common/IGitVersionCalculator.cs b/src/GitVersionCore/Common/IGitVersionCalculator.cs new file mode 100644 index 0000000000..fa4aa9416c --- /dev/null +++ b/src/GitVersionCore/Common/IGitVersionCalculator.cs @@ -0,0 +1,11 @@ +using GitVersion.Model.Configuration; +using GitVersion.OutputVariables; +using System; + +namespace GitVersion +{ + public interface IGitVersionCalculator + { + VersionVariables CalculateVersionVariables(bool? noCache, Config overrideConfig = null); + } +} diff --git a/src/GitVersionCore/Common/IGitVersionTool.cs b/src/GitVersionCore/Common/IGitVersionTool.cs index 7dae46a249..796ffdbdd0 100644 --- a/src/GitVersionCore/Common/IGitVersionTool.cs +++ b/src/GitVersionCore/Common/IGitVersionTool.cs @@ -2,9 +2,10 @@ namespace GitVersion { - public interface IGitVersionTool + + + public interface IGitVersionTool : IGitVersionCalculator { - VersionVariables CalculateVersionVariables(); void OutputVariables(VersionVariables variables); void UpdateAssemblyInfo(VersionVariables variables); void UpdateWixVersionFile(VersionVariables variables); diff --git a/src/GitVersionCore/Core/GitVersionTool.cs b/src/GitVersionCore/Core/GitVersionTool.cs index 041f79a520..b41cdd6c90 100644 --- a/src/GitVersionCore/Core/GitVersionTool.cs +++ b/src/GitVersionCore/Core/GitVersionTool.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using GitVersion.Logging; +using GitVersion.Model.Configuration; using GitVersion.OutputVariables; using GitVersion.VersionCalculation; using GitVersion.VersionCalculation.Cache; @@ -52,21 +53,22 @@ public GitVersionTool(ILog log, INextVersionCalculator nextVersionCalculator, IV this.versionContext = versionContext ?? throw new ArgumentNullException(nameof(versionContext)); } - public VersionVariables CalculateVersionVariables() + + public VersionVariables CalculateVersionVariables(bool? noCache, Config overrideConfig = null) { gitPreparer.Prepare(); //we need to prepare the repository before using it for version calculation - var gitVersionOptions = options.Value; + //var gitVersionOptions = options.Value; - var cacheKey = cacheKeyFactory.Create(gitVersionOptions.ConfigInfo.OverrideConfig); - var versionVariables = gitVersionOptions.Settings.NoCache ? default : gitVersionCache.LoadVersionVariablesFromDiskCache(cacheKey); + var cacheKey = cacheKeyFactory.Create(overrideConfig); + var versionVariables = noCache ?? false ? default : gitVersionCache.LoadVersionVariablesFromDiskCache(cacheKey); if (versionVariables != null) return versionVariables; var semanticVersion = nextVersionCalculator.FindVersion(); versionVariables = variableProvider.GetVariablesFor(semanticVersion, context.Configuration, context.IsCurrentCommitTagged); - if (gitVersionOptions.Settings.NoCache) return versionVariables; + if (noCache ?? false) return versionVariables; try { gitVersionCache.WriteVariablesToDiskCache(cacheKey, versionVariables); @@ -79,6 +81,13 @@ public VersionVariables CalculateVersionVariables() return versionVariables; } + [Obsolete("Use version that doesn't rely on GitVersionOptions")] + public VersionVariables CalculateVersionVariables() + { + var gitVersionOptions = options.Value; + return CalculateVersionVariables(gitVersionOptions.Settings.NoCache, gitVersionOptions.ConfigInfo.OverrideConfig); + } + public void OutputVariables(VersionVariables variables) { var gitVersionOptions = options.Value;