From a7284ce4744a5d8d2192d4cfb42136d7d30e02e3 Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Mon, 2 Mar 2026 23:21:52 +0100 Subject: [PATCH 01/24] build(app): add System.CommandLine v2 package dependency --- src/Directory.Packages.props | 1 + src/GitVersion.App/GitVersion.App.csproj | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 6cc2ddc98d..65706db0ac 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -43,6 +43,7 @@ + diff --git a/src/GitVersion.App/GitVersion.App.csproj b/src/GitVersion.App/GitVersion.App.csproj index 7818265ec5..4837272ee9 100644 --- a/src/GitVersion.App/GitVersion.App.csproj +++ b/src/GitVersion.App/GitVersion.App.csproj @@ -20,6 +20,7 @@ + From 442a8f86071e92d7da01eec7e4a274c72611e952 Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Mon, 2 Mar 2026 23:22:11 +0100 Subject: [PATCH 02/24] refactor(app): rename ArgumentParser to LegacyArgumentParser --- .../{ArgumentParser.cs => LegacyArgumentParser.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/GitVersion.App/{ArgumentParser.cs => LegacyArgumentParser.cs} (99%) diff --git a/src/GitVersion.App/ArgumentParser.cs b/src/GitVersion.App/LegacyArgumentParser.cs similarity index 99% rename from src/GitVersion.App/ArgumentParser.cs rename to src/GitVersion.App/LegacyArgumentParser.cs index fd55cf245e..d0a0def645 100644 --- a/src/GitVersion.App/ArgumentParser.cs +++ b/src/GitVersion.App/LegacyArgumentParser.cs @@ -9,7 +9,7 @@ namespace GitVersion; -internal class ArgumentParser( +internal class LegacyArgumentParser( IEnvironment environment, IFileSystem fileSystem, IConsole console, From 8bfb4823bc059644585dc316b2a7497c3615ce96 Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Mon, 2 Mar 2026 23:22:23 +0100 Subject: [PATCH 03/24] feat(app): add POSIX-style argument parser using System.CommandLine --- .../SystemCommandLineArgumentParser.cs | 730 ++++++++++++++++++ 1 file changed, 730 insertions(+) create mode 100644 src/GitVersion.App/SystemCommandLineArgumentParser.cs diff --git a/src/GitVersion.App/SystemCommandLineArgumentParser.cs b/src/GitVersion.App/SystemCommandLineArgumentParser.cs new file mode 100644 index 0000000000..ce83d933f4 --- /dev/null +++ b/src/GitVersion.App/SystemCommandLineArgumentParser.cs @@ -0,0 +1,730 @@ +using System.CommandLine; +using System.IO.Abstractions; +using GitVersion.Extensions; +using GitVersion.FileSystemGlobbing; +using GitVersion.Helpers; +using GitVersion.Logging; +using GitVersion.OutputVariables; +using Serilog.Core; +using Serilog.Events; + +namespace GitVersion; + +internal class SystemCommandLineArgumentParser( + IEnvironment environment, + IFileSystem fileSystem, + IConsole console, + IGlobbingResolver globbingResolver, + LoggingLevelSwitch loggingLevelSwitch +) + : IArgumentParser +{ + private readonly IEnvironment environment = environment.NotNull(); + private readonly IFileSystem fileSystem = fileSystem.NotNull(); + private readonly IConsole console = console.NotNull(); + private readonly IGlobbingResolver globbingResolver = globbingResolver.NotNull(); + private readonly LoggingLevelSwitch loggingLevelSwitch = loggingLevelSwitch.NotNull(); + + private const string defaultOutputFileName = "GitVersion.json"; + private static readonly IEnumerable availableVariables = GitVersionVariables.AvailableVariables; + + private static readonly Dictionary VerbosityMaps = new() + { + { Verbosity.Verbose, LogEventLevel.Verbose }, + { Verbosity.Diagnostic, LogEventLevel.Debug }, + { Verbosity.Normal, LogEventLevel.Information }, + { Verbosity.Minimal, LogEventLevel.Warning }, + { Verbosity.Quiet, LogEventLevel.Error } + }; + + public Arguments ParseArguments(string commandLineArguments) + { + var arguments = QuotedStringHelpers.SplitUnquoted(commandLineArguments, ' '); + return ParseArguments(arguments); + } + + public Arguments ParseArguments(string[] commandLineArguments) + { + if (commandLineArguments.Length == 0) + { + var args = new Arguments + { + TargetPath = SysEnv.CurrentDirectory + }; + args.Output.Add(OutputType.Json); + AddAuthentication(args); + return args; + } + + var (rootCommand, options) = BuildCommand(); + + // Let System.CommandLine handle --help output natively + if (commandLineArguments.Any(a => a is "--help" or "-h" or "-?" or "/?")) + { + PrintBuiltInHelp(rootCommand); + return new Arguments { IsHelp = true }; + } + + // Handle --version before parsing to avoid System.CommandLine interception + if (commandLineArguments.Any(a => a is "--version")) + { + PrintBuiltInVersion(); + return new Arguments { IsVersion = true }; + } + + var parseResult = rootCommand.Parse(commandLineArguments); + + // Check for parse errors + var errors = parseResult.Errors; + if (errors.Count > 0) + { + var firstError = errors[0]; + var message = firstError.Message; + + // Try to extract the unrecognized token for a friendlier message + if (message.Contains("Unrecognized command or argument")) + { + var token = ExtractUnrecognizedToken(message); + throw new WarningException($"Could not parse command line parameter '{token}'."); + } + + throw new WarningException($"Could not parse command line parameter '{message}'."); + } + + // Check for unmatched tokens that System.CommandLine didn't report as errors + var unmatchedTokens = parseResult.UnmatchedTokens; + if (unmatchedTokens.Count > 0) + { + throw new WarningException($"Could not parse command line parameter '{unmatchedTokens[0]}'."); + } + + // Detect unknown options that were incorrectly consumed as positional path argument + var positionalCheck = parseResult.GetValue(options.Path); + if (positionalCheck != null && positionalCheck.StartsWith('-')) + { + throw new WarningException($"Could not parse command line parameter '{positionalCheck}'."); + } + + var arguments = new Arguments(); + AddAuthentication(arguments); + + // Map parsed values to Arguments + MapParsedValues(arguments, parseResult, options); + + // Defaults + if (arguments.Output.Count == 0) + { + arguments.Output.Add(OutputType.Json); + } + + if (arguments.Output.Contains(OutputType.File) && arguments.OutputFile == null) + { + arguments.OutputFile = defaultOutputFileName; + } + + // Target path + var positionalPath = parseResult.GetValue(options.Path); + arguments.TargetPath ??= positionalPath ?? SysEnv.CurrentDirectory; + arguments.TargetPath = arguments.TargetPath.TrimEnd('/', '\\'); + + if (!arguments.EnsureAssemblyInfo) + arguments.UpdateAssemblyInfoFileName = ResolveFiles(arguments.TargetPath, arguments.UpdateAssemblyInfoFileName).ToHashSet(); + + ValidateConfigurationFile(arguments); + + return arguments; + } + + private void PrintBuiltInHelp(RootCommand rootCommand) + { + rootCommand.SetAction((_, _) => Task.FromResult(0)); + rootCommand.Parse(["--help"]).InvokeAsync().GetAwaiter().GetResult(); + } + + private void PrintBuiltInVersion() + { + var assembly = Assembly.GetExecutingAssembly(); + var version = assembly + .GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false) + .FirstOrDefault() is AssemblyInformationalVersionAttribute attr + ? attr.InformationalVersion + : assembly.GetName().Version?.ToString(); + if (version != null) + this.console.WriteLine(version); + } + + private static string ExtractUnrecognizedToken(string message) + { + // System.CommandLine error format: "Unrecognized command or argument 'xxx'." + var start = message.IndexOf('\''); + var end = message.LastIndexOf('\''); + if (start >= 0 && end > start) + { + return message[(start + 1)..end]; + } + return message; + } + + private void MapParsedValues(Arguments arguments, ParseResult parseResult, CommandOptions options) + { + // Log file + var logFile = parseResult.GetValue(options.LogFile); + if (logFile != null) + arguments.LogFilePath = logFile; + + // Diagnose + if (parseResult.GetValue(options.Diagnose)) + arguments.Diag = true; + + // Output + var outputs = parseResult.GetValue(options.Output); + if (outputs != null) + { + foreach (var output in outputs) + { + arguments.Output.Add(output); + } + } + + // Output file + var outputFile = parseResult.GetValue(options.OutputFile); + if (outputFile != null) + arguments.OutputFile = outputFile; + + // Show variable + var showVariable = parseResult.GetValue(options.ShowVariable); + if (showVariable != null) + ParseShowVariable(arguments, showVariable); + + // Format + var format = parseResult.GetValue(options.Format); + if (format != null) + ParseFormat(arguments, format); + + // Config + var config = parseResult.GetValue(options.Config); + if (config != null) + arguments.ConfigurationFile = config; + + // Show config + if (parseResult.GetValue(options.ShowConfig)) + arguments.ShowConfiguration = true; + + // Override config + var overrideConfigs = parseResult.GetValue(options.OverrideConfig); + if (overrideConfigs is { Length: > 0 }) + ParseOverrideConfig(arguments, overrideConfigs); + + // Target path (explicit option) + var targetPath = parseResult.GetValue(options.TargetPath); + if (targetPath != null) + { + arguments.TargetPath = targetPath; + if (string.IsNullOrWhiteSpace(targetPath) || !this.fileSystem.Directory.Exists(targetPath)) + { + this.console.WriteLine($"The working directory '{targetPath}' does not exist."); + } + } + + // No-fetch + if (parseResult.GetValue(options.NoFetch)) + arguments.NoFetch = true; + + // No-cache + if (parseResult.GetValue(options.NoCache)) + arguments.NoCache = true; + + // No-normalize + if (parseResult.GetValue(options.NoNormalize)) + arguments.NoNormalize = true; + + // Allow shallow + if (parseResult.GetValue(options.AllowShallow)) + arguments.AllowShallow = true; + + // Verbosity + var verbosity = parseResult.GetValue(options.VerbosityOption); + if (verbosity != null) + { + var parsedVerbosity = ParseVerbosity(verbosity); + this.loggingLevelSwitch.MinimumLevel = VerbosityMaps[parsedVerbosity]; + } + + // Update assembly info + var updateAssemblyInfoResult = parseResult.GetResult(options.UpdateAssemblyInfo); + if (updateAssemblyInfoResult is { Implicit: false }) + { + var updateAssemblyInfo = parseResult.GetValue(options.UpdateAssemblyInfo); + + // Check if the option was explicitly disabled with "false" or "0" + if (updateAssemblyInfo is { Length: 1 } && + (updateAssemblyInfo[0].Equals("false", StringComparison.OrdinalIgnoreCase) || + updateAssemblyInfo[0].Equals("0", StringComparison.Ordinal))) + { + arguments.UpdateAssemblyInfo = false; + } + else + { + arguments.UpdateAssemblyInfo = true; + if (updateAssemblyInfo != null) + { + foreach (var file in updateAssemblyInfo) + { + if (!file.Equals("true", StringComparison.OrdinalIgnoreCase) && !file.Equals("1", StringComparison.OrdinalIgnoreCase)) + { + arguments.UpdateAssemblyInfoFileName.Add(file); + } + } + } + } + + if (arguments.UpdateProjectFiles) + { + throw new WarningException("Cannot specify both updateprojectfiles and updateassemblyinfo in the same run. Please rerun GitVersion with only one parameter"); + } + } + + // Update project files + var updateProjectFilesResult = parseResult.GetResult(options.UpdateProjectFiles); + if (updateProjectFilesResult is { Implicit: false }) + { + arguments.UpdateProjectFiles = true; + var updateProjectFiles = parseResult.GetValue(options.UpdateProjectFiles); + if (updateProjectFiles != null) + { + foreach (var file in updateProjectFiles) + { + if (!file.Equals("true", StringComparison.OrdinalIgnoreCase) && !file.Equals("1", StringComparison.OrdinalIgnoreCase)) + { + arguments.UpdateAssemblyInfoFileName.Add(file); + } + } + } + + if (arguments.UpdateAssemblyInfo) + { + throw new WarningException("Cannot specify both updateassemblyinfo and updateprojectfiles in the same run. Please rerun GitVersion with only one parameter"); + } + + if (arguments.EnsureAssemblyInfo) + { + throw new WarningException("Cannot specify -ensureassemblyinfo with updateprojectfiles: please ensure your project file exists before attempting to update it"); + } + } + + // Ensure assembly info + if (parseResult.GetValue(options.EnsureAssemblyInfo)) + { + arguments.EnsureAssemblyInfo = true; + + if (arguments.UpdateProjectFiles) + { + throw new WarningException("Cannot specify -ensureassemblyinfo with updateprojectfiles: please ensure your project file exists before attempting to update it"); + } + + if (arguments.UpdateAssemblyInfoFileName.Count > 1 && arguments.EnsureAssemblyInfo) + { + throw new WarningException("Can't specify multiple assembly info files when using /ensureassemblyinfo switch, either use a single assembly info file or do not specify /ensureassemblyinfo and create assembly info files manually"); + } + } + + // Check assembly info + ensure assembly info cross-validation + if (arguments.UpdateAssemblyInfoFileName.Count > 1 && arguments.EnsureAssemblyInfo) + { + throw new WarningException("Can't specify multiple assembly info files when using /ensureassemblyinfo switch, either use a single assembly info file or do not specify /ensureassemblyinfo and create assembly info files manually"); + } + + // Update wix version file + if (parseResult.GetValue(options.UpdateWixVersionFile)) + arguments.UpdateWixVersionFile = true; + + // Remote repository args + var url = parseResult.GetValue(options.Url); + if (url != null) + arguments.TargetUrl = url; + + var branch = parseResult.GetValue(options.Branch); + if (branch != null) + arguments.TargetBranch = branch; + + var username = parseResult.GetValue(options.Username); + if (username != null) + arguments.Authentication.Username = username; + + var password = parseResult.GetValue(options.Password); + if (password != null) + arguments.Authentication.Password = password; + + var commit = parseResult.GetValue(options.Commit); + if (commit != null) + arguments.CommitId = commit; + + var dynamicRepoLocation = parseResult.GetValue(options.DynamicRepoLocation); + if (dynamicRepoLocation != null) + arguments.ClonePath = dynamicRepoLocation; + } + + private static (RootCommand rootCommand, CommandOptions options) BuildCommand() + { + var pathArgument = new Argument("path") + { + Description = "The directory containing .git. If not defined current directory is used.", + Arity = ArgumentArity.ZeroOrOne + }; + + var versionOption = new Option("--version") + { + Description = "Displays the version of GitVersion" + }; + + var diagnoseOption = new Option("--diagnose", "-d") + { + Description = "Runs GitVersion with additional diagnostic information" + }; + + var logFileOption = new Option("--log-file", "-l") + { + Description = "Path to logfile; specify 'console' to emit to stdout" + }; + + var outputOption = new Option("--output", "-o") + { + Description = "Determines the output to the console. Can be 'json', 'file', 'buildserver' or 'dotenv'", + AllowMultipleArgumentsPerToken = true, + Arity = ArgumentArity.ZeroOrMore + }; + + var outputFileOption = new Option("--output-file") + { + Description = "Path to output file. Used in combination with --output 'file'" + }; + + var showVariableOption = new Option("--show-variable", "-v") + { + Description = "Output just a particular variable" + }; + + var formatOption = new Option("--format", "-f") + { + Description = "Output a format containing version variables" + }; + + var configOption = new Option("--config", "-c") + { + Description = "Path to config file (defaults to GitVersion.yml)" + }; + + var showConfigOption = new Option("--show-config") + { + Description = "Outputs the effective GitVersion config in yaml format" + }; + + var overrideConfigOption = new Option("--override-config") + { + Description = "Overrides GitVersion config values inline (key=value pairs)", + AllowMultipleArgumentsPerToken = false, + Arity = ArgumentArity.ZeroOrMore + }; + + var targetPathOption = new Option("--target-path") + { + Description = "Same as 'path', but not positional" + }; + + var noFetchOption = new Option("--no-fetch") + { + Description = "Disables 'git fetch' during version calculation" + }; + + var noCacheOption = new Option("--no-cache") + { + Description = "Bypasses the cache, result will not be written to the cache" + }; + + var noNormalizeOption = new Option("--no-normalize") + { + Description = "Disables normalize step on a build server" + }; + + var allowShallowOption = new Option("--allow-shallow") + { + Description = "Allows GitVersion to run on a shallow clone" + }; + + var verbosityOption = new Option("--verbosity") + { + Description = "Specifies the amount of information to be displayed (Quiet, Minimal, Normal, Verbose, Diagnostic)" + }; + + var updateAssemblyInfoOption = new Option("--update-assembly-info") + { + Description = "Will recursively search for all 'AssemblyInfo.cs' files and update them", + AllowMultipleArgumentsPerToken = true, + Arity = ArgumentArity.ZeroOrMore + }; + + var updateProjectFilesOption = new Option("--update-project-files") + { + Description = "Will recursively search for all project files and update them", + AllowMultipleArgumentsPerToken = true, + Arity = ArgumentArity.ZeroOrMore + }; + + var ensureAssemblyInfoOption = new Option("--ensure-assembly-info") + { + Description = "If the assembly info file specified with --update-assembly-info is not found, it will be created" + }; + + var updateWixVersionFileOption = new Option("--update-wix-version-file") + { + Description = "All the GitVersion variables are written to 'GitVersion_WixVersion.wxi'" + }; + + var urlOption = new Option("--url") + { + Description = "Url to remote git repository" + }; + + var branchOption = new Option("--branch", "-b") + { + Description = "Name of the branch to use on the remote repository" + }; + + var usernameOption = new Option("--username", "-u") + { + Description = "Username in case authentication is required" + }; + + var passwordOption = new Option("--password", "-p") + { + Description = "Password in case authentication is required" + }; + + var commitOption = new Option("--commit") + { + Description = "The commit id to check" + }; + + var dynamicRepoLocationOption = new Option("--dynamic-repo-location") + { + Description = "Override default dynamic repository clone location" + }; + + var rootCommand = new RootCommand("Use convention to derive a SemVer product version from a GitFlow or GitHub based repository.") + { + pathArgument, versionOption, diagnoseOption, logFileOption, + outputOption, + outputFileOption, + showVariableOption, + formatOption, + configOption, + showConfigOption, + overrideConfigOption, + targetPathOption, + noFetchOption, + noCacheOption, + noNormalizeOption, + allowShallowOption, + verbosityOption, + updateAssemblyInfoOption, + updateProjectFilesOption, + ensureAssemblyInfoOption, + updateWixVersionFileOption, + urlOption, + branchOption, + usernameOption, + passwordOption, + commitOption, + dynamicRepoLocationOption + }; + + var options = new CommandOptions( + Path: pathArgument, + Version: versionOption, + Diagnose: diagnoseOption, + LogFile: logFileOption, + Output: outputOption, + OutputFile: outputFileOption, + ShowVariable: showVariableOption, + Format: formatOption, + Config: configOption, + ShowConfig: showConfigOption, + OverrideConfig: overrideConfigOption, + TargetPath: targetPathOption, + NoFetch: noFetchOption, + NoCache: noCacheOption, + NoNormalize: noNormalizeOption, + AllowShallow: allowShallowOption, + VerbosityOption: verbosityOption, + UpdateAssemblyInfo: updateAssemblyInfoOption, + UpdateProjectFiles: updateProjectFilesOption, + EnsureAssemblyInfo: ensureAssemblyInfoOption, + UpdateWixVersionFile: updateWixVersionFileOption, + Url: urlOption, + Branch: branchOption, + Username: usernameOption, + Password: passwordOption, + Commit: commitOption, + DynamicRepoLocation: dynamicRepoLocationOption + ); + + return (rootCommand, options); + } + + private void AddAuthentication(Arguments arguments) + { + var username = this.environment.GetEnvironmentVariable("GITVERSION_REMOTE_USERNAME"); + if (!username.IsNullOrWhiteSpace()) + { + arguments.Authentication.Username = username; + } + + var password = this.environment.GetEnvironmentVariable("GITVERSION_REMOTE_PASSWORD"); + if (!password.IsNullOrWhiteSpace()) + { + arguments.Authentication.Password = password; + } + } + + private IEnumerable ResolveFiles(string workingDirectory, ISet? assemblyInfoFiles) + { + if (assemblyInfoFiles == null) yield break; + + foreach (var file in assemblyInfoFiles) + { + foreach (var path in this.globbingResolver.Resolve(workingDirectory, file)) + { + yield return path; + } + } + } + + private void ValidateConfigurationFile(Arguments arguments) + { + if (arguments.ConfigurationFile.IsNullOrWhiteSpace()) return; + + if (FileSystemHelper.Path.IsPathRooted(arguments.ConfigurationFile)) + { + if (!this.fileSystem.File.Exists(arguments.ConfigurationFile)) + throw new WarningException($"Could not find config file at '{arguments.ConfigurationFile}'"); + arguments.ConfigurationFile = FileSystemHelper.Path.GetFullPath(arguments.ConfigurationFile); + } + else + { + var configFilePath = FileSystemHelper.Path.GetFullPath( + FileSystemHelper.Path.Combine(arguments.TargetPath, arguments.ConfigurationFile)); + if (!this.fileSystem.File.Exists(configFilePath)) + throw new WarningException($"Could not find config file at '{configFilePath}'"); + arguments.ConfigurationFile = configFilePath; + } + } + + private static void ParseShowVariable(Arguments arguments, string value) + { + string? versionVariable = null; + + if (!value.IsNullOrWhiteSpace()) + { + versionVariable = availableVariables.SingleOrDefault( + av => av.Equals(value.Replace("'", ""), StringComparison.CurrentCultureIgnoreCase)); + } + + if (versionVariable == null) + { + var message = $"--show-variable requires a valid version variable. Available variables are:{FileSystemHelper.Path.NewLine}" + + string.Join(", ", availableVariables.Select(x => $"'{x}'")); + throw new WarningException(message); + } + + arguments.ShowVariable = versionVariable; + } + + private static void ParseFormat(Arguments arguments, string value) + { + if (value.IsNullOrWhiteSpace()) + { + throw new WarningException("Format requires a valid format string. Available variables are: " + + string.Join(", ", availableVariables)); + } + + var foundVariable = availableVariables.Any( + variable => value.Contains(variable, StringComparison.CurrentCultureIgnoreCase)); + + if (!foundVariable) + { + throw new WarningException("Format requires a valid format string. Available variables are: " + + string.Join(", ", availableVariables)); + } + + arguments.Format = value; + } + + internal static Verbosity ParseVerbosity(string? value) + { + if (!Enum.TryParse(value, true, out Verbosity verbosity)) + { + throw new WarningException($"Could not parse Verbosity value '{value}'"); + } + + return verbosity; + } + + private static void ParseOverrideConfig(Arguments arguments, IReadOnlyCollection? values) + { + if (values == null || values.Count == 0) + return; + + var parser = new OverrideConfigurationOptionParser(); + + foreach (var keyValueOption in values) + { + var keyAndValue = QuotedStringHelpers.SplitUnquoted(keyValueOption, '='); + if (keyAndValue.Length != 2) + { + throw new WarningException( + $"Could not parse /overrideconfig option: {keyValueOption}. Ensure it is in format 'key=value'."); + } + + var optionKey = keyAndValue[0].ToLowerInvariant(); + if (!OverrideConfigurationOptionParser.SupportedProperties.Contains(optionKey)) + { + throw new WarningException( + $"Could not parse /overrideconfig option: {keyValueOption}. Unsupported 'key'."); + } + + parser.SetValue(optionKey, keyAndValue[1]); + } + + arguments.OverrideConfiguration = parser.GetOverrideConfiguration(); + } + + private record CommandOptions( + Argument Path, + Option Version, + Option Diagnose, + Option LogFile, + Option Output, + Option OutputFile, + Option ShowVariable, + Option Format, + Option Config, + Option ShowConfig, + Option OverrideConfig, + Option TargetPath, + Option NoFetch, + Option NoCache, + Option NoNormalize, + Option AllowShallow, + Option VerbosityOption, + Option UpdateAssemblyInfo, + Option UpdateProjectFiles, + Option EnsureAssemblyInfo, + Option UpdateWixVersionFile, + Option Url, + Option Branch, + Option Username, + Option Password, + Option Commit, + Option DynamicRepoLocation + ); +} From 6bf3537abe186b61e12fd2ae50efdb49fad5c585 Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Mon, 2 Mar 2026 23:22:35 +0100 Subject: [PATCH 04/24] refactor(app): wire new parser via DI with env-var toggle --- src/GitVersion.App/GitVersionAppModule.cs | 19 +++++++++++++------ src/GitVersion.App/HelpWriter.cs | 4 ++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/GitVersion.App/GitVersionAppModule.cs b/src/GitVersion.App/GitVersionAppModule.cs index 6aff30e990..1dfbf338c6 100644 --- a/src/GitVersion.App/GitVersionAppModule.cs +++ b/src/GitVersion.App/GitVersionAppModule.cs @@ -2,21 +2,28 @@ namespace GitVersion; -internal class GitVersionAppModule(params string[] args) : IGitVersionModule +internal class GitVersionAppModule(string[]? args = null, bool useLegacyParser = false) : IGitVersionModule { public void RegisterTypes(IServiceCollection services) { - services.AddSingleton(); - services.AddSingleton(); + if (useLegacyParser) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } + else + { + services.AddSingleton(); + } - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(sp => { - var arguments = sp.GetRequiredService().ParseArguments(args); + var arguments = sp.GetRequiredService().ParseArguments(args ?? []); var gitVersionOptions = arguments.ToOptions(); return Options.Create(gitVersionOptions); }); diff --git a/src/GitVersion.App/HelpWriter.cs b/src/GitVersion.App/HelpWriter.cs index 494e27bb43..d57a49f304 100644 --- a/src/GitVersion.App/HelpWriter.cs +++ b/src/GitVersion.App/HelpWriter.cs @@ -16,13 +16,13 @@ public void WriteTo(Action writeAction) var assembly = Assembly.GetExecutingAssembly(); this.versionWriter.WriteTo(assembly, v => version = v); - var args = ArgumentList(); + var args = LegacyArgumentList(); var message = $"GitVersion {version}{FileSystemHelper.Path.NewLine}{FileSystemHelper.Path.NewLine}{args}"; writeAction(message); } - private string ArgumentList() + private string LegacyArgumentList() { using var argumentsMarkdownStream = GetType().Assembly.GetManifestResourceStream("GitVersion.arguments.md"); argumentsMarkdownStream.NotNull(); From ea49c765cba74f349dfcfab95c2a43bdf42edfcf Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Mon, 2 Mar 2026 23:22:47 +0100 Subject: [PATCH 05/24] test(app): update ArgumentParserTests for POSIX-style syntax --- .../ArgumentParserTests.cs | 193 +++++++++--------- 1 file changed, 101 insertions(+), 92 deletions(-) diff --git a/src/GitVersion.App.Tests/ArgumentParserTests.cs b/src/GitVersion.App.Tests/ArgumentParserTests.cs index 2c38a431ee..e1faab313c 100644 --- a/src/GitVersion.App.Tests/ArgumentParserTests.cs +++ b/src/GitVersion.App.Tests/ArgumentParserTests.cs @@ -52,6 +52,15 @@ public void NoPathAndLogfileShouldUseCurrentDirectoryTargetDirectory() arguments.IsHelp.ShouldBe(false); } + [Test] + public void NoPathAndLogfileLongFormShouldUseCurrentDirectoryTargetDirectory() + { + var arguments = this.argumentParser.ParseArguments("--log-file logFilePath"); + arguments.TargetPath.ShouldBe(SysEnv.CurrentDirectory); + arguments.LogFilePath.ShouldBe("logFilePath"); + arguments.IsHelp.ShouldBe(false); + } + [Test] public void HelpSwitchTest() { @@ -67,7 +76,7 @@ public void HelpSwitchTest() [Test] public void VersionSwitchTest() { - var arguments = this.argumentParser.ParseArguments("-version"); + var arguments = this.argumentParser.ParseArguments("--version"); Assert.Multiple(() => { Assert.That(arguments.TargetPath, Is.Null); @@ -79,7 +88,7 @@ public void VersionSwitchTest() [Test] public void TargetDirectoryAndLogFilePathCanBeParsed() { - var arguments = this.argumentParser.ParseArguments("targetDirectoryPath -l logFilePath"); + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath --log-file logFilePath"); arguments.TargetPath.ShouldBe("targetDirectoryPath"); arguments.LogFilePath.ShouldBe("logFilePath"); arguments.IsHelp.ShouldBe(false); @@ -98,9 +107,9 @@ public void UsernameAndPasswordCanBeParsed() [Test] public void UnknownOutputShouldThrow() { - var exception = Assert.Throws(() => this.argumentParser.ParseArguments("targetDirectoryPath -output invalid_value")); + var exception = Assert.Throws(() => this.argumentParser.ParseArguments("targetDirectoryPath --output invalid_value")); exception.ShouldNotBeNull(); - exception.Message.ShouldBe("Value 'invalid_value' cannot be parsed as output type, please use 'json', 'file', 'buildserver' or 'dotenv'"); + exception.Message.ShouldContain("invalid_value"); } [Test] @@ -115,7 +124,7 @@ public void OutputDefaultsToJson() [Test] public void OutputJsonCanBeParsed() { - var arguments = this.argumentParser.ParseArguments("targetDirectoryPath -output json"); + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath --output json"); arguments.Output.ShouldContain(OutputType.Json); arguments.Output.ShouldNotContain(OutputType.BuildServer); arguments.Output.ShouldNotContain(OutputType.File); @@ -124,7 +133,7 @@ public void OutputJsonCanBeParsed() [Test] public void MultipleOutputJsonCanBeParsed() { - var arguments = this.argumentParser.ParseArguments("targetDirectoryPath -output json -output json"); + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath --output json --output json"); arguments.Output.ShouldContain(OutputType.Json); arguments.Output.ShouldNotContain(OutputType.BuildServer); arguments.Output.ShouldNotContain(OutputType.File); @@ -133,7 +142,7 @@ public void MultipleOutputJsonCanBeParsed() [Test] public void OutputBuildserverCanBeParsed() { - var arguments = this.argumentParser.ParseArguments("targetDirectoryPath -output buildserver"); + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath --output buildserver"); arguments.Output.ShouldContain(OutputType.BuildServer); arguments.Output.ShouldNotContain(OutputType.Json); arguments.Output.ShouldNotContain(OutputType.File); @@ -142,7 +151,7 @@ public void OutputBuildserverCanBeParsed() [Test] public void MultipleOutputBuildserverCanBeParsed() { - var arguments = this.argumentParser.ParseArguments("targetDirectoryPath -output buildserver -output buildserver"); + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath --output buildserver --output buildserver"); arguments.Output.ShouldContain(OutputType.BuildServer); arguments.Output.ShouldNotContain(OutputType.Json); arguments.Output.ShouldNotContain(OutputType.File); @@ -151,7 +160,7 @@ public void MultipleOutputBuildserverCanBeParsed() [Test] public void OutputFileCanBeParsed() { - var arguments = this.argumentParser.ParseArguments("targetDirectoryPath -output file"); + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath --output file"); arguments.Output.ShouldContain(OutputType.File); arguments.Output.ShouldNotContain(OutputType.BuildServer); arguments.Output.ShouldNotContain(OutputType.Json); @@ -160,7 +169,7 @@ public void OutputFileCanBeParsed() [Test] public void MultipleOutputFileCanBeParsed() { - var arguments = this.argumentParser.ParseArguments("targetDirectoryPath -output file -output file"); + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath --output file --output file"); arguments.Output.ShouldContain(OutputType.File); arguments.Output.ShouldNotContain(OutputType.BuildServer); arguments.Output.ShouldNotContain(OutputType.Json); @@ -169,7 +178,7 @@ public void MultipleOutputFileCanBeParsed() [Test] public void OutputBuildserverAndJsonCanBeParsed() { - var arguments = this.argumentParser.ParseArguments("targetDirectoryPath -output buildserver -output json"); + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath --output buildserver --output json"); arguments.Output.ShouldContain(OutputType.BuildServer); arguments.Output.ShouldContain(OutputType.Json); arguments.Output.ShouldNotContain(OutputType.File); @@ -178,7 +187,7 @@ public void OutputBuildserverAndJsonCanBeParsed() [Test] public void OutputBuildserverAndJsonAndFileCanBeParsed() { - var arguments = this.argumentParser.ParseArguments("targetDirectoryPath -output buildserver -output json -output file"); + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath --output buildserver --output json --output file"); arguments.Output.ShouldContain(OutputType.BuildServer); arguments.Output.ShouldContain(OutputType.Json); arguments.Output.ShouldContain(OutputType.File); @@ -187,12 +196,12 @@ public void OutputBuildserverAndJsonAndFileCanBeParsed() [Test] public void MultipleArgsAndFlag() { - var arguments = this.argumentParser.ParseArguments("targetDirectoryPath -output buildserver -updateAssemblyInfo"); + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath --output buildserver --update-assembly-info"); arguments.Output.ShouldContain(OutputType.BuildServer); } - [TestCase("-output file", "GitVersion.json")] - [TestCase("-output file -outputfile version.json", "version.json")] + [TestCase("--output file", "GitVersion.json")] + [TestCase("--output file --output-file version.json", "version.json")] public void OutputFileArgumentCanBeParsed(string args, string outputFile) { var arguments = this.argumentParser.ParseArguments(args); @@ -204,7 +213,7 @@ public void OutputFileArgumentCanBeParsed(string args, string outputFile) [Test] public void UrlAndBranchNameCanBeParsed() { - var arguments = this.argumentParser.ParseArguments("targetDirectoryPath -url https://github.com/Particular/GitVersion.git -b someBranch"); + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath --url https://github.com/Particular/GitVersion.git -b someBranch"); arguments.TargetPath.ShouldBe("targetDirectoryPath"); arguments.TargetUrl.ShouldBe("https://github.com/Particular/GitVersion.git"); arguments.TargetBranch.ShouldBe("someBranch"); @@ -214,13 +223,13 @@ public void UrlAndBranchNameCanBeParsed() [Test] public void WrongNumberOfArgumentsShouldThrow() { - var exception = Assert.Throws(() => this.argumentParser.ParseArguments("targetDirectoryPath -l logFilePath extraArg")); + var exception = Assert.Throws(() => this.argumentParser.ParseArguments("targetDirectoryPath --log-file logFilePath extraArg")); exception.ShouldNotBeNull(); exception.Message.ShouldBe("Could not parse command line parameter 'extraArg'."); } [TestCase("targetDirectoryPath -x logFilePath")] - [TestCase("/invalid-argument")] + [TestCase("--invalid-argument")] public void UnknownArgumentsShouldThrow(string arguments) { var exception = Assert.Throws(() => this.argumentParser.ParseArguments(arguments)); @@ -228,38 +237,38 @@ public void UnknownArgumentsShouldThrow(string arguments) exception.Message.ShouldStartWith("Could not parse command line parameter"); } - [TestCase("-updateAssemblyInfo true")] - [TestCase("-updateAssemblyInfo 1")] - [TestCase("-updateAssemblyInfo")] - [TestCase("-updateAssemblyInfo assemblyInfo.cs")] - [TestCase("-updateAssemblyInfo assemblyInfo.cs -ensureassemblyinfo")] - [TestCase("-updateAssemblyInfo assemblyInfo.cs otherAssemblyInfo.cs")] - [TestCase("-updateAssemblyInfo Assembly.cs Assembly.cs -ensureassemblyinfo")] + [TestCase("--update-assembly-info true")] + [TestCase("--update-assembly-info 1")] + [TestCase("--update-assembly-info")] + [TestCase("--update-assembly-info assemblyInfo.cs")] + [TestCase("--update-assembly-info assemblyInfo.cs --ensure-assembly-info")] + [TestCase("--update-assembly-info assemblyInfo.cs otherAssemblyInfo.cs")] + [TestCase("--update-assembly-info Assembly.cs Assembly.cs --ensure-assembly-info")] public void UpdateAssemblyInfoTrue(string command) { var arguments = this.argumentParser.ParseArguments(command); arguments.UpdateAssemblyInfo.ShouldBe(true); } - [TestCase("-updateProjectFiles assemblyInfo.csproj")] - [TestCase("-updateProjectFiles assemblyInfo.csproj")] - [TestCase("-updateProjectFiles assemblyInfo.csproj otherAssemblyInfo.fsproj")] - [TestCase("-updateProjectFiles")] + [TestCase("--update-project-files assemblyInfo.csproj")] + [TestCase("--update-project-files assemblyInfo.csproj")] + [TestCase("--update-project-files assemblyInfo.csproj otherAssemblyInfo.fsproj")] + [TestCase("--update-project-files")] public void UpdateProjectTrue(string command) { var arguments = this.argumentParser.ParseArguments(command); arguments.UpdateProjectFiles.ShouldBe(true); } - [TestCase("-updateAssemblyInfo false")] - [TestCase("-updateAssemblyInfo 0")] + [TestCase("--update-assembly-info false")] + [TestCase("--update-assembly-info 0")] public void UpdateAssemblyInfoFalse(string command) { var arguments = this.argumentParser.ParseArguments(command); arguments.UpdateAssemblyInfo.ShouldBe(false); } - [TestCase("-updateAssemblyInfo Assembly.cs Assembly1.cs -ensureassemblyinfo")] + [TestCase("--update-assembly-info Assembly.cs Assembly1.cs --ensure-assembly-info")] public void CreateMultipleAssemblyInfoProtected(string command) { var exception = Assert.Throws(() => this.argumentParser.ParseArguments(command)); @@ -267,7 +276,7 @@ public void CreateMultipleAssemblyInfoProtected(string command) exception.Message.ShouldBe("Can't specify multiple assembly info files when using /ensureassemblyinfo switch, either use a single assembly info file or do not specify /ensureassemblyinfo and create assembly info files manually"); } - [TestCase("-updateProjectFiles Assembly.csproj -ensureassemblyinfo")] + [TestCase("--update-project-files Assembly.csproj --ensure-assembly-info")] public void UpdateProjectInfoWithEnsureAssemblyInfoProtected(string command) { var exception = Assert.Throws(() => this.argumentParser.ParseArguments(command)); @@ -283,7 +292,7 @@ public void UpdateAssemblyInfoWithFilename() var assemblyFile = FileSystemHelper.Path.Combine(repo.RepositoryPath, "CommonAssemblyInfo.cs"); using var file = this.fileSystem.File.Create(assemblyFile); - var arguments = this.argumentParser.ParseArguments($"-targetpath {repo.RepositoryPath} -updateAssemblyInfo CommonAssemblyInfo.cs"); + var arguments = this.argumentParser.ParseArguments($"--target-path {repo.RepositoryPath} --update-assembly-info CommonAssemblyInfo.cs"); arguments.UpdateAssemblyInfo.ShouldBe(true); arguments.UpdateAssemblyInfoFileName.Count.ShouldBe(1); arguments.UpdateAssemblyInfoFileName.ShouldContain(x => FileSystemHelper.Path.GetFileName(x).Equals("CommonAssemblyInfo.cs")); @@ -300,7 +309,7 @@ public void UpdateAssemblyInfoWithMultipleFilenames() var assemblyFile2 = FileSystemHelper.Path.Combine(repo.RepositoryPath, "VersionAssemblyInfo.cs"); using var file2 = this.fileSystem.File.Create(assemblyFile2); - var arguments = this.argumentParser.ParseArguments($"-targetpath {repo.RepositoryPath} -updateAssemblyInfo CommonAssemblyInfo.cs VersionAssemblyInfo.cs"); + var arguments = this.argumentParser.ParseArguments($"--target-path {repo.RepositoryPath} --update-assembly-info CommonAssemblyInfo.cs VersionAssemblyInfo.cs"); arguments.UpdateAssemblyInfo.ShouldBe(true); arguments.UpdateAssemblyInfoFileName.Count.ShouldBe(2); arguments.UpdateAssemblyInfoFileName.ShouldContain(x => FileSystemHelper.Path.GetFileName(x).Equals("CommonAssemblyInfo.cs")); @@ -318,7 +327,7 @@ public void UpdateProjectFilesWithMultipleFilenames() var assemblyFile2 = FileSystemHelper.Path.Combine(repo.RepositoryPath, "VersionAssemblyInfo.csproj"); using var file2 = this.fileSystem.File.Create(assemblyFile2); - var arguments = this.argumentParser.ParseArguments($"-targetpath {repo.RepositoryPath} -updateProjectFiles CommonAssemblyInfo.csproj VersionAssemblyInfo.csproj"); + var arguments = this.argumentParser.ParseArguments($"--target-path {repo.RepositoryPath} --update-project-files CommonAssemblyInfo.csproj VersionAssemblyInfo.csproj"); arguments.UpdateProjectFiles.ShouldBe(true); arguments.UpdateAssemblyInfoFileName.Count.ShouldBe(2); arguments.UpdateAssemblyInfoFileName.ShouldContain(x => FileSystemHelper.Path.GetFileName(x).Equals("CommonAssemblyInfo.csproj")); @@ -342,7 +351,7 @@ public void UpdateAssemblyInfoWithMultipleFilenamesMatchingGlobbing() var assemblyFile3 = FileSystemHelper.Path.Combine(subdir, "LocalAssemblyInfo.cs"); using var file3 = this.fileSystem.File.Create(assemblyFile3); - var arguments = this.argumentParser.ParseArguments($"-targetpath {repo.RepositoryPath} -updateAssemblyInfo **/*AssemblyInfo.cs"); + var arguments = this.argumentParser.ParseArguments($"--target-path {repo.RepositoryPath} --update-assembly-info **/*AssemblyInfo.cs"); arguments.UpdateAssemblyInfo.ShouldBe(true); arguments.UpdateAssemblyInfoFileName.Count.ShouldBe(3); arguments.UpdateAssemblyInfoFileName.ShouldContain(x => FileSystemHelper.Path.GetFileName(x).Equals("CommonAssemblyInfo.cs")); @@ -361,7 +370,7 @@ public void UpdateAssemblyInfoWithRelativeFilename() var targetPath = FileSystemHelper.Path.Combine(repo.RepositoryPath, "subdir1", "subdir2"); this.fileSystem.Directory.CreateDirectory(targetPath); - var arguments = this.argumentParser.ParseArguments($@"-targetpath {targetPath} -updateAssemblyInfo ..\..\CommonAssemblyInfo.cs"); + var arguments = this.argumentParser.ParseArguments($@"--target-path {targetPath} --update-assembly-info ..\..\CommonAssemblyInfo.cs"); arguments.UpdateAssemblyInfo.ShouldBe(true); arguments.UpdateAssemblyInfoFileName.Count.ShouldBe(1); arguments.UpdateAssemblyInfoFileName.ShouldContain(x => FileSystemHelper.Path.GetFileName(x).Equals("CommonAssemblyInfo.cs")); @@ -370,14 +379,14 @@ public void UpdateAssemblyInfoWithRelativeFilename() [Test] public void OverrideconfigWithNoOptions() { - var arguments = this.argumentParser.ParseArguments("/overrideconfig"); + var arguments = this.argumentParser.ParseArguments("--override-config"); arguments.OverrideConfiguration.ShouldBeNull(); } [TestCaseSource(nameof(OverrideconfigWithInvalidOptionTestData))] public string OverrideconfigWithInvalidOption(string options) { - var exception = Assert.Throws(() => this.argumentParser.ParseArguments($"/overrideconfig {options}")); + var exception = Assert.Throws(() => this.argumentParser.ParseArguments($"--override-config {options}")); exception.ShouldNotBeNull(); return exception.Message; } @@ -397,7 +406,7 @@ private static IEnumerable OverrideconfigWithInvalidOptionTestData [TestCaseSource(nameof(OverrideConfigWithSingleOptionTestData))] public void OverrideConfigWithSingleOptions(string options, IGitVersionConfiguration expected) { - var arguments = this.argumentParser.ParseArguments($"/overrideconfig {options}"); + var arguments = this.argumentParser.ParseArguments($"--override-config {options}"); ConfigurationHelper configurationHelper = new(arguments.OverrideConfiguration); configurationHelper.Configuration.ShouldBeEquivalentTo(expected); @@ -551,7 +560,7 @@ public void OverrideConfigWithMultipleOptions(string options, IGitVersionConfigu private static IEnumerable OverrideConfigWithMultipleOptionsTestData() { yield return new TestCaseData( - "/overrideconfig tag-prefix=sample /overrideconfig assembly-versioning-scheme=MajorMinor", + "--override-config tag-prefix=sample --override-config assembly-versioning-scheme=MajorMinor", new GitVersionConfiguration { TagPrefixPattern = "sample", @@ -559,7 +568,7 @@ private static IEnumerable OverrideConfigWithMultipleOptionsTestDa } ); yield return new TestCaseData( - "/overrideconfig tag-prefix=sample /overrideconfig assembly-versioning-format=\"{Major}.{Minor}.{Patch}.{env:CI_JOB_ID ?? 0}\"", + "--override-config tag-prefix=sample --override-config assembly-versioning-format=\"{Major}.{Minor}.{Patch}.{env:CI_JOB_ID ?? 0}\"", new GitVersionConfiguration { TagPrefixPattern = "sample", @@ -567,7 +576,7 @@ private static IEnumerable OverrideConfigWithMultipleOptionsTestDa } ); yield return new TestCaseData( - "/overrideconfig tag-prefix=sample /overrideconfig assembly-versioning-format=\"{Major}.{Minor}.{Patch}.{env:CI_JOB_ID ?? 0}\" /overrideconfig update-build-number=true /overrideconfig assembly-versioning-scheme=MajorMinorPatchTag /overrideconfig mode=ContinuousDelivery /overrideconfig tag-pre-release-weight=4", + "--override-config tag-prefix=sample --override-config assembly-versioning-format=\"{Major}.{Minor}.{Patch}.{env:CI_JOB_ID ?? 0}\" --override-config update-build-number=true --override-config assembly-versioning-scheme=MajorMinorPatchTag --override-config mode=ContinuousDelivery --override-config tag-pre-release-weight=4", new GitVersionConfiguration { TagPrefixPattern = "sample", @@ -583,70 +592,70 @@ private static IEnumerable OverrideConfigWithMultipleOptionsTestDa [Test] public void EnsureAssemblyInfoTrueWhenFound() { - var arguments = this.argumentParser.ParseArguments("-ensureAssemblyInfo"); + var arguments = this.argumentParser.ParseArguments("--ensure-assembly-info"); arguments.EnsureAssemblyInfo.ShouldBe(true); } [Test] public void EnsureAssemblyInfoTrue() { - var arguments = this.argumentParser.ParseArguments("-ensureAssemblyInfo true"); + var arguments = this.argumentParser.ParseArguments("--ensure-assembly-info true"); arguments.EnsureAssemblyInfo.ShouldBe(true); } [Test] public void EnsureAssemblyInfoFalse() { - var arguments = this.argumentParser.ParseArguments("-ensureAssemblyInfo false"); + var arguments = this.argumentParser.ParseArguments("--ensure-assembly-info false"); arguments.EnsureAssemblyInfo.ShouldBe(false); } [Test] public void DynamicRepoLocation() { - var arguments = this.argumentParser.ParseArguments("-dynamicRepoLocation /tmp/foo"); + var arguments = this.argumentParser.ParseArguments("--dynamic-repo-location /tmp/foo"); arguments.ClonePath.ShouldBe("/tmp/foo"); } [Test] public void CanLogToConsole() { - var arguments = this.argumentParser.ParseArguments("-l console"); + var arguments = this.argumentParser.ParseArguments("--log-file console"); arguments.LogFilePath.ShouldBe("console"); } [Test] public void NofetchTrueWhenDefined() { - var arguments = this.argumentParser.ParseArguments("-nofetch"); + var arguments = this.argumentParser.ParseArguments("--no-fetch"); arguments.NoFetch.ShouldBe(true); } [Test] public void NoNormalizeTrueWhenDefined() { - var arguments = this.argumentParser.ParseArguments("-nonormalize"); + var arguments = this.argumentParser.ParseArguments("--no-normalize"); arguments.NoNormalize.ShouldBe(true); } [Test] public void AllowshallowTrueWhenDefined() { - var arguments = this.argumentParser.ParseArguments("-allowshallow"); + var arguments = this.argumentParser.ParseArguments("--allow-shallow"); arguments.AllowShallow.ShouldBe(true); } [Test] public void DiagTrueWhenDefined() { - var arguments = this.argumentParser.ParseArguments("-diag"); + var arguments = this.argumentParser.ParseArguments("--diagnose"); arguments.Diag.ShouldBe(true); } [Test] public void DiagAndLogToConsoleIsNotIgnored() { - var arguments = this.argumentParser.ParseArguments("-diag -l console"); + var arguments = this.argumentParser.ParseArguments("--diagnose --log-file console"); arguments.Diag.ShouldBe(true); arguments.LogFilePath.ShouldBe("console"); } @@ -654,7 +663,7 @@ public void DiagAndLogToConsoleIsNotIgnored() [Test] public void OtherArgumentsCanBeParsedBeforeNofetch() { - var arguments = this.argumentParser.ParseArguments("targetpath -nofetch "); + var arguments = this.argumentParser.ParseArguments("targetpath --no-fetch"); arguments.TargetPath.ShouldBe("targetpath"); arguments.NoFetch.ShouldBe(true); } @@ -662,7 +671,7 @@ public void OtherArgumentsCanBeParsedBeforeNofetch() [Test] public void OtherArgumentsCanBeParsedBeforeNonormalize() { - var arguments = this.argumentParser.ParseArguments("targetpath -nonormalize"); + var arguments = this.argumentParser.ParseArguments("targetpath --no-normalize"); arguments.TargetPath.ShouldBe("targetpath"); arguments.NoNormalize.ShouldBe(true); } @@ -670,7 +679,7 @@ public void OtherArgumentsCanBeParsedBeforeNonormalize() [Test] public void OtherArgumentsCanBeParsedBeforeNocache() { - var arguments = this.argumentParser.ParseArguments("targetpath -nocache"); + var arguments = this.argumentParser.ParseArguments("targetpath --no-cache"); arguments.TargetPath.ShouldBe("targetpath"); arguments.NoCache.ShouldBe(true); } @@ -678,35 +687,35 @@ public void OtherArgumentsCanBeParsedBeforeNocache() [Test] public void OtherArgumentsCanBeParsedBeforeAllowshallow() { - var arguments = this.argumentParser.ParseArguments("targetpath -allowshallow"); + var arguments = this.argumentParser.ParseArguments("targetpath --allow-shallow"); arguments.TargetPath.ShouldBe("targetpath"); arguments.AllowShallow.ShouldBe(true); } - [TestCase("-nofetch -nonormalize -nocache -allowshallow")] - [TestCase("-nofetch -nonormalize -allowshallow -nocache")] - [TestCase("-nofetch -nocache -nonormalize -allowshallow")] - [TestCase("-nofetch -nocache -allowshallow -nonormalize")] - [TestCase("-nofetch -allowshallow -nonormalize -nocache")] - [TestCase("-nofetch -allowshallow -nocache -nonormalize")] - [TestCase("-nonormalize -nofetch -nocache -allowshallow")] - [TestCase("-nonormalize -nofetch -allowshallow -nocache")] - [TestCase("-nonormalize -nocache -nofetch -allowshallow")] - [TestCase("-nonormalize -nocache -allowshallow -nofetch")] - [TestCase("-nonormalize -allowshallow -nofetch -nocache")] - [TestCase("-nonormalize -allowshallow -nocache -nofetch")] - [TestCase("-nocache -nofetch -nonormalize -allowshallow")] - [TestCase("-nocache -nofetch -allowshallow -nonormalize")] - [TestCase("-nocache -nonormalize -nofetch -allowshallow")] - [TestCase("-nocache -nonormalize -allowshallow -nofetch")] - [TestCase("-nocache -allowshallow -nofetch -nonormalize")] - [TestCase("-nocache -allowshallow -nonormalize -nofetch")] - [TestCase("-allowshallow -nofetch -nonormalize -nocache")] - [TestCase("-allowshallow -nofetch -nocache -nonormalize")] - [TestCase("-allowshallow -nonormalize -nofetch -nocache")] - [TestCase("-allowshallow -nonormalize -nocache -nofetch")] - [TestCase("-allowshallow -nocache -nofetch -nonormalize")] - [TestCase("-allowshallow -nocache -nonormalize -nofetch")] + [TestCase("--no-fetch --no-normalize --no-cache --allow-shallow")] + [TestCase("--no-fetch --no-normalize --allow-shallow --no-cache")] + [TestCase("--no-fetch --no-cache --no-normalize --allow-shallow")] + [TestCase("--no-fetch --no-cache --allow-shallow --no-normalize")] + [TestCase("--no-fetch --allow-shallow --no-normalize --no-cache")] + [TestCase("--no-fetch --allow-shallow --no-cache --no-normalize")] + [TestCase("--no-normalize --no-fetch --no-cache --allow-shallow")] + [TestCase("--no-normalize --no-fetch --allow-shallow --no-cache")] + [TestCase("--no-normalize --no-cache --no-fetch --allow-shallow")] + [TestCase("--no-normalize --no-cache --allow-shallow --no-fetch")] + [TestCase("--no-normalize --allow-shallow --no-fetch --no-cache")] + [TestCase("--no-normalize --allow-shallow --no-cache --no-fetch")] + [TestCase("--no-cache --no-fetch --no-normalize --allow-shallow")] + [TestCase("--no-cache --no-fetch --allow-shallow --no-normalize")] + [TestCase("--no-cache --no-normalize --no-fetch --allow-shallow")] + [TestCase("--no-cache --no-normalize --allow-shallow --no-fetch")] + [TestCase("--no-cache --allow-shallow --no-fetch --no-normalize")] + [TestCase("--no-cache --allow-shallow --no-normalize --no-fetch")] + [TestCase("--allow-shallow --no-fetch --no-normalize --no-cache")] + [TestCase("--allow-shallow --no-fetch --no-cache --no-normalize")] + [TestCase("--allow-shallow --no-normalize --no-fetch --no-cache")] + [TestCase("--allow-shallow --no-normalize --no-cache --no-fetch")] + [TestCase("--allow-shallow --no-cache --no-fetch --no-normalize")] + [TestCase("--allow-shallow --no-cache --no-normalize --no-fetch")] public void SeveralSwitchesCanBeParsed(string commandLineArgs) { var arguments = this.argumentParser.ParseArguments(commandLineArgs); @@ -726,7 +735,7 @@ public void LogPathCanContainForwardSlash() [Test] public void BooleanArgumentHandling() { - var arguments = this.argumentParser.ParseArguments("/nofetch /updateassemblyinfo true"); + var arguments = this.argumentParser.ParseArguments("--no-fetch --update-assembly-info true"); arguments.NoFetch.ShouldBe(true); arguments.UpdateAssemblyInfo.ShouldBe(true); } @@ -734,7 +743,7 @@ public void BooleanArgumentHandling() [Test] public void NocacheTrueWhenDefined() { - var arguments = this.argumentParser.ParseArguments("-nocache"); + var arguments = this.argumentParser.ParseArguments("--no-cache"); arguments.NoCache.ShouldBe(true); } @@ -748,11 +757,11 @@ public void CheckVerbosityParsing(string command, bool shouldThrow, Verbosity ex { if (shouldThrow) { - Assert.Throws(() => ArgumentParser.ParseVerbosity(command)); + Assert.Throws(() => SystemCommandLineArgumentParser.ParseVerbosity(command)); } else { - var verbosity = ArgumentParser.ParseVerbosity(command); + var verbosity = SystemCommandLineArgumentParser.ParseVerbosity(command); verbosity.ShouldBe(expectedVerbosity); } } @@ -777,7 +786,7 @@ public void EmptyArgumentsRemotePasswordDefinedSetsPassword() public void ArbitraryArgumentsRemoteUsernameDefinedSetsUsername() { this.environment.SetEnvironmentVariable("GITVERSION_REMOTE_USERNAME", "value"); - var arguments = this.argumentParser.ParseArguments("-nocache"); + var arguments = this.argumentParser.ParseArguments("--no-cache"); arguments.Authentication.Username.ShouldBe("value"); } @@ -785,35 +794,35 @@ public void ArbitraryArgumentsRemoteUsernameDefinedSetsUsername() public void ArbitraryArgumentsRemotePasswordDefinedSetsPassword() { this.environment.SetEnvironmentVariable("GITVERSION_REMOTE_PASSWORD", "value"); - var arguments = this.argumentParser.ParseArguments("-nocache"); + var arguments = this.argumentParser.ParseArguments("--no-cache"); arguments.Authentication.Password.ShouldBe("value"); } [Test] public void EnsureShowVariableIsSet() { - var arguments = this.argumentParser.ParseArguments("-showvariable SemVer"); + var arguments = this.argumentParser.ParseArguments("--show-variable SemVer"); arguments.ShowVariable.ShouldBe("SemVer"); } [Test] public void EnsureFormatIsSet() { - var arguments = this.argumentParser.ParseArguments("-format {Major}.{Minor}.{Patch}"); + var arguments = this.argumentParser.ParseArguments("--format {Major}.{Minor}.{Patch}"); arguments.Format.ShouldBe("{Major}.{Minor}.{Patch}"); } [TestCase("custom-config.yaml")] [TestCase("/tmp/custom-config.yaml")] public void ThrowIfConfigurationFileDoesNotExist(string configFile) => - Should.Throw(() => _ = this.argumentParser.ParseArguments($"-config {configFile}")); + Should.Throw(() => _ = this.argumentParser.ParseArguments($"--config {configFile}")); [Test] public void EnsureConfigurationFileIsSet() { var configFile = FileSystemHelper.Path.GetTempPath() + Guid.NewGuid() + ".yaml"; this.fileSystem.File.WriteAllText(configFile, "next-version: 1.0.0"); - var arguments = this.argumentParser.ParseArguments($"-config {configFile}"); + var arguments = this.argumentParser.ParseArguments($"--config {configFile}"); arguments.ConfigurationFile.ShouldBe(configFile); this.fileSystem.File.Delete(configFile); } From c2f50c163e941d506310f5ea553180885158c29f Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Mon, 2 Mar 2026 23:22:59 +0100 Subject: [PATCH 06/24] test(app): add LegacyArgumentParserTests and isolate with TestEnvironment --- src/GitVersion.App.Tests/HelpWriterTests.cs | 54 +- .../LegacyArgumentParserTests.cs | 819 ++++++++++++++++++ .../VersionWriterTests.cs | 2 +- 3 files changed, 848 insertions(+), 27 deletions(-) create mode 100644 src/GitVersion.App.Tests/LegacyArgumentParserTests.cs diff --git a/src/GitVersion.App.Tests/HelpWriterTests.cs b/src/GitVersion.App.Tests/HelpWriterTests.cs index 7387bf9279..cf8b3a7852 100644 --- a/src/GitVersion.App.Tests/HelpWriterTests.cs +++ b/src/GitVersion.App.Tests/HelpWriterTests.cs @@ -9,7 +9,7 @@ public class HelpWriterTests : TestBase public HelpWriterTests() { - var sp = ConfigureServices(services => services.AddModule(new GitVersionAppModule())); + var sp = ConfigureServices(services => services.AddModule(new GitVersionAppModule(useLegacyParser: true))); this.helpWriter = sp.GetRequiredService(); } @@ -18,35 +18,35 @@ public void AllArgsAreInHelp() { var lookup = new Dictionary { - { nameof(Arguments.IsHelp), "/?" }, - { nameof(Arguments.IsVersion), "/version" }, + { nameof(Arguments.IsHelp), "--help" }, + { nameof(Arguments.IsVersion), "--version" }, - { nameof(Arguments.TargetUrl), "/url" }, - { nameof(Arguments.TargetBranch), "/b" }, - { nameof(Arguments.ClonePath), "/dynamicRepoLocation" }, - { nameof(Arguments.CommitId), "/c" }, + { nameof(Arguments.TargetUrl), "--url" }, + { nameof(Arguments.TargetBranch), "--branch" }, + { nameof(Arguments.ClonePath), "--dynamic-repo-location" }, + { nameof(Arguments.CommitId), "--commit" }, - { nameof(Arguments.Diag) , "/diag" }, - { nameof(Arguments.LogFilePath) , "/l" }, - { "verbosity", "/verbosity" }, - { nameof(Arguments.Output) , "/output" }, - { nameof(Arguments.OutputFile) , "/outputfile" }, - { nameof(Arguments.ShowVariable), "/showvariable" }, - { nameof(Arguments.Format), "/format" }, + { nameof(Arguments.Diag), "--diagnose" }, + { nameof(Arguments.LogFilePath), "--log-file" }, + { "verbosity", "--verbosity" }, + { nameof(Arguments.Output), "--output" }, + { nameof(Arguments.OutputFile), "--output-file" }, + { nameof(Arguments.ShowVariable), "--show-variable" }, + { nameof(Arguments.Format), "--format" }, - { nameof(Arguments.UpdateWixVersionFile), "/updatewixversionfile" }, - { nameof(Arguments.UpdateProjectFiles), "/updateprojectfiles" }, - { nameof(Arguments.UpdateAssemblyInfo), "/updateassemblyinfo" }, - { nameof(Arguments.EnsureAssemblyInfo), "/ensureassemblyinfo" }, + { nameof(Arguments.UpdateWixVersionFile), "--update-wix-version-file" }, + { nameof(Arguments.UpdateProjectFiles), "--update-project-files" }, + { nameof(Arguments.UpdateAssemblyInfo), "--update-assembly-info" }, + { nameof(Arguments.EnsureAssemblyInfo), "--ensure-assembly-info" }, - { nameof(Arguments.ConfigurationFile), "/config" }, - { nameof(Arguments.ShowConfiguration), "/showconfig" }, - { nameof(Arguments.OverrideConfiguration), "/overrideconfig" }, + { nameof(Arguments.ConfigurationFile), "--config" }, + { nameof(Arguments.ShowConfiguration), "--show-config" }, + { nameof(Arguments.OverrideConfiguration), "--override-config" }, - { nameof(Arguments.NoCache), "/nocache" }, - { nameof(Arguments.NoFetch), "/nofetch" }, - { nameof(Arguments.NoNormalize), "/nonormalize" }, - { nameof(Arguments.AllowShallow), "/allowshallow" } + { nameof(Arguments.NoCache), "--no-cache" }, + { nameof(Arguments.NoFetch), "--no-fetch" }, + { nameof(Arguments.NoNormalize), "--no-normalize" }, + { nameof(Arguments.AllowShallow), "--allow-shallow" } }; var helpText = string.Empty; @@ -69,6 +69,8 @@ private static bool IsNotInHelp(Dictionary lookup, string proper if (lookup.TryGetValue(propertyName, out var value)) return !helpText.Contains(value); - return !helpText.Contains("/" + propertyName.ToLower()); + // Fallback: convert PascalCase to kebab-case and check for --option + var kebab = System.Text.RegularExpressions.Regex.Replace(propertyName, "(? services.AddModule(new GitVersionAppModule(useLegacyParser: true))); + this.environment = sp.GetRequiredService(); + this.argumentParser = sp.GetRequiredService(); + this.fileSystem = sp.GetRequiredService(); + } + + [Test] + public void EmptyMeansUseCurrentDirectory() + { + var arguments = this.argumentParser.ParseArguments(""); + arguments.TargetPath.ShouldBe(SysEnv.CurrentDirectory); + arguments.LogFilePath.ShouldBe(null); + arguments.IsHelp.ShouldBe(false); + } + + [Test] + public void SingleMeansUseAsTargetDirectory() + { + var arguments = this.argumentParser.ParseArguments("path"); + arguments.TargetPath.ShouldBe("path"); + arguments.LogFilePath.ShouldBe(null); + arguments.IsHelp.ShouldBe(false); + } + + [Test] + public void NoPathAndLogfileShouldUseCurrentDirectoryTargetDirectory() + { + var arguments = this.argumentParser.ParseArguments("-l logFilePath"); + arguments.TargetPath.ShouldBe(SysEnv.CurrentDirectory); + arguments.LogFilePath.ShouldBe("logFilePath"); + arguments.IsHelp.ShouldBe(false); + } + + [Test] + public void HelpSwitchTest() + { + var arguments = this.argumentParser.ParseArguments("-h"); + Assert.Multiple(() => + { + Assert.That(arguments.TargetPath, Is.Null); + Assert.That(arguments.LogFilePath, Is.Null); + }); + arguments.IsHelp.ShouldBe(true); + } + + [Test] + public void VersionSwitchTest() + { + var arguments = this.argumentParser.ParseArguments("-version"); + Assert.Multiple(() => + { + Assert.That(arguments.TargetPath, Is.Null); + Assert.That(arguments.LogFilePath, Is.Null); + }); + arguments.IsVersion.ShouldBe(true); + } + + [Test] + public void TargetDirectoryAndLogFilePathCanBeParsed() + { + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath -l logFilePath"); + arguments.TargetPath.ShouldBe("targetDirectoryPath"); + arguments.LogFilePath.ShouldBe("logFilePath"); + arguments.IsHelp.ShouldBe(false); + } + + [Test] + public void UsernameAndPasswordCanBeParsed() + { + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath -u [username] -p [password]"); + arguments.TargetPath.ShouldBe("targetDirectoryPath"); + arguments.Authentication.Username.ShouldBe("[username]"); + arguments.Authentication.Password.ShouldBe("[password]"); + arguments.IsHelp.ShouldBe(false); + } + + [Test] + public void UnknownOutputShouldThrow() + { + var exception = Assert.Throws(() => this.argumentParser.ParseArguments("targetDirectoryPath -output invalid_value")); + exception.ShouldNotBeNull(); + exception.Message.ShouldBe("Value 'invalid_value' cannot be parsed as output type, please use 'json', 'file', 'buildserver' or 'dotenv'"); + } + + [Test] + public void OutputDefaultsToJson() + { + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath"); + arguments.Output.ShouldContain(OutputType.Json); + arguments.Output.ShouldNotContain(OutputType.BuildServer); + arguments.Output.ShouldNotContain(OutputType.File); + } + + [Test] + public void OutputJsonCanBeParsed() + { + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath -output json"); + arguments.Output.ShouldContain(OutputType.Json); + arguments.Output.ShouldNotContain(OutputType.BuildServer); + arguments.Output.ShouldNotContain(OutputType.File); + } + + [Test] + public void MultipleOutputJsonCanBeParsed() + { + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath -output json -output json"); + arguments.Output.ShouldContain(OutputType.Json); + arguments.Output.ShouldNotContain(OutputType.BuildServer); + arguments.Output.ShouldNotContain(OutputType.File); + } + + [Test] + public void OutputBuildserverCanBeParsed() + { + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath -output buildserver"); + arguments.Output.ShouldContain(OutputType.BuildServer); + arguments.Output.ShouldNotContain(OutputType.Json); + arguments.Output.ShouldNotContain(OutputType.File); + } + + [Test] + public void MultipleOutputBuildserverCanBeParsed() + { + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath -output buildserver -output buildserver"); + arguments.Output.ShouldContain(OutputType.BuildServer); + arguments.Output.ShouldNotContain(OutputType.Json); + arguments.Output.ShouldNotContain(OutputType.File); + } + + [Test] + public void OutputFileCanBeParsed() + { + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath -output file"); + arguments.Output.ShouldContain(OutputType.File); + arguments.Output.ShouldNotContain(OutputType.BuildServer); + arguments.Output.ShouldNotContain(OutputType.Json); + } + + [Test] + public void MultipleOutputFileCanBeParsed() + { + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath -output file -output file"); + arguments.Output.ShouldContain(OutputType.File); + arguments.Output.ShouldNotContain(OutputType.BuildServer); + arguments.Output.ShouldNotContain(OutputType.Json); + } + + [Test] + public void OutputBuildserverAndJsonCanBeParsed() + { + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath -output buildserver -output json"); + arguments.Output.ShouldContain(OutputType.BuildServer); + arguments.Output.ShouldContain(OutputType.Json); + arguments.Output.ShouldNotContain(OutputType.File); + } + + [Test] + public void OutputBuildserverAndJsonAndFileCanBeParsed() + { + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath -output buildserver -output json -output file"); + arguments.Output.ShouldContain(OutputType.BuildServer); + arguments.Output.ShouldContain(OutputType.Json); + arguments.Output.ShouldContain(OutputType.File); + } + + [Test] + public void MultipleArgsAndFlag() + { + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath -output buildserver -updateAssemblyInfo"); + arguments.Output.ShouldContain(OutputType.BuildServer); + } + + [TestCase("-output file", "GitVersion.json")] + [TestCase("-output file -outputfile version.json", "version.json")] + public void OutputFileArgumentCanBeParsed(string args, string outputFile) + { + var arguments = this.argumentParser.ParseArguments(args); + + arguments.Output.ShouldContain(OutputType.File); + arguments.OutputFile.ShouldBe(outputFile); + } + + [Test] + public void UrlAndBranchNameCanBeParsed() + { + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath -url https://github.com/Particular/GitVersion.git -b someBranch"); + arguments.TargetPath.ShouldBe("targetDirectoryPath"); + arguments.TargetUrl.ShouldBe("https://github.com/Particular/GitVersion.git"); + arguments.TargetBranch.ShouldBe("someBranch"); + arguments.IsHelp.ShouldBe(false); + } + + [Test] + public void WrongNumberOfArgumentsShouldThrow() + { + var exception = Assert.Throws(() => this.argumentParser.ParseArguments("targetDirectoryPath -l logFilePath extraArg")); + exception.ShouldNotBeNull(); + exception.Message.ShouldBe("Could not parse command line parameter 'extraArg'."); + } + + [TestCase("targetDirectoryPath -x logFilePath")] + [TestCase("/invalid-argument")] + public void UnknownArgumentsShouldThrow(string arguments) + { + var exception = Assert.Throws(() => this.argumentParser.ParseArguments(arguments)); + exception.ShouldNotBeNull(); + exception.Message.ShouldStartWith("Could not parse command line parameter"); + } + + [TestCase("-updateAssemblyInfo true")] + [TestCase("-updateAssemblyInfo 1")] + [TestCase("-updateAssemblyInfo")] + [TestCase("-updateAssemblyInfo assemblyInfo.cs")] + [TestCase("-updateAssemblyInfo assemblyInfo.cs -ensureassemblyinfo")] + [TestCase("-updateAssemblyInfo assemblyInfo.cs otherAssemblyInfo.cs")] + [TestCase("-updateAssemblyInfo Assembly.cs Assembly.cs -ensureassemblyinfo")] + public void UpdateAssemblyInfoTrue(string command) + { + var arguments = this.argumentParser.ParseArguments(command); + arguments.UpdateAssemblyInfo.ShouldBe(true); + } + + [TestCase("-updateProjectFiles assemblyInfo.csproj")] + [TestCase("-updateProjectFiles assemblyInfo.csproj")] + [TestCase("-updateProjectFiles assemblyInfo.csproj otherAssemblyInfo.fsproj")] + [TestCase("-updateProjectFiles")] + public void UpdateProjectTrue(string command) + { + var arguments = this.argumentParser.ParseArguments(command); + arguments.UpdateProjectFiles.ShouldBe(true); + } + + [TestCase("-updateAssemblyInfo false")] + [TestCase("-updateAssemblyInfo 0")] + public void UpdateAssemblyInfoFalse(string command) + { + var arguments = this.argumentParser.ParseArguments(command); + arguments.UpdateAssemblyInfo.ShouldBe(false); + } + + [TestCase("-updateAssemblyInfo Assembly.cs Assembly1.cs -ensureassemblyinfo")] + public void CreateMultipleAssemblyInfoProtected(string command) + { + var exception = Assert.Throws(() => this.argumentParser.ParseArguments(command)); + exception.ShouldNotBeNull(); + exception.Message.ShouldBe("Can't specify multiple assembly info files when using /ensureassemblyinfo switch, either use a single assembly info file or do not specify /ensureassemblyinfo and create assembly info files manually"); + } + + [TestCase("-updateProjectFiles Assembly.csproj -ensureassemblyinfo")] + public void UpdateProjectInfoWithEnsureAssemblyInfoProtected(string command) + { + var exception = Assert.Throws(() => this.argumentParser.ParseArguments(command)); + exception.ShouldNotBeNull(); + exception.Message.ShouldBe("Cannot specify -ensureassemblyinfo with updateprojectfiles: please ensure your project file exists before attempting to update it"); + } + + [Test] + public void UpdateAssemblyInfoWithFilename() + { + using var repo = new EmptyRepositoryFixture(); + + var assemblyFile = FileSystemHelper.Path.Combine(repo.RepositoryPath, "CommonAssemblyInfo.cs"); + using var file = this.fileSystem.File.Create(assemblyFile); + + var arguments = this.argumentParser.ParseArguments($"-targetpath {repo.RepositoryPath} -updateAssemblyInfo CommonAssemblyInfo.cs"); + arguments.UpdateAssemblyInfo.ShouldBe(true); + arguments.UpdateAssemblyInfoFileName.Count.ShouldBe(1); + arguments.UpdateAssemblyInfoFileName.ShouldContain(x => FileSystemHelper.Path.GetFileName(x).Equals("CommonAssemblyInfo.cs")); + } + + [Test] + public void UpdateAssemblyInfoWithMultipleFilenames() + { + using var repo = new EmptyRepositoryFixture(); + + var assemblyFile1 = FileSystemHelper.Path.Combine(repo.RepositoryPath, "CommonAssemblyInfo.cs"); + using var file = this.fileSystem.File.Create(assemblyFile1); + + var assemblyFile2 = FileSystemHelper.Path.Combine(repo.RepositoryPath, "VersionAssemblyInfo.cs"); + using var file2 = this.fileSystem.File.Create(assemblyFile2); + + var arguments = this.argumentParser.ParseArguments($"-targetpath {repo.RepositoryPath} -updateAssemblyInfo CommonAssemblyInfo.cs VersionAssemblyInfo.cs"); + arguments.UpdateAssemblyInfo.ShouldBe(true); + arguments.UpdateAssemblyInfoFileName.Count.ShouldBe(2); + arguments.UpdateAssemblyInfoFileName.ShouldContain(x => FileSystemHelper.Path.GetFileName(x).Equals("CommonAssemblyInfo.cs")); + arguments.UpdateAssemblyInfoFileName.ShouldContain(x => FileSystemHelper.Path.GetFileName(x).Equals("VersionAssemblyInfo.cs")); + } + + [Test] + public void UpdateProjectFilesWithMultipleFilenames() + { + using var repo = new EmptyRepositoryFixture(); + + var assemblyFile1 = FileSystemHelper.Path.Combine(repo.RepositoryPath, "CommonAssemblyInfo.csproj"); + using var file = this.fileSystem.File.Create(assemblyFile1); + + var assemblyFile2 = FileSystemHelper.Path.Combine(repo.RepositoryPath, "VersionAssemblyInfo.csproj"); + using var file2 = this.fileSystem.File.Create(assemblyFile2); + + var arguments = this.argumentParser.ParseArguments($"-targetpath {repo.RepositoryPath} -updateProjectFiles CommonAssemblyInfo.csproj VersionAssemblyInfo.csproj"); + arguments.UpdateProjectFiles.ShouldBe(true); + arguments.UpdateAssemblyInfoFileName.Count.ShouldBe(2); + arguments.UpdateAssemblyInfoFileName.ShouldContain(x => FileSystemHelper.Path.GetFileName(x).Equals("CommonAssemblyInfo.csproj")); + arguments.UpdateAssemblyInfoFileName.ShouldContain(x => FileSystemHelper.Path.GetFileName(x).Equals("VersionAssemblyInfo.csproj")); + } + + [Test] + public void UpdateAssemblyInfoWithMultipleFilenamesMatchingGlobbing() + { + using var repo = new EmptyRepositoryFixture(); + + var assemblyFile1 = FileSystemHelper.Path.Combine(repo.RepositoryPath, "CommonAssemblyInfo.cs"); + using var file = this.fileSystem.File.Create(assemblyFile1); + + var assemblyFile2 = FileSystemHelper.Path.Combine(repo.RepositoryPath, "VersionAssemblyInfo.cs"); + using var file2 = this.fileSystem.File.Create(assemblyFile2); + + var subdir = FileSystemHelper.Path.Combine(repo.RepositoryPath, "subdir"); + + this.fileSystem.Directory.CreateDirectory(subdir); + var assemblyFile3 = FileSystemHelper.Path.Combine(subdir, "LocalAssemblyInfo.cs"); + using var file3 = this.fileSystem.File.Create(assemblyFile3); + + var arguments = this.argumentParser.ParseArguments($"-targetpath {repo.RepositoryPath} -updateAssemblyInfo **/*AssemblyInfo.cs"); + arguments.UpdateAssemblyInfo.ShouldBe(true); + arguments.UpdateAssemblyInfoFileName.Count.ShouldBe(3); + arguments.UpdateAssemblyInfoFileName.ShouldContain(x => FileSystemHelper.Path.GetFileName(x).Equals("CommonAssemblyInfo.cs")); + arguments.UpdateAssemblyInfoFileName.ShouldContain(x => FileSystemHelper.Path.GetFileName(x).Equals("VersionAssemblyInfo.cs")); + arguments.UpdateAssemblyInfoFileName.ShouldContain(x => FileSystemHelper.Path.GetFileName(x).Equals("LocalAssemblyInfo.cs")); + } + + [Test] + public void UpdateAssemblyInfoWithRelativeFilename() + { + using var repo = new EmptyRepositoryFixture(); + + var assemblyFile = FileSystemHelper.Path.Combine(repo.RepositoryPath, "CommonAssemblyInfo.cs"); + using var file = this.fileSystem.File.Create(assemblyFile); + + var targetPath = FileSystemHelper.Path.Combine(repo.RepositoryPath, "subdir1", "subdir2"); + this.fileSystem.Directory.CreateDirectory(targetPath); + + var arguments = this.argumentParser.ParseArguments($@"-targetpath {targetPath} -updateAssemblyInfo ..\..\CommonAssemblyInfo.cs"); + arguments.UpdateAssemblyInfo.ShouldBe(true); + arguments.UpdateAssemblyInfoFileName.Count.ShouldBe(1); + arguments.UpdateAssemblyInfoFileName.ShouldContain(x => FileSystemHelper.Path.GetFileName(x).Equals("CommonAssemblyInfo.cs")); + } + + [Test] + public void OverrideconfigWithNoOptions() + { + var arguments = this.argumentParser.ParseArguments("/overrideconfig"); + arguments.OverrideConfiguration.ShouldBeNull(); + } + + [TestCaseSource(nameof(OverrideconfigWithInvalidOptionTestData))] + public string OverrideconfigWithInvalidOption(string options) + { + var exception = Assert.Throws(() => this.argumentParser.ParseArguments($"/overrideconfig {options}")); + exception.ShouldNotBeNull(); + return exception.Message; + } + + private static IEnumerable OverrideconfigWithInvalidOptionTestData() + { + yield return new TestCaseData("tag-prefix=sample=asdf") + { + ExpectedResult = "Could not parse /overrideconfig option: tag-prefix=sample=asdf. Ensure it is in format 'key=value'." + }; + yield return new TestCaseData("unknown-option=25") + { + ExpectedResult = "Could not parse /overrideconfig option: unknown-option=25. Unsupported 'key'." + }; + } + + [TestCaseSource(nameof(OverrideConfigWithSingleOptionTestData))] + public void OverrideConfigWithSingleOptions(string options, IGitVersionConfiguration expected) + { + var arguments = this.argumentParser.ParseArguments($"/overrideconfig {options}"); + + ConfigurationHelper configurationHelper = new(arguments.OverrideConfiguration); + configurationHelper.Configuration.ShouldBeEquivalentTo(expected); + } + + private static IEnumerable OverrideConfigWithSingleOptionTestData() + { + yield return new TestCaseData( + "assembly-versioning-scheme=MajorMinor", + new GitVersionConfiguration + { + AssemblyVersioningScheme = AssemblyVersioningScheme.MajorMinor + } + ); + yield return new TestCaseData( + "assembly-file-versioning-scheme=\"MajorMinorPatch\"", + new GitVersionConfiguration + { + AssemblyFileVersioningScheme = AssemblyFileVersioningScheme.MajorMinorPatch + } + ); + yield return new TestCaseData( + "assembly-informational-format=\"{Major}.{Minor}.{Patch}.{env:CI_JOB_ID ?? 0}\"", + new GitVersionConfiguration + { + AssemblyInformationalFormat = "{Major}.{Minor}.{Patch}.{env:CI_JOB_ID ?? 0}" + } + ); + yield return new TestCaseData( + "assembly-versioning-format=\"{Major}.{Minor}.{Patch}.{env:CI_JOB_ID ?? 0}\"", + new GitVersionConfiguration + { + AssemblyVersioningFormat = "{Major}.{Minor}.{Patch}.{env:CI_JOB_ID ?? 0}" + } + ); + yield return new TestCaseData( + "assembly-file-versioning-format=\"{Major}.{Minor}.{Patch}.{env:CI_JOB_ID ?? 0}\"", + new GitVersionConfiguration + { + AssemblyFileVersioningFormat = "{Major}.{Minor}.{Patch}.{env:CI_JOB_ID ?? 0}" + } + ); + yield return new TestCaseData( + "mode=ContinuousDelivery", + new GitVersionConfiguration + { + DeploymentMode = DeploymentMode.ContinuousDelivery + } + ); + yield return new TestCaseData( + "tag-prefix=sample", + new GitVersionConfiguration + { + TagPrefixPattern = "sample" + } + ); + yield return new TestCaseData( + "label=cd-label", + new GitVersionConfiguration + { + Label = "cd-label" + } + ); + yield return new TestCaseData( + "next-version=1", + new GitVersionConfiguration + { + NextVersion = "1" + } + ); + yield return new TestCaseData( + "major-version-bump-message=\"This is major version bump message.\"", + new GitVersionConfiguration + { + MajorVersionBumpMessage = "This is major version bump message." + } + ); + yield return new TestCaseData( + "minor-version-bump-message=\"This is minor version bump message.\"", + new GitVersionConfiguration + { + MinorVersionBumpMessage = "This is minor version bump message." + } + ); + yield return new TestCaseData( + "patch-version-bump-message=\"This is patch version bump message.\"", + new GitVersionConfiguration + { + PatchVersionBumpMessage = "This is patch version bump message." + } + ); + yield return new TestCaseData( + "no-bump-message=\"This is no bump message.\"", + new GitVersionConfiguration + { + NoBumpMessage = "This is no bump message." + } + ); + yield return new TestCaseData( + "tag-pre-release-weight=2", + new GitVersionConfiguration + { + TagPreReleaseWeight = 2 + } + ); + yield return new TestCaseData( + "commit-message-incrementing=MergeMessageOnly", + new GitVersionConfiguration + { + CommitMessageIncrementing = CommitMessageIncrementMode.MergeMessageOnly + } + ); + yield return new TestCaseData( + "increment=Minor", + new GitVersionConfiguration + { + Increment = IncrementStrategy.Minor + } + ); + yield return new TestCaseData( + "commit-date-format=\"MM/dd/yyyy h:mm tt\"", + new GitVersionConfiguration + { + CommitDateFormat = "MM/dd/yyyy h:mm tt" + } + ); + yield return new TestCaseData( + "update-build-number=true", + new GitVersionConfiguration + { + UpdateBuildNumber = true + } + ); + yield return new TestCaseData( + "strategies=[\"None\",\"Mainline\"]", + new GitVersionConfiguration + { + VersionStrategies = [VersionStrategies.None, VersionStrategies.Mainline] + } + ); + } + + [TestCaseSource(nameof(OverrideConfigWithMultipleOptionsTestData))] + public void OverrideConfigWithMultipleOptions(string options, IGitVersionConfiguration expected) + { + var arguments = this.argumentParser.ParseArguments(options); + ConfigurationHelper configurationHelper = new(arguments.OverrideConfiguration); + configurationHelper.Configuration.ShouldBeEquivalentTo(expected); + } + + private static IEnumerable OverrideConfigWithMultipleOptionsTestData() + { + yield return new TestCaseData( + "/overrideconfig tag-prefix=sample /overrideconfig assembly-versioning-scheme=MajorMinor", + new GitVersionConfiguration + { + TagPrefixPattern = "sample", + AssemblyVersioningScheme = AssemblyVersioningScheme.MajorMinor + } + ); + yield return new TestCaseData( + "/overrideconfig tag-prefix=sample /overrideconfig assembly-versioning-format=\"{Major}.{Minor}.{Patch}.{env:CI_JOB_ID ?? 0}\"", + new GitVersionConfiguration + { + TagPrefixPattern = "sample", + AssemblyVersioningFormat = "{Major}.{Minor}.{Patch}.{env:CI_JOB_ID ?? 0}" + } + ); + yield return new TestCaseData( + "/overrideconfig tag-prefix=sample /overrideconfig assembly-versioning-format=\"{Major}.{Minor}.{Patch}.{env:CI_JOB_ID ?? 0}\" /overrideconfig update-build-number=true /overrideconfig assembly-versioning-scheme=MajorMinorPatchTag /overrideconfig mode=ContinuousDelivery /overrideconfig tag-pre-release-weight=4", + new GitVersionConfiguration + { + TagPrefixPattern = "sample", + AssemblyVersioningFormat = "{Major}.{Minor}.{Patch}.{env:CI_JOB_ID ?? 0}", + UpdateBuildNumber = true, + AssemblyVersioningScheme = AssemblyVersioningScheme.MajorMinorPatchTag, + DeploymentMode = DeploymentMode.ContinuousDelivery, + TagPreReleaseWeight = 4 + } + ); + } + + [Test] + public void EnsureAssemblyInfoTrueWhenFound() + { + var arguments = this.argumentParser.ParseArguments("-ensureAssemblyInfo"); + arguments.EnsureAssemblyInfo.ShouldBe(true); + } + + [Test] + public void EnsureAssemblyInfoTrue() + { + var arguments = this.argumentParser.ParseArguments("-ensureAssemblyInfo true"); + arguments.EnsureAssemblyInfo.ShouldBe(true); + } + + [Test] + public void EnsureAssemblyInfoFalse() + { + var arguments = this.argumentParser.ParseArguments("-ensureAssemblyInfo false"); + arguments.EnsureAssemblyInfo.ShouldBe(false); + } + + [Test] + public void DynamicRepoLocation() + { + var arguments = this.argumentParser.ParseArguments("-dynamicRepoLocation /tmp/foo"); + arguments.ClonePath.ShouldBe("/tmp/foo"); + } + + [Test] + public void CanLogToConsole() + { + var arguments = this.argumentParser.ParseArguments("-l console"); + arguments.LogFilePath.ShouldBe("console"); + } + + [Test] + public void NofetchTrueWhenDefined() + { + var arguments = this.argumentParser.ParseArguments("-nofetch"); + arguments.NoFetch.ShouldBe(true); + } + + [Test] + public void NoNormalizeTrueWhenDefined() + { + var arguments = this.argumentParser.ParseArguments("-nonormalize"); + arguments.NoNormalize.ShouldBe(true); + } + + [Test] + public void AllowshallowTrueWhenDefined() + { + var arguments = this.argumentParser.ParseArguments("-allowshallow"); + arguments.AllowShallow.ShouldBe(true); + } + + [Test] + public void DiagTrueWhenDefined() + { + var arguments = this.argumentParser.ParseArguments("-diag"); + arguments.Diag.ShouldBe(true); + } + + [Test] + public void DiagAndLogToConsoleIsNotIgnored() + { + var arguments = this.argumentParser.ParseArguments("-diag -l console"); + arguments.Diag.ShouldBe(true); + arguments.LogFilePath.ShouldBe("console"); + } + + [Test] + public void OtherArgumentsCanBeParsedBeforeNofetch() + { + var arguments = this.argumentParser.ParseArguments("targetpath -nofetch "); + arguments.TargetPath.ShouldBe("targetpath"); + arguments.NoFetch.ShouldBe(true); + } + + [Test] + public void OtherArgumentsCanBeParsedBeforeNonormalize() + { + var arguments = this.argumentParser.ParseArguments("targetpath -nonormalize"); + arguments.TargetPath.ShouldBe("targetpath"); + arguments.NoNormalize.ShouldBe(true); + } + + [Test] + public void OtherArgumentsCanBeParsedBeforeNocache() + { + var arguments = this.argumentParser.ParseArguments("targetpath -nocache"); + arguments.TargetPath.ShouldBe("targetpath"); + arguments.NoCache.ShouldBe(true); + } + + [Test] + public void OtherArgumentsCanBeParsedBeforeAllowshallow() + { + var arguments = this.argumentParser.ParseArguments("targetpath -allowshallow"); + arguments.TargetPath.ShouldBe("targetpath"); + arguments.AllowShallow.ShouldBe(true); + } + + [TestCase("-nofetch -nonormalize -nocache -allowshallow")] + [TestCase("-nofetch -nonormalize -allowshallow -nocache")] + [TestCase("-nofetch -nocache -nonormalize -allowshallow")] + [TestCase("-nofetch -nocache -allowshallow -nonormalize")] + [TestCase("-nofetch -allowshallow -nonormalize -nocache")] + [TestCase("-nofetch -allowshallow -nocache -nonormalize")] + [TestCase("-nonormalize -nofetch -nocache -allowshallow")] + [TestCase("-nonormalize -nofetch -allowshallow -nocache")] + [TestCase("-nonormalize -nocache -nofetch -allowshallow")] + [TestCase("-nonormalize -nocache -allowshallow -nofetch")] + [TestCase("-nonormalize -allowshallow -nofetch -nocache")] + [TestCase("-nonormalize -allowshallow -nocache -nofetch")] + [TestCase("-nocache -nofetch -nonormalize -allowshallow")] + [TestCase("-nocache -nofetch -allowshallow -nonormalize")] + [TestCase("-nocache -nonormalize -nofetch -allowshallow")] + [TestCase("-nocache -nonormalize -allowshallow -nofetch")] + [TestCase("-nocache -allowshallow -nofetch -nonormalize")] + [TestCase("-nocache -allowshallow -nonormalize -nofetch")] + [TestCase("-allowshallow -nofetch -nonormalize -nocache")] + [TestCase("-allowshallow -nofetch -nocache -nonormalize")] + [TestCase("-allowshallow -nonormalize -nofetch -nocache")] + [TestCase("-allowshallow -nonormalize -nocache -nofetch")] + [TestCase("-allowshallow -nocache -nofetch -nonormalize")] + [TestCase("-allowshallow -nocache -nonormalize -nofetch")] + public void SeveralSwitchesCanBeParsed(string commandLineArgs) + { + var arguments = this.argumentParser.ParseArguments(commandLineArgs); + arguments.NoCache.ShouldBe(true); + arguments.NoNormalize.ShouldBe(true); + arguments.NoFetch.ShouldBe(true); + arguments.AllowShallow.ShouldBe(true); + } + + [Test] + public void LogPathCanContainForwardSlash() + { + var arguments = this.argumentParser.ParseArguments("-l /some/path"); + arguments.LogFilePath.ShouldBe("/some/path"); + } + + [Test] + public void BooleanArgumentHandling() + { + var arguments = this.argumentParser.ParseArguments("/nofetch /updateassemblyinfo true"); + arguments.NoFetch.ShouldBe(true); + arguments.UpdateAssemblyInfo.ShouldBe(true); + } + + [Test] + public void NocacheTrueWhenDefined() + { + var arguments = this.argumentParser.ParseArguments("-nocache"); + arguments.NoCache.ShouldBe(true); + } + + [TestCase("x", true, Verbosity.Normal)] + [TestCase("diagnostic", false, Verbosity.Diagnostic)] + [TestCase("Minimal", false, Verbosity.Minimal)] + [TestCase("NORMAL", false, Verbosity.Normal)] + [TestCase("quiet", false, Verbosity.Quiet)] + [TestCase("Verbose", false, Verbosity.Verbose)] + public void CheckVerbosityParsing(string command, bool shouldThrow, Verbosity expectedVerbosity) + { + if (shouldThrow) + { + Assert.Throws(() => LegacyArgumentParser.ParseVerbosity(command)); + } + else + { + var verbosity = LegacyArgumentParser.ParseVerbosity(command); + verbosity.ShouldBe(expectedVerbosity); + } + } + + [Test] + public void EmptyArgumentsRemoteUsernameDefinedSetsUsername() + { + this.environment.SetEnvironmentVariable("GITVERSION_REMOTE_USERNAME", "value"); + var arguments = this.argumentParser.ParseArguments(string.Empty); + arguments.Authentication.Username.ShouldBe("value"); + } + + [Test] + public void EmptyArgumentsRemotePasswordDefinedSetsPassword() + { + this.environment.SetEnvironmentVariable("GITVERSION_REMOTE_PASSWORD", "value"); + var arguments = this.argumentParser.ParseArguments(string.Empty); + arguments.Authentication.Password.ShouldBe("value"); + } + + [Test] + public void ArbitraryArgumentsRemoteUsernameDefinedSetsUsername() + { + this.environment.SetEnvironmentVariable("GITVERSION_REMOTE_USERNAME", "value"); + var arguments = this.argumentParser.ParseArguments("-nocache"); + arguments.Authentication.Username.ShouldBe("value"); + } + + [Test] + public void ArbitraryArgumentsRemotePasswordDefinedSetsPassword() + { + this.environment.SetEnvironmentVariable("GITVERSION_REMOTE_PASSWORD", "value"); + var arguments = this.argumentParser.ParseArguments("-nocache"); + arguments.Authentication.Password.ShouldBe("value"); + } + + [Test] + public void EnsureShowVariableIsSet() + { + var arguments = this.argumentParser.ParseArguments("-showvariable SemVer"); + arguments.ShowVariable.ShouldBe("SemVer"); + } + + [Test] + public void EnsureFormatIsSet() + { + var arguments = this.argumentParser.ParseArguments("-format {Major}.{Minor}.{Patch}"); + arguments.Format.ShouldBe("{Major}.{Minor}.{Patch}"); + } + + [TestCase("custom-config.yaml")] + [TestCase("/tmp/custom-config.yaml")] + public void ThrowIfConfigurationFileDoesNotExist(string configFile) => + Should.Throw(() => _ = this.argumentParser.ParseArguments($"-config {configFile}")); + + [Test] + public void EnsureConfigurationFileIsSet() + { + var configFile = FileSystemHelper.Path.GetTempPath() + Guid.NewGuid() + ".yaml"; + this.fileSystem.File.WriteAllText(configFile, "next-version: 1.0.0"); + var arguments = this.argumentParser.ParseArguments($"-config {configFile}"); + arguments.ConfigurationFile.ShouldBe(configFile); + this.fileSystem.File.Delete(configFile); + } +} diff --git a/src/GitVersion.App.Tests/VersionWriterTests.cs b/src/GitVersion.App.Tests/VersionWriterTests.cs index 4971b05269..27244b312a 100644 --- a/src/GitVersion.App.Tests/VersionWriterTests.cs +++ b/src/GitVersion.App.Tests/VersionWriterTests.cs @@ -12,7 +12,7 @@ public class VersionWriterTests : TestBase public VersionWriterTests() { - var sp = ConfigureServices(services => services.AddModule(new GitVersionAppModule())); + var sp = ConfigureServices(services => services.AddModule(new GitVersionAppModule(useLegacyParser: true))); this.versionWriter = sp.GetRequiredService(); } From b22670c93321bc56031d9eb7f224ada0cc7842de Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Mon, 2 Mar 2026 23:23:12 +0100 Subject: [PATCH 07/24] test(app): update integration tests to use POSIX CLI syntax --- .../ExecCmdLineArgumentTest.cs | 20 +++++++++---------- .../Helpers/ArgumentBuilder.cs | 4 ++-- .../Helpers/ProgramFixture.cs | 2 +- .../JsonOutputOnBuildServerTest.cs | 7 ++++--- .../UpdateWixVersionFileTests.cs | 6 +++--- 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/GitVersion.App.Tests/ExecCmdLineArgumentTest.cs b/src/GitVersion.App.Tests/ExecCmdLineArgumentTest.cs index dd39e7b5b8..e8e06cf949 100644 --- a/src/GitVersion.App.Tests/ExecCmdLineArgumentTest.cs +++ b/src/GitVersion.App.Tests/ExecCmdLineArgumentTest.cs @@ -11,11 +11,11 @@ public class ExecCmdLineArgumentTest public void InvalidArgumentsExitCodeShouldNotBeZero() { using var fixture = new EmptyRepositoryFixture(); - var result = GitVersionHelper.ExecuteIn(fixture.RepositoryPath, arguments: " /invalid-argument"); + var result = GitVersionHelper.ExecuteIn(fixture.RepositoryPath, arguments: " --invalid-argument"); result.ExitCode.ShouldNotBe(0); result.Output.ShouldNotBeNull(); - result.Output.ShouldContain("Could not parse command line parameter '/invalid-argument'"); + result.Output.ShouldContain("Could not parse command line parameter '--invalid-argument'"); } [Test] @@ -26,7 +26,7 @@ public void LogPathContainsForwardSlash() fixture.MakeACommit(); var result = GitVersionHelper.ExecuteIn(fixture.RepositoryPath, - """ /l "/tmp/path" """, false); + """ --log-file "/tmp/path" """, false); result.ExitCode.ShouldBe(0); result.Output.ShouldNotBeNull(); @@ -38,8 +38,8 @@ public void LogPathContainsForwardSlash() [Theory] [TestCase("", "INFO")] - [TestCase("-verbosity NORMAL", "INFO")] - [TestCase("-verbosity quiet", "")] + [TestCase("--verbosity NORMAL", "INFO")] + [TestCase("--verbosity quiet", "")] public void CheckBuildServerVerbosityConsole(string verbosityArg, string expectedOutput) { using var fixture = new EmptyRepositoryFixture(); @@ -47,7 +47,7 @@ public void CheckBuildServerVerbosityConsole(string verbosityArg, string expecte fixture.MakeACommit(); var result = GitVersionHelper.ExecuteIn(fixture.RepositoryPath, - $""" {verbosityArg} -output buildserver /l "/tmp/path" """, false); + $""" {verbosityArg} --output buildserver --log-file "/tmp/path" """, false); result.ExitCode.ShouldBe(0); result.Output.ShouldNotBeNull(); @@ -65,8 +65,8 @@ public void WorkingDirectoryWithoutGitFolderFailsWithInformativeMessage() result.Output.ShouldContain("Cannot find the .git directory"); } - [TestCase(" -help")] - [TestCase(" -version")] + [TestCase(" --help")] + [TestCase(" --version")] public void WorkingDirectoryWithoutGitFolderDoesNotFailForVersionAndHelp(string argument) { var result = GitVersionHelper.ExecuteIn(workingDirectory: null, arguments: argument); @@ -80,7 +80,7 @@ public void WorkingDirectoryWithoutCommitsFailsWithInformativeMessage() { using var fixture = new EmptyRepositoryFixture(); - var result = GitVersionHelper.ExecuteIn(fixture.RepositoryPath, " /l console", false); + var result = GitVersionHelper.ExecuteIn(fixture.RepositoryPath, " --log-file console", false); result.ExitCode.ShouldNotBe(0); result.Output.ShouldNotBeNull(); @@ -94,7 +94,7 @@ public void WorkingDirectoryDoesNotExistFailsWithInformativeMessage() var executable = ExecutableHelper.GetDotNetExecutable(); var output = new StringBuilder(); - var args = ExecutableHelper.GetExecutableArgs($" /targetpath {workingDirectory} "); + var args = ExecutableHelper.GetExecutableArgs($" --target-path {workingDirectory} "); var exitCode = ProcessHelper.Run( s => output.AppendLine(s), diff --git a/src/GitVersion.App.Tests/Helpers/ArgumentBuilder.cs b/src/GitVersion.App.Tests/Helpers/ArgumentBuilder.cs index 38678c4987..48bc76bffe 100644 --- a/src/GitVersion.App.Tests/Helpers/ArgumentBuilder.cs +++ b/src/GitVersion.App.Tests/Helpers/ArgumentBuilder.cs @@ -14,12 +14,12 @@ public override string ToString() if (!this.WorkingDirectory.IsNullOrWhiteSpace()) { - arguments.Append(" /targetpath \"").Append(this.WorkingDirectory).Append('\"'); + arguments.Append(" --target-path \"").Append(this.WorkingDirectory).Append('\"'); } if (!this.LogFile.IsNullOrWhiteSpace()) { - arguments.Append(" /l \"").Append(this.LogFile).Append('\"'); + arguments.Append(" --log-file \"").Append(this.LogFile).Append('\"'); } arguments.Append(additionalArguments); diff --git a/src/GitVersion.App.Tests/Helpers/ProgramFixture.cs b/src/GitVersion.App.Tests/Helpers/ProgramFixture.cs index 52f070041f..c274e8b470 100644 --- a/src/GitVersion.App.Tests/Helpers/ProgramFixture.cs +++ b/src/GitVersion.App.Tests/Helpers/ProgramFixture.cs @@ -55,7 +55,7 @@ public async Task Run(params string[] args) { if (!this.workingDirectory.IsNullOrWhiteSpace()) { - args = ["-targetpath", this.workingDirectory, .. args]; + args = ["--target-path", this.workingDirectory, .. args]; } var builder = CliHost.CreateCliHostBuilder(args); diff --git a/src/GitVersion.App.Tests/JsonOutputOnBuildServerTest.cs b/src/GitVersion.App.Tests/JsonOutputOnBuildServerTest.cs index d9f8f1a3ec..5b724976d0 100644 --- a/src/GitVersion.App.Tests/JsonOutputOnBuildServerTest.cs +++ b/src/GitVersion.App.Tests/JsonOutputOnBuildServerTest.cs @@ -16,7 +16,7 @@ public void BeingOnBuildServerDoesntOverrideOutputJson() var env = new KeyValuePair(TeamCity.EnvironmentVariableName, "8.0.0"); - var result = GitVersionHelper.ExecuteIn(fixture.LocalRepositoryFixture.RepositoryPath, arguments: " /output json", environments: env); + var result = GitVersionHelper.ExecuteIn(fixture.LocalRepositoryFixture.RepositoryPath, arguments: " --output json", environments: env); result.ExitCode.ShouldBe(0); result.Output.ShouldStartWith("{"); @@ -32,7 +32,7 @@ public void BeingOnBuildServerWithOutputJsonDoesNotFail() var env = new KeyValuePair(TeamCity.EnvironmentVariableName, "8.0.0"); - var result = GitVersionHelper.ExecuteIn(fixture.LocalRepositoryFixture.RepositoryPath, arguments: " /output json /output buildserver", environments: env); + var result = GitVersionHelper.ExecuteIn(fixture.LocalRepositoryFixture.RepositoryPath, arguments: " --output json --output buildserver", environments: env); result.ExitCode.ShouldBe(0); const string expectedVersion = "0.0.1-5"; @@ -52,7 +52,8 @@ public void BeingOnBuildServerWithOutputJsonAndOutputFileDoesNotFail(string outp var env = new KeyValuePair(TeamCity.EnvironmentVariableName, "8.0.0"); - var result = GitVersionHelper.ExecuteIn(fixture.LocalRepositoryFixture.RepositoryPath, arguments: $" /output json /output buildserver /output file /outputfile {outputFile}", environments: env); + var outputFileArg = string.IsNullOrEmpty(outputFile) ? "" : $" --output-file {outputFile}"; + var result = GitVersionHelper.ExecuteIn(fixture.LocalRepositoryFixture.RepositoryPath, arguments: $" --output json --output buildserver --output file{outputFileArg}", environments: env); result.ExitCode.ShouldBe(0); const string expectedVersion = "0.0.1-5"; diff --git a/src/GitVersion.App.Tests/UpdateWixVersionFileTests.cs b/src/GitVersion.App.Tests/UpdateWixVersionFileTests.cs index f3ba06a794..f31cfcc472 100644 --- a/src/GitVersion.App.Tests/UpdateWixVersionFileTests.cs +++ b/src/GitVersion.App.Tests/UpdateWixVersionFileTests.cs @@ -20,7 +20,7 @@ public void WixVersionFileCreationTest() fixture.MakeATaggedCommit("1.2.3"); fixture.MakeACommit(); - GitVersionHelper.ExecuteIn(fixture.RepositoryPath, arguments: " /updatewixversionfile"); + GitVersionHelper.ExecuteIn(fixture.RepositoryPath, arguments: " --update-wix-version-file"); Assert.That(FileSystemHelper.File.Exists(FileSystemHelper.Path.Combine(fixture.RepositoryPath, this.wixVersionFileName)), Is.True); } @@ -34,7 +34,7 @@ public void WixVersionFileVarCountTest() GitVersionHelper.ExecuteIn(fixture.RepositoryPath, arguments: null); - GitVersionHelper.ExecuteIn(fixture.RepositoryPath, arguments: " /updatewixversionfile"); + GitVersionHelper.ExecuteIn(fixture.RepositoryPath, arguments: " --update-wix-version-file"); var gitVersionVarsInWix = GetGitVersionVarsInWixFile(FileSystemHelper.Path.Combine(fixture.RepositoryPath, this.wixVersionFileName)); var gitVersionVars = GitVersionVariables.AvailableVariables; @@ -53,7 +53,7 @@ public void WixVersionFileContentTest() var vars = gitVersionExecutionResults.OutputVariables; vars.ShouldNotBeNull(); - GitVersionHelper.ExecuteIn(fixture.RepositoryPath, arguments: " /updatewixversionfile"); + GitVersionHelper.ExecuteIn(fixture.RepositoryPath, arguments: " --update-wix-version-file"); var gitVersionVarsInWix = GetGitVersionVarsInWixFile(FileSystemHelper.Path.Combine(fixture.RepositoryPath, this.wixVersionFileName)); var gitVersionVars = GitVersionVariables.AvailableVariables; From 3eebf4ca53090054dbf679b999e729047872bd20 Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Mon, 2 Mar 2026 23:23:24 +0100 Subject: [PATCH 08/24] docs(cli): update all documentation to POSIX --long-name syntax --- docs/input/docs/learn/dynamic-repositories.md | 14 +-- .../build-servers/bitbucket-pipelines.md | 6 +- .../docs/reference/build-servers/continua.md | 2 +- .../docs/reference/build-servers/index.cshtml | 4 +- .../docs/reference/build-servers/jenkins.md | 4 +- .../docs/reference/build-servers/myget.md | 4 +- .../reference/build-servers/octopus-deploy.md | 4 +- .../docs/reference/build-servers/teamcity.md | 2 +- docs/input/docs/reference/configuration.md | 2 +- .../mdsource/configuration.source.md | 2 +- docs/input/docs/usage/cli/arguments.md | 103 +++++++++--------- docs/input/docs/usage/cli/assembly-patch.md | 22 ++-- docs/input/docs/usage/cli/output.md | 12 +- 13 files changed, 92 insertions(+), 89 deletions(-) diff --git a/docs/input/docs/learn/dynamic-repositories.md b/docs/input/docs/learn/dynamic-repositories.md index 848ac16391..de548bd9a0 100644 --- a/docs/input/docs/learn/dynamic-repositories.md +++ b/docs/input/docs/learn/dynamic-repositories.md @@ -31,15 +31,15 @@ will assume there is already a ".git" folder present, and it will use it. To tell GitVersion.exe to obtain the repository on the fly, you need to call `GitVersion.exe` with the following arguments: -* `/url [the url of your git repo]` -* `/u [authentication username]` -* `/p [authentication password]` -* `/b [branch name]` -* `/c [commit id]` +* `--url [the url of your git repo]` +* `--username [authentication username]` (or `-u`) +* `--password [authentication password]` (or `-p`) +* `--branch [branch name]` (or `-b`) +* `--commit [commit id]` -Please note that these arguments are described when calling `GitVersion.exe /?`. +Please note that these arguments are described when calling `GitVersion.exe --help`. -Also, be aware that if you don't specify the `/b` argument (branch name) then +Also, be aware that if you don't specify the `--branch` argument (branch name) then GitVersion will currently fallback to targeting whatever the default branch name happens to be for the repo. This could lead to incorrect results, so for that reason it's recommended to always explicitly specify the branch name. diff --git a/docs/input/docs/reference/build-servers/bitbucket-pipelines.md b/docs/input/docs/reference/build-servers/bitbucket-pipelines.md index f2e91f83db..268b94d0a3 100644 --- a/docs/input/docs/reference/build-servers/bitbucket-pipelines.md +++ b/docs/input/docs/reference/build-servers/bitbucket-pipelines.md @@ -28,7 +28,7 @@ pipelines: script: - export PATH="$PATH:/root/.dotnet/tools" - dotnet tool install --global GitVersion.Tool - - dotnet-gitversion /output buildserver + - dotnet-gitversion --output buildserver - source gitversion.properties - echo Building with semver $GITVERSION_FULLSEMVER - dotnet build @@ -41,7 +41,7 @@ You must set the `clone:depth` setting as shown above; without it, BitBucket Pip cause GitVersion to display an error message. ::: -When the action `dotnet-gitversion /output buildserver` is executed, it will detect that it is running in BitBucket Pipelines by the presence of +When the action `dotnet-gitversion --output buildserver` is executed, it will detect that it is running in BitBucket Pipelines by the presence of the `BITBUCKET_WORKSPACE` environment variable, which is set by the BitBucket Pipelines engine. It will generate a text file named `gitversion.properties` which contains all the output of the GitVersion tool, exported as individual environment variables prefixed with `GITVERSION_`. These environment variables can then be imported back into the build step using the `source gitversion.properties` action. @@ -62,7 +62,7 @@ pipelines: script: - export PATH="$PATH:/root/.dotnet/tools" - dotnet tool install --global GitVersion.Tool - - dotnet-gitversion /output buildserver + - dotnet-gitversion --output buildserver artifacts: - gitversion.properties - step: diff --git a/docs/input/docs/reference/build-servers/continua.md b/docs/input/docs/reference/build-servers/continua.md index e2af69c06e..fc3c29253d 100644 --- a/docs/input/docs/reference/build-servers/continua.md +++ b/docs/input/docs/reference/build-servers/continua.md @@ -48,7 +48,7 @@ follow the steps below: * Executable path: $Agent.GitVersion.Path$ * Working directory: %RepositoryPath% * In the `Arguments` tab, set the following values: - * Arguments: /url %RepositoryUrl% /b %RepositoryBranchName% /c %RepositoryCommitId% /output buildserver + * Arguments: --url %RepositoryUrl% --branch %RepositoryBranchName% --commit %RepositoryCommitId% --output buildserver * In the `Options` tab, set the following values: * Wait for completion: checked * Log output: checked diff --git a/docs/input/docs/reference/build-servers/index.cshtml b/docs/input/docs/reference/build-servers/index.cshtml index 9688a560d8..dec0653f25 100644 --- a/docs/input/docs/reference/build-servers/index.cshtml +++ b/docs/input/docs/reference/build-servers/index.cshtml @@ -29,11 +29,11 @@ RedirectFrom:

When the gitVersion executable is run with the - /output buildserver flag instead of outputting JSON, it will + --output buildserver flag instead of outputting JSON, it will export its version variables to the current build server as build-server native variables. For instance if you are running in TeamCity after you run - gitversion /output buildserver you will have the + gitversion --output buildserver you will have the %system.GitVersion.SemVer% variable available for you to use in the rest of the build configuration.

diff --git a/docs/input/docs/reference/build-servers/jenkins.md b/docs/input/docs/reference/build-servers/jenkins.md index 93eb9cb6ab..73923f45f2 100644 --- a/docs/input/docs/reference/build-servers/jenkins.md +++ b/docs/input/docs/reference/build-servers/jenkins.md @@ -31,7 +31,7 @@ To inject the GitVersion variables as environment variables for a build job using [EnvInject][env-inject], do the following: 1. Add an **Execute Windows batch command** build step with _Command_: - `gitversion /output buildserver` + `gitversion --output buildserver` 2. Add an **Inject environment variables** build step and use value 'gitversion.properties' for the _Properties File Path_ parameter @@ -58,7 +58,7 @@ In a pipeline stage: 1. Run GitVersion with the flag for _buildserver_ output (this only works when run from Jenkins, specifically when the `JENKINS_URL` environment variable is defined): ```groovy -sh 'gitversion /output buildserver'` +sh 'gitversion --output buildserver'` ``` 2. Add a script block to read the properties file, assign environment variables as needed: diff --git a/docs/input/docs/reference/build-servers/myget.md b/docs/input/docs/reference/build-servers/myget.md index 895862c2a9..c948bbdee2 100644 --- a/docs/input/docs/reference/build-servers/myget.md +++ b/docs/input/docs/reference/build-servers/myget.md @@ -15,7 +15,7 @@ to leverage GitVersion + GitFlow to produce Semantically Versioned packages. automatically picks up any of the following file names as pre-build script: * `pre-build.(bat|cmd|ps1)` * `pre-myget.(bat|cmd|ps1)` -* Run `GitVersion /output buildserver`: this will cause MyGet Build Services to +* Run `GitVersion --output buildserver`: this will cause MyGet Build Services to set the current `%PackageVersion%` value to the NuGet-compatible SemVer generated by GitVersion and apply this [MyGet Environment Variable](https://docs.myget.org/docs/reference/build-services#Available_Environment_Variables) wherever it is used during the build process. @@ -28,7 +28,7 @@ If you require the `AssemblyInfo.cs` files in your project to be patched with the information from GitVersion, you will have to run it manually, for example using the command: -`call %GitVersion% /updateassemblyinfo true`. +`call %GitVersion% --update-assembly-info true`. ::: Also check [docs.myget.org](https://docs.myget.org/docs/reference/build-services#GitVersion_and_Semantic_Versioning) diff --git a/docs/input/docs/reference/build-servers/octopus-deploy.md b/docs/input/docs/reference/build-servers/octopus-deploy.md index e7d50ff3e0..f7054ae549 100644 --- a/docs/input/docs/reference/build-servers/octopus-deploy.md +++ b/docs/input/docs/reference/build-servers/octopus-deploy.md @@ -106,7 +106,7 @@ if ($pendingChanges -ne $null) & git merge origin/main --ff-only # Determine version to release -$output = & $gitversion /output json +$output = & $gitversion --output json $versionInfoJson = $output -join "`n" $versionInfo = $versionInfoJson | ConvertFrom-Json @@ -146,7 +146,7 @@ $gitversion = "tools\GitVersion\GitVersion.exe" $octo = "tools\Octo\Octo.exe" $nuget = "tools\NuGet\NuGet.exe" # Calculate version -$output = & $gitversion /output json /l GitVersion.log /updateAssemblyInfo /nofetch +$output = & $gitversion --output json --log-file GitVersion.log --update-assembly-info --no-fetch if ($LASTEXITCODE -ne 0) { Write-Verbose "$output" throw "GitVersion Exit Code: $LASTEXITCODE" diff --git a/docs/input/docs/reference/build-servers/teamcity.md b/docs/input/docs/reference/build-servers/teamcity.md index 60f84f5129..f710b06ec1 100644 --- a/docs/input/docs/reference/build-servers/teamcity.md +++ b/docs/input/docs/reference/build-servers/teamcity.md @@ -12,7 +12,7 @@ In [TeamCity][teamcity] you can create a build step as follows: * **Runner type:** Command Line * **Run:** Executable with parameters * **Command executable:** `GitVersion.exe` -* **Command parameters:** `/output buildserver /updateassemblyinfo true` +* **Command parameters:** `--output buildserver --update-assembly-info true` Then in your build parameters simply [add a placeholder](#nuget-in-teamcity) of the GitVersion variables you would like to use. diff --git a/docs/input/docs/reference/configuration.md b/docs/input/docs/reference/configuration.md index 2a1350176f..3bcb4c4faf 100644 --- a/docs/input/docs/reference/configuration.md +++ b/docs/input/docs/reference/configuration.md @@ -19,7 +19,7 @@ The `develop` branch is set to `ContinuousDeployment` mode by default as we have found that is generally what is needed when using GitFlow. To see the effective configuration (defaults and overrides), you can run -`gitversion /showConfig`. +`gitversion --show-config`. ## Global configuration diff --git a/docs/input/docs/reference/mdsource/configuration.source.md b/docs/input/docs/reference/mdsource/configuration.source.md index 8c745a16d3..1a3afa1a00 100644 --- a/docs/input/docs/reference/mdsource/configuration.source.md +++ b/docs/input/docs/reference/mdsource/configuration.source.md @@ -19,7 +19,7 @@ The `develop` branch is set to `ContinuousDeployment` mode by default as we have found that is generally what is needed when using GitFlow. To see the effective configuration (defaults and overrides), you can run -`gitversion /showConfig`. +`gitversion --show-config`. ## Global configuration diff --git a/docs/input/docs/usage/cli/arguments.md b/docs/input/docs/usage/cli/arguments.md index d873130986..00a1a46ebf 100644 --- a/docs/input/docs/usage/cli/arguments.md +++ b/docs/input/docs/usage/cli/arguments.md @@ -5,14 +5,14 @@ Description: The supported arguments of the GitVersion Command Line Interface --- :::{.alert .alert-info} -**Hint:** While documentation and help use `/` as command prefix the hyphen `-` -is supported as well and is a better alternative for usage on \*nix systems. -Example: `-output json` vs. `/output json` +**Note:** GitVersion now uses POSIX-style `--long-name` arguments. Short aliases +(e.g. `-l`, `-o`, `-b`) are also supported. The legacy `/switch` and `-switch` +syntax is still available when `USE_V6_ARGUMENT_PARSER=true` is set. ::: ## Help -Below is the output from `gitversion /help` as a best effort to provide +Below is the output from `gitversion --help` as a best effort to provide documentation for which arguments GitVersion supports and their meaning. ```bash @@ -23,56 +23,59 @@ GitVersion [path] path The directory containing .git. If not defined current directory is used. (Must be first argument) - /version Displays the version of GitVersion - /diag Runs GitVersion with additional diagnostic information; - also needs the '/l' argument to specify a logfile or stdout - (requires git.exe to be installed) - /h or /? Shows Help - - /targetpath Same as 'path', but not positional - /output Determines the output to the console. Can be either 'json', + --version Displays the version of GitVersion + --diagnose, -d Runs GitVersion with additional diagnostic information; + also needs the '--log-file' argument to specify a logfile + or stdout (requires git.exe to be installed) + --help, -h Shows Help + + --target-path Same as 'path', but not positional + --output, -o Determines the output to the console. Can be either 'json', 'file', 'buildserver' or 'dotenv', will default to 'json'. - /outputfile Path to output file. It is used in combination with /output - 'file'. - /showvariable Used in conjunction with /output json, will output just a - particular variable. E.g. /output json /showvariable SemVer + --output-file Path to output file. It is used in combination with + --output 'file'. + --show-variable, -v + Used in conjunction with --output json, will output just a + particular variable. + E.g. --output json --show-variable SemVer - will output `1.2.3+beta.4` - /format Used in conjunction with /output json, will output a format + --format, -f Used in conjunction with --output json, will output a format containing version variables. Supports C# format strings - see [Format Strings](/docs/reference/custom-formatting) for details. - E.g. /output json /format {SemVer} - will output `1.2.3+beta.4` - /output json /format {Major}.{Minor} - will output `1.2` - /l Path to logfile; specify 'console' to emit to stdout. - /config Path to config file (defaults to GitVersion.yml, GitVersion.yaml, .GitVersion.yml or .GitVersion.yaml) - /showconfig Outputs the effective GitVersion config (defaults + custom + E.g. --output json --format {SemVer} - will output `1.2.3+beta.4` + --output json --format {Major}.{Minor} - will output `1.2` + --log-file, -l Path to logfile; specify 'console' to emit to stdout. + --config, -c Path to config file (defaults to GitVersion.yml, GitVersion.yaml, .GitVersion.yml or .GitVersion.yaml) + --show-config Outputs the effective GitVersion config (defaults + custom from GitVersion.yml, GitVersion.yaml, .GitVersion.yml or .GitVersion.yaml) in yaml format - /overrideconfig Overrides GitVersion config values inline (semicolon- - separated key value pairs e.g. /overrideconfig + --override-config + Overrides GitVersion config values inline (semicolon- + separated key value pairs e.g. --override-config tag-prefix=Foo) Currently supported config overrides: tag-prefix - /nocache Bypasses the cache, result will not be written to the cache. - /nonormalize Disables normalize step on a build server. - /allowshallow Allows GitVersion to run on a shallow clone. + --no-cache Bypasses the cache, result will not be written to the cache. + --no-normalize Disables normalize step on a build server. + --allow-shallow Allows GitVersion to run on a shallow clone. This is not recommended, but can be used if you are sure that the shallow clone contains all the information needed to calculate the version. - /verbosity Specifies the amount of information to be displayed. + --verbosity Specifies the amount of information to be displayed. (Quiet, Minimal, Normal, Verbose, Diagnostic) Default is Normal # AssemblyInfo updating - /updateassemblyinfo + --update-assembly-info Will recursively search for all 'AssemblyInfo.cs' files in the git repo and update them - /updateprojectfiles + --update-project-files Will recursively search for all project files (.csproj/.vbproj/.fsproj/.sqlproj) files in the git repo and update them Note: This is only compatible with the newer Sdk projects - /ensureassemblyinfo + --ensure-assembly-info If the assembly info file specified with - /updateassemblyinfo is not + --update-assembly-info is not found, it will be created with these attributes: AssemblyFileVersion, AssemblyVersion and AssemblyInformationalVersion. @@ -80,33 +83,33 @@ GitVersion [path] # Create or update Wix version file - /updatewixversionfile + --update-wix-version-file All the GitVersion variables are written to 'GitVersion_WixVersion.wxi'. The variables can then be referenced in other WiX project files for versioning. # Remote repository args - /url Url to remote git repository. - /b Name of the branch to use on the remote repository, must be - used in combination with /url. - /u Username in case authentication is required. - /p Password in case authentication is required. - /c The commit id to check. If not specified, the latest + --url Url to remote git repository. + --branch, -b Name of the branch to use on the remote repository, must be + used in combination with --url. + --username, -u Username in case authentication is required. + --password, -p Password in case authentication is required. + --commit The commit id to check. If not specified, the latest available commit on the specified branch will be used. - /dynamicRepoLocation + --dynamic-repo-location By default dynamic repositories will be cloned to %tmp%. Use this switch to override - /nofetch Disables 'git fetch' during version calculation. Might cause + --no-fetch Disables 'git fetch' during version calculation. Might cause GitVersion to not calculate your version as expected. ``` ## Override config -`/overrideconfig [key=value]` will override appropriate `key` from 'GitVersion.yml', 'GitVersion.yaml', '.GitVersion.yml' or '.GitVersion.yaml'. +`--override-config key=value` will override appropriate `key` from 'GitVersion.yml', 'GitVersion.yaml', '.GitVersion.yml' or '.GitVersion.yaml'. -To specify multiple options add multiple `/overrideconfig [key=value]` entries: -`/overrideconfig key1=value1 /overrideconfig key2=value2`. +To specify multiple options add multiple `--override-config key=value` entries: +`--override-config key1=value1 --override-config key2=value2`. To have **space characters** as a part of `value`, `value` has be enclosed with double quotes - `key="My value"`. @@ -139,28 +142,28 @@ Using `override-config` on the command line will not change the contents of the ### Example: How to override configuration option 'tag-prefix' to use prefix 'custom' -`GitVersion.exe /output json /overrideconfig tag-prefix=custom` +`GitVersion.exe --output json --override-config tag-prefix=custom` ### Example: How to override configuration option 'assembly-versioning-format' -`GitVersion.exe /output json /overrideconfig assembly-versioning-format="{Major}.{Minor}.{Patch}.{env:BUILD_NUMBER ?? 0}"` +`GitVersion.exe --output json --override-config assembly-versioning-format="{Major}.{Minor}.{Patch}.{env:BUILD_NUMBER ?? 0}"` Will pickup up environment variable `BUILD_NUMBER` or fallback to zero for assembly revision number. ### Example: How to override configuration option 'assembly-versioning-scheme' -`GitVersion.exe /output json /overrideconfig assembly-versioning-scheme=MajorMinor` +`GitVersion.exe --output json --override-config assembly-versioning-scheme=MajorMinor` Will use only major and minor version numbers for assembly version. Assembly build and revision numbers will be 0 (e.g. `1.2.0.0`) ### Example: How to override multiple configuration options -`GitVersion.exe /output json /overrideconfig tag-prefix=custom /overrideconfig assembly-versioning-scheme=MajorMinor` +`GitVersion.exe --output json --override-config tag-prefix=custom --override-config assembly-versioning-scheme=MajorMinor` ### Example: How to override configuration option 'update-build-number' -`GitVersion.exe /output json /overrideconfig update-build-number=true` +`GitVersion.exe --output json --override-config update-build-number=true` ### Example: How to override configuration option 'next-version' -`GitVersion.exe /output json /overrideconfig next-version=6` +`GitVersion.exe --output json --override-config next-version=6` diff --git a/docs/input/docs/usage/cli/assembly-patch.md b/docs/input/docs/usage/cli/assembly-patch.md index 94509e65c8..f8ec0ee58c 100644 --- a/docs/input/docs/usage/cli/assembly-patch.md +++ b/docs/input/docs/usage/cli/assembly-patch.md @@ -6,7 +6,7 @@ Description: | assemblies --- -`GitVersion.exe /updateassemblyinfo` will recursively search for all +`GitVersion.exe --update-assembly-info` will recursively search for all `AssemblyInfo.cs` or `AssemblyInfo.vb` files in the git repo and update them. It will update the following assembly attributes: @@ -20,13 +20,13 @@ Note that contrary to when using the [MSBuild Task][msbuild-task] the attributes must already exist in the `AssemblyInfo.cs` or `AssemblyInfo.vb` files prior to calling GitVersion. -By adding `/updateassemblyinfo ` the name of AssemblyInfo file to +By adding `--update-assembly-info ` the name of AssemblyInfo file to update can be set. This switch can accept multiple files with the path to the file specified relative to the working directory. GitVersion can generate an assembly info source file for you if it does not -already exist. Use the `/ensureassemblyinfo` switch alongside -`/updateassemblyinfo `, if the filename specified does not exist it +already exist. Use the `--ensure-assembly-info` switch alongside +`--update-assembly-info `, if the filename specified does not exist it will be generated based on a known template that adds: * `AssemblyVersion` will be set to the `AssemblySemVer` variable. @@ -38,45 +38,45 @@ will be generated based on a known template that adds: This can be done for \*.cs, \*.vb and \*.fs files. When requesting that GitVersion generate an assembly info file you are limited -to only specifying a single `` within the `/updateassemblyinfo` +to only specifying a single `` within the `--update-assembly-info` switch, this is to prevent the creation of multiple assembly info files with the same assembly version attributes. If this occurs your build will fail. ## Example: When AssemblyInfo.cs does not exist -`GitVersion.exe /updateassemblyinfo AssemblyInfo.cs /ensureassemblyinfo` +`GitVersion.exe --update-assembly-info AssemblyInfo.cs --ensure-assembly-info` A file is generated that contains version attributes (`AssemblyVersion`, `AssemblyFileVersion`, `AssemblyInformationalVersion`) ## Example: When AssemblyInfo.cs already exists -`GitVersion.exe /updateassemblyinfo AssemblyInfo.cs /ensureassemblyinfo` +`GitVersion.exe --update-assembly-info AssemblyInfo.cs --ensure-assembly-info` All known attributes (`AssemblyVersion`, `AssemblyFileVersion`, `AssemblyInformationalVersion`) will be updated ## Example: When AssemblyInfo.cs and AssemblyVersionInfo.cs do not exist -`GitVersion.exe /updateassemblyinfo AssemblyInfo.cs AssemblyVersionInfo.cs /ensureassemblyinfo` +`GitVersion.exe --update-assembly-info AssemblyInfo.cs AssemblyVersionInfo.cs --ensure-assembly-info` Will result in command line argument error ## Example: When AssemblyInfo.cs and AssemblyVersionInfo.cs already exist -`GitVersion.exe /updateassemblyinfo AssemblyInfo.cs AssemblyVersionInfo.cs` +`GitVersion.exe --update-assembly-info AssemblyInfo.cs AssemblyVersionInfo.cs` Will iterate through each file and update known attributes (`AssemblyVersion`, `AssemblyFileVersion`, `AssemblyInformationalVersion`). ## Example: How to override configuration option 'tag-prefix' to use prefix 'custom' -`GitVersion.exe /output json /overrideconfig tag-prefix=custom` +`GitVersion.exe --output json --override-config tag-prefix=custom` ## Writing version metadata in WiX format To support integration with WiX projects, use `GitVersion.exe -/updatewixversionfile`. All the [variables][variables] are written to +--update-wix-version-file`. All the [variables][variables] are written to `GitVersion_WixVersion.wxi` under the current working directory and can be referenced in the WiX project files. diff --git a/docs/input/docs/usage/cli/output.md b/docs/input/docs/usage/cli/output.md index e8cdaf051d..c20eecb21b 100644 --- a/docs/input/docs/usage/cli/output.md +++ b/docs/input/docs/usage/cli/output.md @@ -9,7 +9,7 @@ By default GitVersion returns a json object to stdout containing all the great if you want to get your build scripts to parse the json object then use the variables, but there is a simpler way. -`GitVersion.exe /output buildserver` will change the mode of GitVersion to write +`GitVersion.exe --output buildserver` will change the mode of GitVersion to write out the variables to whatever build server it is running in. You can then use those variables in your build scripts or run different tools to create versioned NuGet packages or whatever you would like to do. See [build @@ -17,21 +17,21 @@ servers](/docs/reference/build-servers) for more information about this. You can even store the [variables](/docs/reference/variables) in a Dotenv file and load it to have the variables available in your environment. -For that you have to run `GitVersion.exe /output dotenv` and store the output +For that you have to run `GitVersion.exe --output dotenv` and store the output into e.g. a `gitversion.env` file. These files can also be passed around in CI environments like [GitHub](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#passing-values-between-steps-and-jobs-in-a-workflow) or [GitLab](https://docs.gitlab.com/ee/ci/variables/#pass-an-environment-variable-to-another-job). Below are some examples of using the Dotenv format in the Unix command line: ```bash # Output version variables in Dotenv format -gitversion /output dotenv +gitversion --output dotenv # Show only a subset of the version variables in Dotenv format -gitversion /output dotenv | grep -i "prerelease" +gitversion --output dotenv | grep -i "prerelease" # Show only a subset of the version variables that match the regex in Dotenv format -gitversion /output dotenv | grep -iE "major|sha=|_prerelease" +gitversion --output dotenv | grep -iE "major|sha=|_prerelease" # Write version variables in Dotenv format into a file -gitversion /output dotenv > gitversion.env +gitversion --output dotenv > gitversion.env ``` From 3a14f9476f6aca8c297f31179148bbde2dc3319d Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Mon, 2 Mar 2026 23:27:41 +0100 Subject: [PATCH 09/24] refactor(parser): Rename SystemCommandLineArgumentParser Renames the `SystemCommandLineArgumentParser` class and file to `ArgumentParser` for improved clarity. Applies minor code improvements like using the null-conditional operator and sealing the `CommandOptions` record. --- build/build/Tasks/ValidateVersion.cs | 2 +- .../ArgumentParserTests.cs | 12 +++++----- ...ineArgumentParser.cs => ArgumentParser.cs} | 22 +++++++++---------- src/GitVersion.App/GitVersionAppModule.cs | 2 +- 4 files changed, 19 insertions(+), 19 deletions(-) rename src/GitVersion.App/{SystemCommandLineArgumentParser.cs => ArgumentParser.cs} (95%) diff --git a/build/build/Tasks/ValidateVersion.cs b/build/build/Tasks/ValidateVersion.cs index e88866ffca..5e79d567a1 100644 --- a/build/build/Tasks/ValidateVersion.cs +++ b/build/build/Tasks/ValidateVersion.cs @@ -11,6 +11,6 @@ public override void Run(BuildContext context) { ArgumentNullException.ThrowIfNull(context.Version); var gitVersionTool = context.GetGitVersionToolLocation(); - context.ValidateOutput("dotnet", $"\"{gitVersionTool}\" -version", context.Version.GitVersion.InformationalVersion); + context.ValidateOutput("dotnet", $"\"{gitVersionTool}\" --version", context.Version.GitVersion.InformationalVersion); } } diff --git a/src/GitVersion.App.Tests/ArgumentParserTests.cs b/src/GitVersion.App.Tests/ArgumentParserTests.cs index e1faab313c..e6e396d3a8 100644 --- a/src/GitVersion.App.Tests/ArgumentParserTests.cs +++ b/src/GitVersion.App.Tests/ArgumentParserTests.cs @@ -273,7 +273,7 @@ public void CreateMultipleAssemblyInfoProtected(string command) { var exception = Assert.Throws(() => this.argumentParser.ParseArguments(command)); exception.ShouldNotBeNull(); - exception.Message.ShouldBe("Can't specify multiple assembly info files when using /ensureassemblyinfo switch, either use a single assembly info file or do not specify /ensureassemblyinfo and create assembly info files manually"); + exception.Message.ShouldBe("Can't specify multiple assembly info files when using --ensure-assembly-info, either use a single assembly info file or do not specify --ensure-assembly-info and create assembly info files manually"); } [TestCase("--update-project-files Assembly.csproj --ensure-assembly-info")] @@ -281,7 +281,7 @@ public void UpdateProjectInfoWithEnsureAssemblyInfoProtected(string command) { var exception = Assert.Throws(() => this.argumentParser.ParseArguments(command)); exception.ShouldNotBeNull(); - exception.Message.ShouldBe("Cannot specify -ensureassemblyinfo with updateprojectfiles: please ensure your project file exists before attempting to update it"); + exception.Message.ShouldBe("Cannot specify --ensure-assembly-info with --update-project-files: please ensure your project file exists before attempting to update it"); } [Test] @@ -395,11 +395,11 @@ private static IEnumerable OverrideconfigWithInvalidOptionTestData { yield return new TestCaseData("tag-prefix=sample=asdf") { - ExpectedResult = "Could not parse /overrideconfig option: tag-prefix=sample=asdf. Ensure it is in format 'key=value'." + ExpectedResult = "Could not parse --override-config option: tag-prefix=sample=asdf. Ensure it is in format 'key=value'." }; yield return new TestCaseData("unknown-option=25") { - ExpectedResult = "Could not parse /overrideconfig option: unknown-option=25. Unsupported 'key'." + ExpectedResult = "Could not parse --override-config option: unknown-option=25. Unsupported 'key'." }; } @@ -757,11 +757,11 @@ public void CheckVerbosityParsing(string command, bool shouldThrow, Verbosity ex { if (shouldThrow) { - Assert.Throws(() => SystemCommandLineArgumentParser.ParseVerbosity(command)); + Assert.Throws(() => ArgumentParser.ParseVerbosity(command)); } else { - var verbosity = SystemCommandLineArgumentParser.ParseVerbosity(command); + var verbosity = ArgumentParser.ParseVerbosity(command); verbosity.ShouldBe(expectedVerbosity); } } diff --git a/src/GitVersion.App/SystemCommandLineArgumentParser.cs b/src/GitVersion.App/ArgumentParser.cs similarity index 95% rename from src/GitVersion.App/SystemCommandLineArgumentParser.cs rename to src/GitVersion.App/ArgumentParser.cs index ce83d933f4..855bbf6011 100644 --- a/src/GitVersion.App/SystemCommandLineArgumentParser.cs +++ b/src/GitVersion.App/ArgumentParser.cs @@ -10,7 +10,7 @@ namespace GitVersion; -internal class SystemCommandLineArgumentParser( +internal class ArgumentParser( IEnvironment environment, IFileSystem fileSystem, IConsole console, @@ -100,7 +100,7 @@ public Arguments ParseArguments(string[] commandLineArguments) // Detect unknown options that were incorrectly consumed as positional path argument var positionalCheck = parseResult.GetValue(options.Path); - if (positionalCheck != null && positionalCheck.StartsWith('-')) + if (positionalCheck?.StartsWith('-') == true) { throw new WarningException($"Could not parse command line parameter '{positionalCheck}'."); } @@ -280,7 +280,7 @@ private void MapParsedValues(Arguments arguments, ParseResult parseResult, Comma if (arguments.UpdateProjectFiles) { - throw new WarningException("Cannot specify both updateprojectfiles and updateassemblyinfo in the same run. Please rerun GitVersion with only one parameter"); + throw new WarningException("Cannot specify both --update-project-files and --update-assembly-info in the same run. Please rerun GitVersion with only one parameter"); } } @@ -303,12 +303,12 @@ private void MapParsedValues(Arguments arguments, ParseResult parseResult, Comma if (arguments.UpdateAssemblyInfo) { - throw new WarningException("Cannot specify both updateassemblyinfo and updateprojectfiles in the same run. Please rerun GitVersion with only one parameter"); + throw new WarningException("Cannot specify both --update-assembly-info and --update-project-files in the same run. Please rerun GitVersion with only one parameter"); } if (arguments.EnsureAssemblyInfo) { - throw new WarningException("Cannot specify -ensureassemblyinfo with updateprojectfiles: please ensure your project file exists before attempting to update it"); + throw new WarningException("Cannot specify --ensure-assembly-info with --update-project-files: please ensure your project file exists before attempting to update it"); } } @@ -319,19 +319,19 @@ private void MapParsedValues(Arguments arguments, ParseResult parseResult, Comma if (arguments.UpdateProjectFiles) { - throw new WarningException("Cannot specify -ensureassemblyinfo with updateprojectfiles: please ensure your project file exists before attempting to update it"); + throw new WarningException("Cannot specify --ensure-assembly-info with --update-project-files: please ensure your project file exists before attempting to update it"); } if (arguments.UpdateAssemblyInfoFileName.Count > 1 && arguments.EnsureAssemblyInfo) { - throw new WarningException("Can't specify multiple assembly info files when using /ensureassemblyinfo switch, either use a single assembly info file or do not specify /ensureassemblyinfo and create assembly info files manually"); + throw new WarningException("Can't specify multiple assembly info files when using --ensure-assembly-info, either use a single assembly info file or do not specify --ensure-assembly-info and create assembly info files manually"); } } // Check assembly info + ensure assembly info cross-validation if (arguments.UpdateAssemblyInfoFileName.Count > 1 && arguments.EnsureAssemblyInfo) { - throw new WarningException("Can't specify multiple assembly info files when using /ensureassemblyinfo switch, either use a single assembly info file or do not specify /ensureassemblyinfo and create assembly info files manually"); + throw new WarningException("Can't specify multiple assembly info files when using --ensure-assembly-info, either use a single assembly info file or do not specify --ensure-assembly-info and create assembly info files manually"); } // Update wix version file @@ -682,14 +682,14 @@ private static void ParseOverrideConfig(Arguments arguments, IReadOnlyCollection if (keyAndValue.Length != 2) { throw new WarningException( - $"Could not parse /overrideconfig option: {keyValueOption}. Ensure it is in format 'key=value'."); + $"Could not parse --override-config option: {keyValueOption}. Ensure it is in format 'key=value'."); } var optionKey = keyAndValue[0].ToLowerInvariant(); if (!OverrideConfigurationOptionParser.SupportedProperties.Contains(optionKey)) { throw new WarningException( - $"Could not parse /overrideconfig option: {keyValueOption}. Unsupported 'key'."); + $"Could not parse --override-config option: {keyValueOption}. Unsupported 'key'."); } parser.SetValue(optionKey, keyAndValue[1]); @@ -698,7 +698,7 @@ private static void ParseOverrideConfig(Arguments arguments, IReadOnlyCollection arguments.OverrideConfiguration = parser.GetOverrideConfiguration(); } - private record CommandOptions( + private sealed record CommandOptions( Argument Path, Option Version, Option Diagnose, diff --git a/src/GitVersion.App/GitVersionAppModule.cs b/src/GitVersion.App/GitVersionAppModule.cs index 1dfbf338c6..1bf2aa492c 100644 --- a/src/GitVersion.App/GitVersionAppModule.cs +++ b/src/GitVersion.App/GitVersionAppModule.cs @@ -14,7 +14,7 @@ public void RegisterTypes(IServiceCollection services) } else { - services.AddSingleton(); + services.AddSingleton(); } services.AddSingleton(); From 8997061599ad3dfee11d385d9d7b4379921d02e9 Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Tue, 3 Mar 2026 00:00:03 +0100 Subject: [PATCH 10/24] perf(parser): Optimize command-line argument parser initialization Introduce lazy initialization for the System.CommandLine schema to improve performance on subsequent argument parsing calls. Refactors the ArgumentParser for enhanced efficiency. --- src/GitVersion.App/ArgumentParser.cs | 557 ++++++++------------------- 1 file changed, 171 insertions(+), 386 deletions(-) diff --git a/src/GitVersion.App/ArgumentParser.cs b/src/GitVersion.App/ArgumentParser.cs index 855bbf6011..72230a8cad 100644 --- a/src/GitVersion.App/ArgumentParser.cs +++ b/src/GitVersion.App/ArgumentParser.cs @@ -25,7 +25,7 @@ LoggingLevelSwitch loggingLevelSwitch private readonly IGlobbingResolver globbingResolver = globbingResolver.NotNull(); private readonly LoggingLevelSwitch loggingLevelSwitch = loggingLevelSwitch.NotNull(); - private const string defaultOutputFileName = "GitVersion.json"; + private const string DefaultOutputFileName = "GitVersion.json"; private static readonly IEnumerable availableVariables = GitVersionVariables.AvailableVariables; private static readonly Dictionary VerbosityMaps = new() @@ -37,26 +37,23 @@ LoggingLevelSwitch loggingLevelSwitch { Verbosity.Quiet, LogEventLevel.Error } }; - public Arguments ParseArguments(string commandLineArguments) - { - var arguments = QuotedStringHelpers.SplitUnquoted(commandLineArguments, ' '); - return ParseArguments(arguments); - } + // Build the command schema at once — it's stateless and safe to reuse across calls. + private static readonly Lazy<(RootCommand Root, CommandOptions Options)> commandFactory = new(BuildCommand); + + public Arguments ParseArguments(string commandLineArguments) => + ParseArguments(QuotedStringHelpers.SplitUnquoted(commandLineArguments, ' ')); public Arguments ParseArguments(string[] commandLineArguments) { if (commandLineArguments.Length == 0) { - var args = new Arguments - { - TargetPath = SysEnv.CurrentDirectory - }; + var args = new Arguments { TargetPath = SysEnv.CurrentDirectory }; args.Output.Add(OutputType.Json); AddAuthentication(args); return args; } - var (rootCommand, options) = BuildCommand(); + var (rootCommand, options) = commandFactory.Value; // Let System.CommandLine handle --help output natively if (commandLineArguments.Any(a => a is "--help" or "-h" or "-?" or "/?")) @@ -74,31 +71,20 @@ public Arguments ParseArguments(string[] commandLineArguments) var parseResult = rootCommand.Parse(commandLineArguments); - // Check for parse errors - var errors = parseResult.Errors; - if (errors.Count > 0) + if (parseResult.Errors.Count > 0) { - var firstError = errors[0]; - var message = firstError.Message; - - // Try to extract the unrecognized token for a friendlier message - if (message.Contains("Unrecognized command or argument")) - { - var token = ExtractUnrecognizedToken(message); - throw new WarningException($"Could not parse command line parameter '{token}'."); - } - - throw new WarningException($"Could not parse command line parameter '{message}'."); + var message = parseResult.Errors[0].Message; + var token = message.Contains("Unrecognized command or argument") + ? ExtractUnrecognizedToken(message) + : message; + throw new WarningException($"Could not parse command line parameter '{token}'."); } - // Check for unmatched tokens that System.CommandLine didn't report as errors - var unmatchedTokens = parseResult.UnmatchedTokens; - if (unmatchedTokens.Count > 0) + if (parseResult.UnmatchedTokens.Count > 0) { - throw new WarningException($"Could not parse command line parameter '{unmatchedTokens[0]}'."); + throw new WarningException($"Could not parse command line parameter '{parseResult.UnmatchedTokens[0]}'."); } - // Detect unknown options that were incorrectly consumed as positional path argument var positionalCheck = parseResult.GetValue(options.Path); if (positionalCheck?.StartsWith('-') == true) { @@ -107,11 +93,8 @@ public Arguments ParseArguments(string[] commandLineArguments) var arguments = new Arguments(); AddAuthentication(arguments); - - // Map parsed values to Arguments MapParsedValues(arguments, parseResult, options); - // Defaults if (arguments.Output.Count == 0) { arguments.Output.Add(OutputType.Json); @@ -119,23 +102,23 @@ public Arguments ParseArguments(string[] commandLineArguments) if (arguments.Output.Contains(OutputType.File) && arguments.OutputFile == null) { - arguments.OutputFile = defaultOutputFileName; + arguments.OutputFile = DefaultOutputFileName; } - // Target path - var positionalPath = parseResult.GetValue(options.Path); - arguments.TargetPath ??= positionalPath ?? SysEnv.CurrentDirectory; + arguments.TargetPath ??= parseResult.GetValue(options.Path) ?? SysEnv.CurrentDirectory; arguments.TargetPath = arguments.TargetPath.TrimEnd('/', '\\'); if (!arguments.EnsureAssemblyInfo) + { arguments.UpdateAssemblyInfoFileName = ResolveFiles(arguments.TargetPath, arguments.UpdateAssemblyInfoFileName).ToHashSet(); + } ValidateConfigurationFile(arguments); return arguments; } - private void PrintBuiltInHelp(RootCommand rootCommand) + private static void PrintBuiltInHelp(RootCommand rootCommand) { rootCommand.SetAction((_, _) => Task.FromResult(0)); rootCommand.Parse(["--help"]).InvokeAsync().GetAwaiter().GetResult(); @@ -144,13 +127,14 @@ private void PrintBuiltInHelp(RootCommand rootCommand) private void PrintBuiltInVersion() { var assembly = Assembly.GetExecutingAssembly(); - var version = assembly - .GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false) + var version = assembly.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false) .FirstOrDefault() is AssemblyInformationalVersionAttribute attr ? attr.InformationalVersion : assembly.GetName().Version?.ToString(); if (version != null) + { this.console.WriteLine(version); + } } private static string ExtractUnrecognizedToken(string message) @@ -158,27 +142,21 @@ private static string ExtractUnrecognizedToken(string message) // System.CommandLine error format: "Unrecognized command or argument 'xxx'." var start = message.IndexOf('\''); var end = message.LastIndexOf('\''); - if (start >= 0 && end > start) - { - return message[(start + 1)..end]; - } - return message; + return start >= 0 && end > start ? message[(start + 1)..end] : message; } private void MapParsedValues(Arguments arguments, ParseResult parseResult, CommandOptions options) { - // Log file - var logFile = parseResult.GetValue(options.LogFile); - if (logFile != null) - arguments.LogFilePath = logFile; - - // Diagnose - if (parseResult.GetValue(options.Diagnose)) - arguments.Diag = true; + arguments.LogFilePath = parseResult.GetValue(options.LogFile) ?? arguments.LogFilePath; + arguments.Diag = parseResult.GetValue(options.Diagnose); + arguments.ShowConfiguration = parseResult.GetValue(options.ShowConfig); + arguments.NoFetch = parseResult.GetValue(options.NoFetch); + arguments.NoCache = parseResult.GetValue(options.NoCache); + arguments.NoNormalize = parseResult.GetValue(options.NoNormalize); + arguments.AllowShallow = parseResult.GetValue(options.AllowShallow); + arguments.UpdateWixVersionFile = parseResult.GetValue(options.UpdateWixVersionFile); - // Output - var outputs = parseResult.GetValue(options.Output); - if (outputs != null) + if (parseResult.GetValue(options.Output) is { } outputs) { foreach (var output in outputs) { @@ -186,38 +164,32 @@ private void MapParsedValues(Arguments arguments, ParseResult parseResult, Comma } } - // Output file - var outputFile = parseResult.GetValue(options.OutputFile); - if (outputFile != null) + if (parseResult.GetValue(options.OutputFile) is { } outputFile) + { arguments.OutputFile = outputFile; + } - // Show variable - var showVariable = parseResult.GetValue(options.ShowVariable); - if (showVariable != null) + if (parseResult.GetValue(options.ShowVariable) is { } showVariable) + { ParseShowVariable(arguments, showVariable); + } - // Format - var format = parseResult.GetValue(options.Format); - if (format != null) + if (parseResult.GetValue(options.Format) is { } format) + { ParseFormat(arguments, format); + } - // Config - var config = parseResult.GetValue(options.Config); - if (config != null) + if (parseResult.GetValue(options.Config) is { } config) + { arguments.ConfigurationFile = config; + } - // Show config - if (parseResult.GetValue(options.ShowConfig)) - arguments.ShowConfiguration = true; - - // Override config - var overrideConfigs = parseResult.GetValue(options.OverrideConfig); - if (overrideConfigs is { Length: > 0 }) + if (parseResult.GetValue(options.OverrideConfig) is { Length: > 0 } overrideConfigs) + { ParseOverrideConfig(arguments, overrideConfigs); + } - // Target path (explicit option) - var targetPath = parseResult.GetValue(options.TargetPath); - if (targetPath != null) + if (parseResult.GetValue(options.TargetPath) is { } targetPath) { arguments.TargetPath = targetPath; if (string.IsNullOrWhiteSpace(targetPath) || !this.fileSystem.Directory.Exists(targetPath)) @@ -226,54 +198,26 @@ private void MapParsedValues(Arguments arguments, ParseResult parseResult, Comma } } - // No-fetch - if (parseResult.GetValue(options.NoFetch)) - arguments.NoFetch = true; - - // No-cache - if (parseResult.GetValue(options.NoCache)) - arguments.NoCache = true; - - // No-normalize - if (parseResult.GetValue(options.NoNormalize)) - arguments.NoNormalize = true; - - // Allow shallow - if (parseResult.GetValue(options.AllowShallow)) - arguments.AllowShallow = true; - - // Verbosity - var verbosity = parseResult.GetValue(options.VerbosityOption); - if (verbosity != null) + if (parseResult.GetValue(options.VerbosityOption) is { } verbosity) { - var parsedVerbosity = ParseVerbosity(verbosity); - this.loggingLevelSwitch.MinimumLevel = VerbosityMaps[parsedVerbosity]; + this.loggingLevelSwitch.MinimumLevel = VerbosityMaps[ParseVerbosity(verbosity)]; } - // Update assembly info - var updateAssemblyInfoResult = parseResult.GetResult(options.UpdateAssemblyInfo); - if (updateAssemblyInfoResult is { Implicit: false }) + if (parseResult.GetResult(options.UpdateAssemblyInfo) is { Implicit: false }) { - var updateAssemblyInfo = parseResult.GetValue(options.UpdateAssemblyInfo); - - // Check if the option was explicitly disabled with "false" or "0" - if (updateAssemblyInfo is { Length: 1 } && - (updateAssemblyInfo[0].Equals("false", StringComparison.OrdinalIgnoreCase) || - updateAssemblyInfo[0].Equals("0", StringComparison.Ordinal))) + var values = parseResult.GetValue(options.UpdateAssemblyInfo); + if (values is [var single] && (single.Equals("false", StringComparison.OrdinalIgnoreCase) || single.Equals("0", StringComparison.Ordinal))) { arguments.UpdateAssemblyInfo = false; } else { arguments.UpdateAssemblyInfo = true; - if (updateAssemblyInfo != null) + if (values != null) { - foreach (var file in updateAssemblyInfo) + foreach (var file in values.Where(f => !IsBooleanTrue(f))) { - if (!file.Equals("true", StringComparison.OrdinalIgnoreCase) && !file.Equals("1", StringComparison.OrdinalIgnoreCase)) - { - arguments.UpdateAssemblyInfoFileName.Add(file); - } + arguments.UpdateAssemblyInfoFileName.Add(file); } } } @@ -284,20 +228,14 @@ private void MapParsedValues(Arguments arguments, ParseResult parseResult, Comma } } - // Update project files - var updateProjectFilesResult = parseResult.GetResult(options.UpdateProjectFiles); - if (updateProjectFilesResult is { Implicit: false }) + if (parseResult.GetResult(options.UpdateProjectFiles) is { Implicit: false }) { arguments.UpdateProjectFiles = true; - var updateProjectFiles = parseResult.GetValue(options.UpdateProjectFiles); - if (updateProjectFiles != null) + if (parseResult.GetValue(options.UpdateProjectFiles) is { } projectFiles) { - foreach (var file in updateProjectFiles) + foreach (var file in projectFiles.Where(f => !IsBooleanTrue(f))) { - if (!file.Equals("true", StringComparison.OrdinalIgnoreCase) && !file.Equals("1", StringComparison.OrdinalIgnoreCase)) - { - arguments.UpdateAssemblyInfoFileName.Add(file); - } + arguments.UpdateAssemblyInfoFileName.Add(file); } } @@ -312,7 +250,6 @@ private void MapParsedValues(Arguments arguments, ParseResult parseResult, Comma } } - // Ensure assembly info if (parseResult.GetValue(options.EnsureAssemblyInfo)) { arguments.EnsureAssemblyInfo = true; @@ -321,292 +258,153 @@ private void MapParsedValues(Arguments arguments, ParseResult parseResult, Comma { throw new WarningException("Cannot specify --ensure-assembly-info with --update-project-files: please ensure your project file exists before attempting to update it"); } - - if (arguments.UpdateAssemblyInfoFileName.Count > 1 && arguments.EnsureAssemblyInfo) - { - throw new WarningException("Can't specify multiple assembly info files when using --ensure-assembly-info, either use a single assembly info file or do not specify --ensure-assembly-info and create assembly info files manually"); - } } - // Check assembly info + ensure assembly info cross-validation if (arguments.UpdateAssemblyInfoFileName.Count > 1 && arguments.EnsureAssemblyInfo) { throw new WarningException("Can't specify multiple assembly info files when using --ensure-assembly-info, either use a single assembly info file or do not specify --ensure-assembly-info and create assembly info files manually"); } - // Update wix version file - if (parseResult.GetValue(options.UpdateWixVersionFile)) - arguments.UpdateWixVersionFile = true; - - // Remote repository args - var url = parseResult.GetValue(options.Url); - if (url != null) - arguments.TargetUrl = url; - - var branch = parseResult.GetValue(options.Branch); - if (branch != null) - arguments.TargetBranch = branch; - - var username = parseResult.GetValue(options.Username); - if (username != null) - arguments.Authentication.Username = username; - - var password = parseResult.GetValue(options.Password); - if (password != null) - arguments.Authentication.Password = password; - - var commit = parseResult.GetValue(options.Commit); - if (commit != null) - arguments.CommitId = commit; - - var dynamicRepoLocation = parseResult.GetValue(options.DynamicRepoLocation); - if (dynamicRepoLocation != null) - arguments.ClonePath = dynamicRepoLocation; - } - - private static (RootCommand rootCommand, CommandOptions options) BuildCommand() - { - var pathArgument = new Argument("path") - { - Description = "The directory containing .git. If not defined current directory is used.", - Arity = ArgumentArity.ZeroOrOne - }; - - var versionOption = new Option("--version") - { - Description = "Displays the version of GitVersion" - }; - - var diagnoseOption = new Option("--diagnose", "-d") - { - Description = "Runs GitVersion with additional diagnostic information" - }; - - var logFileOption = new Option("--log-file", "-l") - { - Description = "Path to logfile; specify 'console' to emit to stdout" - }; - - var outputOption = new Option("--output", "-o") - { - Description = "Determines the output to the console. Can be 'json', 'file', 'buildserver' or 'dotenv'", - AllowMultipleArgumentsPerToken = true, - Arity = ArgumentArity.ZeroOrMore - }; - - var outputFileOption = new Option("--output-file") - { - Description = "Path to output file. Used in combination with --output 'file'" - }; - - var showVariableOption = new Option("--show-variable", "-v") - { - Description = "Output just a particular variable" - }; - - var formatOption = new Option("--format", "-f") - { - Description = "Output a format containing version variables" - }; - - var configOption = new Option("--config", "-c") - { - Description = "Path to config file (defaults to GitVersion.yml)" - }; - - var showConfigOption = new Option("--show-config") - { - Description = "Outputs the effective GitVersion config in yaml format" - }; - - var overrideConfigOption = new Option("--override-config") - { - Description = "Overrides GitVersion config values inline (key=value pairs)", - AllowMultipleArgumentsPerToken = false, - Arity = ArgumentArity.ZeroOrMore - }; - - var targetPathOption = new Option("--target-path") - { - Description = "Same as 'path', but not positional" - }; - - var noFetchOption = new Option("--no-fetch") - { - Description = "Disables 'git fetch' during version calculation" - }; - - var noCacheOption = new Option("--no-cache") + if (parseResult.GetValue(options.Url) is { } url) { - Description = "Bypasses the cache, result will not be written to the cache" - }; - - var noNormalizeOption = new Option("--no-normalize") - { - Description = "Disables normalize step on a build server" - }; - - var allowShallowOption = new Option("--allow-shallow") - { - Description = "Allows GitVersion to run on a shallow clone" - }; - - var verbosityOption = new Option("--verbosity") - { - Description = "Specifies the amount of information to be displayed (Quiet, Minimal, Normal, Verbose, Diagnostic)" - }; - - var updateAssemblyInfoOption = new Option("--update-assembly-info") - { - Description = "Will recursively search for all 'AssemblyInfo.cs' files and update them", - AllowMultipleArgumentsPerToken = true, - Arity = ArgumentArity.ZeroOrMore - }; - - var updateProjectFilesOption = new Option("--update-project-files") - { - Description = "Will recursively search for all project files and update them", - AllowMultipleArgumentsPerToken = true, - Arity = ArgumentArity.ZeroOrMore - }; - - var ensureAssemblyInfoOption = new Option("--ensure-assembly-info") - { - Description = "If the assembly info file specified with --update-assembly-info is not found, it will be created" - }; + arguments.TargetUrl = url; + } - var updateWixVersionFileOption = new Option("--update-wix-version-file") + if (parseResult.GetValue(options.Branch) is { } branch) { - Description = "All the GitVersion variables are written to 'GitVersion_WixVersion.wxi'" - }; + arguments.TargetBranch = branch; + } - var urlOption = new Option("--url") + if (parseResult.GetValue(options.Username) is { } username) { - Description = "Url to remote git repository" - }; + arguments.Authentication.Username = username; + } - var branchOption = new Option("--branch", "-b") + if (parseResult.GetValue(options.Password) is { } password) { - Description = "Name of the branch to use on the remote repository" - }; + arguments.Authentication.Password = password; + } - var usernameOption = new Option("--username", "-u") + if (parseResult.GetValue(options.Commit) is { } commit) { - Description = "Username in case authentication is required" - }; + arguments.CommitId = commit; + } - var passwordOption = new Option("--password", "-p") + if (parseResult.GetValue(options.DynamicRepoLocation) is { } dynRepo) { - Description = "Password in case authentication is required" - }; + arguments.ClonePath = dynRepo; + } + } - var commitOption = new Option("--commit") - { - Description = "The commit id to check" - }; + private static bool IsBooleanTrue(string value) => + value.Equals("true", StringComparison.OrdinalIgnoreCase) || value.Equals("1", StringComparison.Ordinal); - var dynamicRepoLocationOption = new Option("--dynamic-repo-location") - { - Description = "Override default dynamic repository clone location" - }; + private static (RootCommand Root, CommandOptions Options) BuildCommand() + { + var path = new Argument("path") { Description = "The directory containing .git. If not defined current directory is used.", Arity = ArgumentArity.ZeroOrOne }; + var version = new Option("--version") { Description = "Displays the version of GitVersion" }; + var diagnose = new Option("--diagnose", "-d") { Description = "Runs GitVersion with additional diagnostic information" }; + var logFile = new Option("--log-file", "-l") { Description = "Path to logfile; specify 'console' to emit to stdout" }; + var output = new Option("--output", "-o") { Description = "Determines the output to the console. Can be 'json', 'file', 'buildserver' or 'dotenv'", AllowMultipleArgumentsPerToken = true, Arity = ArgumentArity.ZeroOrMore }; + var outputFile = new Option("--output-file") { Description = "Path to output file. Used in combination with --output 'file'" }; + var showVariable = new Option("--show-variable", "-v") { Description = "Output just a particular variable" }; + var format = new Option("--format", "-f") { Description = "Output a format containing version variables" }; + var config = new Option("--config", "-c") { Description = "Path to config file (defaults to GitVersion.yml)" }; + var showConfig = new Option("--show-config") { Description = "Outputs the effective GitVersion config in yaml format" }; + var overrideConfig = new Option("--override-config") { Description = "Overrides GitVersion config values inline (key=value pairs)", AllowMultipleArgumentsPerToken = false, Arity = ArgumentArity.ZeroOrMore }; + var targetPath = new Option("--target-path") { Description = "Same as 'path', but not positional" }; + var noFetch = new Option("--no-fetch") { Description = "Disables 'git fetch' during version calculation" }; + var noCache = new Option("--no-cache") { Description = "Bypasses the cache, result will not be written to the cache" }; + var noNormalize = new Option("--no-normalize") { Description = "Disables normalize step on a build server" }; + var allowShallow = new Option("--allow-shallow") { Description = "Allows GitVersion to run on a shallow clone" }; + var verbosity = new Option("--verbosity") { Description = "Specifies the amount of information to be displayed (Quiet, Minimal, Normal, Verbose, Diagnostic)" }; + var updateAssemblyInfo = new Option("--update-assembly-info") { Description = "Will recursively search for all 'AssemblyInfo.cs' files and update them", AllowMultipleArgumentsPerToken = true, Arity = ArgumentArity.ZeroOrMore }; + var updateProjectFiles = new Option("--update-project-files") { Description = "Will recursively search for all project files and update them", AllowMultipleArgumentsPerToken = true, Arity = ArgumentArity.ZeroOrMore }; + var ensureAssemblyInfo = new Option("--ensure-assembly-info") { Description = "If the assembly info file specified with --update-assembly-info is not found, it will be created" }; + var updateWixVersionFile = new Option("--update-wix-version-file") { Description = "All the GitVersion variables are written to 'GitVersion_WixVersion.wxi'" }; + var url = new Option("--url") { Description = "Url to remote git repository" }; + var branch = new Option("--branch", "-b") { Description = "Name of the branch to use on the remote repository" }; + var username = new Option("--username", "-u") { Description = "Username in case authentication is required" }; + var password = new Option("--password", "-p") { Description = "Password in case authentication is required" }; + var commit = new Option("--commit") { Description = "The commit id to check" }; + var dynamicRepoLocation = new Option("--dynamic-repo-location") { Description = "Override default dynamic repository clone location" }; var rootCommand = new RootCommand("Use convention to derive a SemVer product version from a GitFlow or GitHub based repository.") { - pathArgument, versionOption, diagnoseOption, logFileOption, - outputOption, - outputFileOption, - showVariableOption, - formatOption, - configOption, - showConfigOption, - overrideConfigOption, - targetPathOption, - noFetchOption, - noCacheOption, - noNormalizeOption, - allowShallowOption, - verbosityOption, - updateAssemblyInfoOption, - updateProjectFilesOption, - ensureAssemblyInfoOption, - updateWixVersionFileOption, - urlOption, - branchOption, - usernameOption, - passwordOption, - commitOption, - dynamicRepoLocationOption + path, + version, + diagnose, + logFile, + output, + outputFile, + showVariable, + format, + config, + showConfig, + overrideConfig, + targetPath, + noFetch, + noCache, + noNormalize, + allowShallow, + verbosity, + updateAssemblyInfo, + updateProjectFiles, + ensureAssemblyInfo, + updateWixVersionFile, + url, + branch, + username, + password, + commit, + dynamicRepoLocation }; - var options = new CommandOptions( - Path: pathArgument, - Version: versionOption, - Diagnose: diagnoseOption, - LogFile: logFileOption, - Output: outputOption, - OutputFile: outputFileOption, - ShowVariable: showVariableOption, - Format: formatOption, - Config: configOption, - ShowConfig: showConfigOption, - OverrideConfig: overrideConfigOption, - TargetPath: targetPathOption, - NoFetch: noFetchOption, - NoCache: noCacheOption, - NoNormalize: noNormalizeOption, - AllowShallow: allowShallowOption, - VerbosityOption: verbosityOption, - UpdateAssemblyInfo: updateAssemblyInfoOption, - UpdateProjectFiles: updateProjectFilesOption, - EnsureAssemblyInfo: ensureAssemblyInfoOption, - UpdateWixVersionFile: updateWixVersionFileOption, - Url: urlOption, - Branch: branchOption, - Username: usernameOption, - Password: passwordOption, - Commit: commitOption, - DynamicRepoLocation: dynamicRepoLocationOption - ); - - return (rootCommand, options); + return (rootCommand, new CommandOptions( + Path: path, Version: version, Diagnose: diagnose, LogFile: logFile, + Output: output, OutputFile: outputFile, ShowVariable: showVariable, Format: format, + Config: config, ShowConfig: showConfig, OverrideConfig: overrideConfig, TargetPath: targetPath, + NoFetch: noFetch, NoCache: noCache, NoNormalize: noNormalize, AllowShallow: allowShallow, + VerbosityOption: verbosity, UpdateAssemblyInfo: updateAssemblyInfo, UpdateProjectFiles: updateProjectFiles, + EnsureAssemblyInfo: ensureAssemblyInfo, UpdateWixVersionFile: updateWixVersionFile, + Url: url, Branch: branch, Username: username, Password: password, + Commit: commit, DynamicRepoLocation: dynamicRepoLocation + )); } private void AddAuthentication(Arguments arguments) { var username = this.environment.GetEnvironmentVariable("GITVERSION_REMOTE_USERNAME"); + var password = this.environment.GetEnvironmentVariable("GITVERSION_REMOTE_PASSWORD"); if (!username.IsNullOrWhiteSpace()) { arguments.Authentication.Username = username; } - var password = this.environment.GetEnvironmentVariable("GITVERSION_REMOTE_PASSWORD"); if (!password.IsNullOrWhiteSpace()) { arguments.Authentication.Password = password; } } - private IEnumerable ResolveFiles(string workingDirectory, ISet? assemblyInfoFiles) - { - if (assemblyInfoFiles == null) yield break; - - foreach (var file in assemblyInfoFiles) - { - foreach (var path in this.globbingResolver.Resolve(workingDirectory, file)) - { - yield return path; - } - } - } + private IEnumerable ResolveFiles(string workingDirectory, ISet? assemblyInfoFiles) => + assemblyInfoFiles?.SelectMany(file => this.globbingResolver.Resolve(workingDirectory, file)) + ?? []; private void ValidateConfigurationFile(Arguments arguments) { - if (arguments.ConfigurationFile.IsNullOrWhiteSpace()) return; + if (arguments.ConfigurationFile.IsNullOrWhiteSpace()) + { + return; + } if (FileSystemHelper.Path.IsPathRooted(arguments.ConfigurationFile)) { if (!this.fileSystem.File.Exists(arguments.ConfigurationFile)) + { throw new WarningException($"Could not find config file at '{arguments.ConfigurationFile}'"); + } + arguments.ConfigurationFile = FileSystemHelper.Path.GetFullPath(arguments.ConfigurationFile); } else @@ -614,26 +412,22 @@ private void ValidateConfigurationFile(Arguments arguments) var configFilePath = FileSystemHelper.Path.GetFullPath( FileSystemHelper.Path.Combine(arguments.TargetPath, arguments.ConfigurationFile)); if (!this.fileSystem.File.Exists(configFilePath)) + { throw new WarningException($"Could not find config file at '{configFilePath}'"); + } + arguments.ConfigurationFile = configFilePath; } } private static void ParseShowVariable(Arguments arguments, string value) { - string? versionVariable = null; - - if (!value.IsNullOrWhiteSpace()) - { - versionVariable = availableVariables.SingleOrDefault( - av => av.Equals(value.Replace("'", ""), StringComparison.CurrentCultureIgnoreCase)); - } + var versionVariable = value.IsNullOrWhiteSpace() ? null : availableVariables.SingleOrDefault(av => av.Equals(value.Replace("'", ""), StringComparison.CurrentCultureIgnoreCase)); if (versionVariable == null) { - var message = $"--show-variable requires a valid version variable. Available variables are:{FileSystemHelper.Path.NewLine}" + - string.Join(", ", availableVariables.Select(x => $"'{x}'")); - throw new WarningException(message); + var available = string.Join(", ", availableVariables.Select(x => $"'{x}'")); + throw new WarningException($"--show-variable requires a valid version variable. Available variables are:{FileSystemHelper.Path.NewLine}{available}"); } arguments.ShowVariable = versionVariable; @@ -641,16 +435,7 @@ private static void ParseShowVariable(Arguments arguments, string value) private static void ParseFormat(Arguments arguments, string value) { - if (value.IsNullOrWhiteSpace()) - { - throw new WarningException("Format requires a valid format string. Available variables are: " + - string.Join(", ", availableVariables)); - } - - var foundVariable = availableVariables.Any( - variable => value.Contains(variable, StringComparison.CurrentCultureIgnoreCase)); - - if (!foundVariable) + if (value.IsNullOrWhiteSpace() || !availableVariables.Any(v => value.Contains(v, StringComparison.CurrentCultureIgnoreCase))) { throw new WarningException("Format requires a valid format string. Available variables are: " + string.Join(", ", availableVariables)); @@ -672,7 +457,9 @@ internal static Verbosity ParseVerbosity(string? value) private static void ParseOverrideConfig(Arguments arguments, IReadOnlyCollection? values) { if (values == null || values.Count == 0) + { return; + } var parser = new OverrideConfigurationOptionParser(); @@ -681,15 +468,13 @@ private static void ParseOverrideConfig(Arguments arguments, IReadOnlyCollection var keyAndValue = QuotedStringHelpers.SplitUnquoted(keyValueOption, '='); if (keyAndValue.Length != 2) { - throw new WarningException( - $"Could not parse --override-config option: {keyValueOption}. Ensure it is in format 'key=value'."); + throw new WarningException($"Could not parse --override-config option: {keyValueOption}. Ensure it is in format 'key=value'."); } var optionKey = keyAndValue[0].ToLowerInvariant(); if (!OverrideConfigurationOptionParser.SupportedProperties.Contains(optionKey)) { - throw new WarningException( - $"Could not parse --override-config option: {keyValueOption}. Unsupported 'key'."); + throw new WarningException($"Could not parse --override-config option: {keyValueOption}. Unsupported 'key'."); } parser.SetValue(optionKey, keyAndValue[1]); From 80e7a8ab25d5cb2360576a43dffb73dfcceb0ecb Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Tue, 3 Mar 2026 00:02:11 +0100 Subject: [PATCH 11/24] refactor(gitversion): Adopt POSIX-style long arguments Migrates GitVersion command-line arguments to use double-dash long options for improved clarity and consistency with modern CLI standards. --- build/build/Tasks/Package/PackagePrepare.cs | 2 +- .../Addins/GitVersion/GitVersionRunner.cs | 20 +++++++------- .../Utilities/DockerContextExtensions.cs | 2 +- src/.run/cli (help).run.xml | 4 +-- src/.run/{cli.run.xml => cli (new).run.xml} | 4 +-- src/.run/cli (old).run.xml | 26 +++++++++++++++++++ src/.run/cli (showconfig).run.xml | 4 +-- src/.run/cli (version).run.xml | 4 +-- .../msbuild/tools/GitVersion.MsBuild.props | 10 +++---- tests/scripts/test-global-tool.sh | 2 +- tests/scripts/test-native-tool.sh | 2 +- 11 files changed, 53 insertions(+), 27 deletions(-) rename src/.run/{cli.run.xml => cli (new).run.xml} (86%) create mode 100644 src/.run/cli (old).run.xml diff --git a/build/build/Tasks/Package/PackagePrepare.cs b/build/build/Tasks/Package/PackagePrepare.cs index 0f32b29f08..430c4fcb3e 100644 --- a/build/build/Tasks/Package/PackagePrepare.cs +++ b/build/build/Tasks/Package/PackagePrepare.cs @@ -39,7 +39,7 @@ private static void PackPrepareNative(BuildContext context) { context.Information("Validating native lib:"); var nativeExe = outputPath.CombineWithFilePath(context.IsOnWindows ? "gitversion.exe" : "gitversion"); - context.ValidateOutput(nativeExe.FullPath, "/showvariable FullSemver", context.Version?.GitVersion?.FullSemVer); + context.ValidateOutput(nativeExe.FullPath, "--show-variable FullSemver", context.Version?.GitVersion?.FullSemVer); } } } diff --git a/build/common/Addins/GitVersion/GitVersionRunner.cs b/build/common/Addins/GitVersion/GitVersionRunner.cs index 7da7defc4d..d8731abb7c 100644 --- a/build/common/Addins/GitVersion/GitVersionRunner.cs +++ b/build/common/Addins/GitVersion/GitVersionRunner.cs @@ -64,19 +64,19 @@ private ProcessArgumentBuilder GetArguments(GitVersionSettings settings) if (settings.OutputTypes.Contains(GitVersionOutput.Json)) { - builder.Append("-output"); + builder.Append("--output"); builder.Append("json"); } if (settings.OutputTypes.Contains(GitVersionOutput.BuildServer)) { - builder.Append("-output"); + builder.Append("--output"); builder.Append("buildserver"); } if (!string.IsNullOrWhiteSpace(settings.ShowVariable)) { - builder.Append("-showvariable"); + builder.Append("--show-variable"); builder.Append(settings.ShowVariable); } @@ -91,7 +91,7 @@ private ProcessArgumentBuilder GetArguments(GitVersionSettings settings) if (settings.UpdateAssemblyInfo) { - builder.Append("-updateassemblyinfo"); + builder.Append("--update-assembly-info"); if (settings.UpdateAssemblyInfoFilePath != null) { @@ -101,12 +101,12 @@ private ProcessArgumentBuilder GetArguments(GitVersionSettings settings) if (settings.RepositoryPath != null) { - builder.Append("-targetpath"); + builder.Append("--target-path"); builder.AppendQuoted(settings.RepositoryPath.FullPath); } else if (!string.IsNullOrWhiteSpace(settings.Url)) { - builder.Append("-url"); + builder.Append("--url"); builder.AppendQuoted(settings.Url); if (!string.IsNullOrWhiteSpace(settings.Branch)) @@ -122,13 +122,13 @@ private ProcessArgumentBuilder GetArguments(GitVersionSettings settings) if (!string.IsNullOrWhiteSpace(settings.Commit)) { - builder.Append("-c"); + builder.Append("--commit"); builder.AppendQuoted(settings.Commit); } if (settings.DynamicRepositoryPath != null) { - builder.Append("-dynamicRepoLocation"); + builder.Append("--dynamic-repo-location"); builder.AppendQuoted(settings.DynamicRepositoryPath.FullPath); } } @@ -141,14 +141,14 @@ private ProcessArgumentBuilder GetArguments(GitVersionSettings settings) if (settings.NoFetch) { - builder.Append("-nofetch"); + builder.Append("--no-fetch"); } var verbosity = settings.Verbosity ?? this._log.Verbosity; if (verbosity != Verbosity.Normal) { - builder.Append("-verbosity"); + builder.Append("--verbosity"); builder.Append(verbosity.ToString()); } diff --git a/build/common/Utilities/DockerContextExtensions.cs b/build/common/Utilities/DockerContextExtensions.cs index eb8dd85a9d..4ffcd7ce33 100644 --- a/build/common/Utilities/DockerContextExtensions.cs +++ b/build/common/Utilities/DockerContextExtensions.cs @@ -156,7 +156,7 @@ public void DockerTestImage(DockerImage dockerImage) var tags = context.GetDockerTags(dockerImage, dockerImage.Architecture); foreach (var tag in tags) { - context.DockerTestRun(tag, dockerImage.Architecture, "/repo", ["/showvariable", "FullSemver", "/nocache"]); + context.DockerTestRun(tag, dockerImage.Architecture, "/repo", ["--show-variable", "FullSemver", "--no-cache"]); } } diff --git a/src/.run/cli (help).run.xml b/src/.run/cli (help).run.xml index a8457356e3..158dac3140 100644 --- a/src/.run/cli (help).run.xml +++ b/src/.run/cli (help).run.xml @@ -1,7 +1,7 @@ - \ No newline at end of file + diff --git a/src/.run/cli.run.xml b/src/.run/cli (new).run.xml similarity index 86% rename from src/.run/cli.run.xml rename to src/.run/cli (new).run.xml index 150a725934..6e7bf1f916 100644 --- a/src/.run/cli.run.xml +++ b/src/.run/cli (new).run.xml @@ -1,7 +1,7 @@ - + diff --git a/src/.run/cli (version).run.xml b/src/.run/cli (version).run.xml index 9afb33fcc3..9c32c06088 100644 --- a/src/.run/cli (version).run.xml +++ b/src/.run/cli (version).run.xml @@ -1,7 +1,7 @@ - \ No newline at end of file + diff --git a/src/GitVersion.MsBuild/msbuild/tools/GitVersion.MsBuild.props b/src/GitVersion.MsBuild/msbuild/tools/GitVersion.MsBuild.props index 0b63e3feee..7d0fbef6c2 100644 --- a/src/GitVersion.MsBuild/msbuild/tools/GitVersion.MsBuild.props +++ b/src/GitVersion.MsBuild/msbuild/tools/GitVersion.MsBuild.props @@ -13,11 +13,11 @@ false false - $(GitVersion_CommandLineArguments) -output file -outputfile "$(GitVersionOutputFile)" - $(GitVersion_ToolArgments) -nofetch - $(GitVersion_ToolArgments) -nonormalize - $(GitVersion_ToolArgments) -nocache - $(GitVersion_ToolArgments) -allowshallow + $(GitVersion_CommandLineArguments) --output file --output-file "$(GitVersionOutputFile)" + $(GitVersion_ToolArgments) --no-fetch + $(GitVersion_ToolArgments) --no-normalize + $(GitVersion_ToolArgments) --no-cache + $(GitVersion_ToolArgments) --allow-shallow diff --git a/tests/scripts/test-global-tool.sh b/tests/scripts/test-global-tool.sh index 778863663f..900545ba34 100644 --- a/tests/scripts/test-global-tool.sh +++ b/tests/scripts/test-global-tool.sh @@ -21,7 +21,7 @@ result=$(dotnet tool install GitVersion.Tool --version $version --tool-path /too status=$? if test $status -eq 0 then - /tools/dotnet-gitversion $repoPath /showvariable FullSemver /nocache + /tools/dotnet-gitversion $repoPath --show-variable FullSemver --no-cache else echo $result fi diff --git a/tests/scripts/test-native-tool.sh b/tests/scripts/test-native-tool.sh index 81aa2d9314..c19cc3cac6 100644 --- a/tests/scripts/test-native-tool.sh +++ b/tests/scripts/test-native-tool.sh @@ -21,7 +21,7 @@ result=$(tar -xvpf /native/gitversion-$runtime-$version.tar.gz -C /native) # >/d status=$? if test $status -eq 0 then - /native/gitversion $repoPath /showvariable FullSemver /nocache + /native/gitversion $repoPath --show-variable FullSemver --no-cache else echo $result fi From bf91dae284920bb8df0000ccae1696c3eb716025 Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Tue, 3 Mar 2026 13:58:14 +0100 Subject: [PATCH 12/24] feat(cli): Add toggle for legacy argument parser Introduce an environment variable to switch between argument parser implementations. --- src/GitVersion.App/CliHost.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/GitVersion.App/CliHost.cs b/src/GitVersion.App/CliHost.cs index 5b81c86d1b..88fdf06969 100644 --- a/src/GitVersion.App/CliHost.cs +++ b/src/GitVersion.App/CliHost.cs @@ -34,6 +34,9 @@ private static void RegisterGitVersionModules(IServiceCollection services, strin services.AddModule(new GitVersionOutputModule()); services.AddModule(new GitVersionLibGit2SharpModule()); - services.AddModule(new GitVersionAppModule(args)); + + var envValue = SysEnv.GetEnvironmentVariable("USE_V6_ARGUMENT_PARSER"); + var useLegacyParser = string.Equals(envValue, "true", StringComparison.OrdinalIgnoreCase); + services.AddModule(new GitVersionAppModule(args, useLegacyParser)); } } From 96d1a994fd6e9a1b6484499f61e0eb279a640cfa Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Tue, 3 Mar 2026 15:51:50 +0100 Subject: [PATCH 13/24] docs(breaking-changes): Document CLI and logging refactors Include details on the POSIX-style CLI arguments --- BREAKING_CHANGES.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index c6da444ae8..0aeea65c42 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -1,5 +1,47 @@ ## Unreleased +### CLI Arguments — POSIX-style Syntax + +The command-line interface has been migrated from Windows-style (`/switch` and single-dash `-switch`) arguments to POSIX-style `--long-name` arguments using [System.CommandLine](https://github.com/dotnet/command-line-api). + +**Old-style arguments are no longer accepted by default.** Update any scripts, CI pipelines, or tooling accordingly. + +As a temporary migration aid, set the environment variable `USE_V6_ARGUMENT_PARSER=true` to restore the legacy `/switch` and `-switch` argument handling. This escape hatch will be removed in a future release. + +#### Full argument mapping + +| Old argument | New argument | Short alias | Env var alternative | +| ----------------------------- | -------------------------------- | ------------------------------ | ---------------------------- | +| `/targetpath ` | `--target-path ` | _(positional `path` argument)_ | | +| `/output ` | `--output ` | `-o` | | +| `/outputfile ` | `--output-file ` | | | +| `/showvariable ` | `--show-variable ` | `-v` | | +| `/format ` | `--format ` | `-f` | | +| `/config ` | `--config ` | `-c` | | +| `/showconfig` | `--show-config` | | | +| `/overrideconfig ` | `--override-config ` | | | +| `/nocache` | `--no-cache` | | | +| `/nofetch` | `--no-fetch` | | | +| `/nonormalize` | `--no-normalize` | | | +| `/allowshallow` | `--allow-shallow` | | | +| `/verbosity ` | `--verbosity ` | | | +| `/l ` | `--log-file ` | `-l` | | +| `/diag` | `--diagnose` | `-d` | | +| `/updateassemblyinfo [files]` | `--update-assembly-info [files]` | | | +| `/updateprojectfiles [files]` | `--update-project-files [files]` | | | +| `/ensureassemblyinfo` | `--ensure-assembly-info` | | | +| `/updatewixversionfile` | `--update-wix-version-file` | | | +| `/url ` | `--url ` | | | +| `/b ` | `--branch ` | `-b` | | +| `/u ` | `--username ` | `-u` | `GITVERSION_REMOTE_USERNAME` | +| `/p ` | `--password ` | `-p` | `GITVERSION_REMOTE_PASSWORD` | +| `/c ` | `--commit ` | _(no short alias)_ | | +| `/dynamicRepoLocation ` | `--dynamic-repo-location ` | | | + +> **Critical**: `-c` previously referred to the commit id. It is now aliased to `--config` (config file path). Any usage of `-c ` must be changed to `--commit `. + +The `GITVERSION_REMOTE_USERNAME` and `GITVERSION_REMOTE_PASSWORD` environment variables can be used as alternatives to `--username` and `--password`. Environment variables take lower precedence than explicit CLI arguments. + ### Logging System Replacement * The custom `ILog` logging abstraction has been replaced with the industry-standard `Microsoft.Extensions.Logging` (M.E.L.) infrastructure using Serilog as the underlying provider. From bca042c84a7cacf99df4f39809281922a537ac2a Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Tue, 3 Mar 2026 16:04:51 +0100 Subject: [PATCH 14/24] fix(parser): Clarify -c and --commit argument mapping Adds tests to confirm -c maps to --config and --commit sets the commit ID, aligning with updated parser behavior. --- .../ArgumentParserTests.cs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/GitVersion.App.Tests/ArgumentParserTests.cs b/src/GitVersion.App.Tests/ArgumentParserTests.cs index e6e396d3a8..738a531896 100644 --- a/src/GitVersion.App.Tests/ArgumentParserTests.cs +++ b/src/GitVersion.App.Tests/ArgumentParserTests.cs @@ -213,7 +213,7 @@ public void OutputFileArgumentCanBeParsed(string args, string outputFile) [Test] public void UrlAndBranchNameCanBeParsed() { - var arguments = this.argumentParser.ParseArguments("targetDirectoryPath --url https://github.com/Particular/GitVersion.git -b someBranch"); + var arguments = this.argumentParser.ParseArguments("targetDirectoryPath --url https://github.com/Particular/GitVersion.git --branch someBranch"); arguments.TargetPath.ShouldBe("targetDirectoryPath"); arguments.TargetUrl.ShouldBe("https://github.com/Particular/GitVersion.git"); arguments.TargetBranch.ShouldBe("someBranch"); @@ -826,4 +826,27 @@ public void EnsureConfigurationFileIsSet() arguments.ConfigurationFile.ShouldBe(configFile); this.fileSystem.File.Delete(configFile); } + + [Test] + public void ShortAliasCIsConfigNotCommit() + { + // -c is aliased to --config, NOT --commit (breaking change from v6) + var configFile = FileSystemHelper.Path.GetTempPath() + Guid.NewGuid() + ".yaml"; + this.fileSystem.File.WriteAllText(configFile, "next-version: 1.0.0"); + + var arguments = this.argumentParser.ParseArguments($"-c {configFile}"); + + arguments.ConfigurationFile.ShouldBe(configFile); + arguments.CommitId.ShouldBeNullOrEmpty(); + + this.fileSystem.File.Delete(configFile); + } + + [Test] + public void CommitLongFormSetsCommitId() + { + var arguments = this.argumentParser.ParseArguments("--url https://github.com/GitTools/GitVersion.git --branch main --commit abc1234"); + arguments.CommitId.ShouldBe("abc1234"); + arguments.ConfigurationFile.ShouldBeNull(); + } } From 358aaf22339a9d7a689551ccece72bc65f46f55f Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Tue, 3 Mar 2026 16:21:57 +0100 Subject: [PATCH 15/24] feat(cli): Introduce POSIX-style argument parsing and documentation Add a new `arguments.md` file detailing POSIX-style CLI arguments. Create separate run configurations to facilitate testing both existing and new argument parsing behaviors. --- ...help).run.xml => cli (help - new).run.xml} | 4 +- src/.run/cli (help - old).run.xml | 26 ++++++ ...n).run.xml => cli (version - new).run.xml} | 6 +- src/.run/cli (version - old).run.xml | 26 ++++++ src/GitVersion.App/ArgumentParser.cs | 4 +- src/GitVersion.App/GitVersion.App.csproj | 2 +- src/GitVersion.App/HelpWriter.cs | 9 +- src/GitVersion.App/arguments.md | 85 +++++++++++++++++++ 8 files changed, 151 insertions(+), 11 deletions(-) rename src/.run/{cli (help).run.xml => cli (help - new).run.xml} (90%) create mode 100644 src/.run/cli (help - old).run.xml rename src/.run/{cli (version).run.xml => cli (version - new).run.xml} (82%) create mode 100644 src/.run/cli (version - old).run.xml create mode 100644 src/GitVersion.App/arguments.md diff --git a/src/.run/cli (help).run.xml b/src/.run/cli (help - new).run.xml similarity index 90% rename from src/.run/cli (help).run.xml rename to src/.run/cli (help - new).run.xml index 158dac3140..d064788803 100644 --- a/src/.run/cli (help).run.xml +++ b/src/.run/cli (help - new).run.xml @@ -1,5 +1,5 @@ - + - + \ No newline at end of file diff --git a/src/.run/cli (help - old).run.xml b/src/.run/cli (help - old).run.xml new file mode 100644 index 0000000000..b6eedd55fa --- /dev/null +++ b/src/.run/cli (help - old).run.xml @@ -0,0 +1,26 @@ + + + + \ No newline at end of file diff --git a/src/.run/cli (version).run.xml b/src/.run/cli (version - new).run.xml similarity index 82% rename from src/.run/cli (version).run.xml rename to src/.run/cli (version - new).run.xml index 9c32c06088..7c2a4b5097 100644 --- a/src/.run/cli (version).run.xml +++ b/src/.run/cli (version - new).run.xml @@ -1,8 +1,8 @@ - + - + \ No newline at end of file diff --git a/src/.run/cli (version - old).run.xml b/src/.run/cli (version - old).run.xml new file mode 100644 index 0000000000..c390bb64ad --- /dev/null +++ b/src/.run/cli (version - old).run.xml @@ -0,0 +1,26 @@ + + + + \ No newline at end of file diff --git a/src/GitVersion.App/ArgumentParser.cs b/src/GitVersion.App/ArgumentParser.cs index 72230a8cad..9d5fe19e37 100644 --- a/src/GitVersion.App/ArgumentParser.cs +++ b/src/GitVersion.App/ArgumentParser.cs @@ -56,7 +56,7 @@ public Arguments ParseArguments(string[] commandLineArguments) var (rootCommand, options) = commandFactory.Value; // Let System.CommandLine handle --help output natively - if (commandLineArguments.Any(a => a is "--help" or "-h" or "-?" or "/?")) + if (commandLineArguments.Any(a => a is "--help" or "-h")) { PrintBuiltInHelp(rootCommand); return new Arguments { IsHelp = true }; @@ -121,7 +121,7 @@ public Arguments ParseArguments(string[] commandLineArguments) private static void PrintBuiltInHelp(RootCommand rootCommand) { rootCommand.SetAction((_, _) => Task.FromResult(0)); - rootCommand.Parse(["--help"]).InvokeAsync().GetAwaiter().GetResult(); + rootCommand.Parse(["--help"]).Invoke(); } private void PrintBuiltInVersion() diff --git a/src/GitVersion.App/GitVersion.App.csproj b/src/GitVersion.App/GitVersion.App.csproj index 4837272ee9..f9db5ec749 100644 --- a/src/GitVersion.App/GitVersion.App.csproj +++ b/src/GitVersion.App/GitVersion.App.csproj @@ -37,7 +37,7 @@
- + diff --git a/src/GitVersion.App/HelpWriter.cs b/src/GitVersion.App/HelpWriter.cs index d57a49f304..3bc135e449 100644 --- a/src/GitVersion.App/HelpWriter.cs +++ b/src/GitVersion.App/HelpWriter.cs @@ -1,5 +1,4 @@ using GitVersion.Extensions; -using GitVersion.Helpers; namespace GitVersion; @@ -17,7 +16,11 @@ public void WriteTo(Action writeAction) this.versionWriter.WriteTo(assembly, v => version = v); var args = LegacyArgumentList(); - var message = $"GitVersion {version}{FileSystemHelper.Path.NewLine}{FileSystemHelper.Path.NewLine}{args}"; + var message = $""" + GitVersion {version} + + {args} + """; writeAction(message); } @@ -28,7 +31,7 @@ private string LegacyArgumentList() argumentsMarkdownStream.NotNull(); using var sr = new StreamReader(argumentsMarkdownStream); var argsMarkdown = sr.ReadToEnd(); - var codeBlockStart = argsMarkdown.IndexOf("```", StringComparison.Ordinal) + 3; + var codeBlockStart = argsMarkdown.IndexOf("```bash", StringComparison.Ordinal) + 7; var codeBlockEnd = argsMarkdown.LastIndexOf("```", StringComparison.Ordinal) - codeBlockStart; return argsMarkdown.Substring(codeBlockStart, codeBlockEnd).Trim(); } diff --git a/src/GitVersion.App/arguments.md b/src/GitVersion.App/arguments.md new file mode 100644 index 0000000000..6c1025460d --- /dev/null +++ b/src/GitVersion.App/arguments.md @@ -0,0 +1,85 @@ +```bash +Use convention to derive a SemVer product version from a GitFlow or GitHub based +repository. + +GitVersion [path] + + path The directory containing .git. If not defined current + directory is used. (Must be first argument) + /version Displays the version of GitVersion + /diag Runs GitVersion with additional diagnostic information; + also needs the '/l' argument to specify a logfile or stdout + (requires git.exe to be installed) + /h or /? Shows Help + + /targetpath Same as 'path', but not positional + /output Determines the output to the console. Can be either 'json', + 'file', 'buildserver' or 'dotenv', will default to 'json'. + /outputfile Path to output file. It is used in combination with /output + 'file'. + /showvariable Used in conjunction with /output json, will output just a + particular variable. E.g. /output json /showvariable SemVer + - will output `1.2.3+beta.4` + /format Used in conjunction with /output json, will output a format + containing version variables. + Supports C# format strings - see [Format Strings](/docs/reference/custom-formatting) for details. + E.g. /output json /format {SemVer} - will output `1.2.3+beta.4` + /output json /format {Major}.{Minor} - will output `1.2` + /l Path to logfile; specify 'console' to emit to stdout. + /config Path to config file (defaults to GitVersion.yml, GitVersion.yaml, .GitVersion.yml or .GitVersion.yaml) + /showconfig Outputs the effective GitVersion config (defaults + custom + from GitVersion.yml, GitVersion.yaml, .GitVersion.yml or .GitVersion.yaml) in yaml format + /overrideconfig Overrides GitVersion config values inline (semicolon- + separated key value pairs e.g. /overrideconfig + tag-prefix=Foo) + Currently supported config overrides: tag-prefix + /nocache Bypasses the cache, result will not be written to the cache. + /nonormalize Disables normalize step on a build server. + /allowshallow Allows GitVersion to run on a shallow clone. + This is not recommended, but can be used if you are sure + that the shallow clone contains all the information needed + to calculate the version. + /verbosity Specifies the amount of information to be displayed. + (Quiet, Minimal, Normal, Verbose, Diagnostic) + Default is Normal + +# AssemblyInfo updating + + /updateassemblyinfo + Will recursively search for all 'AssemblyInfo.cs' files in + the git repo and update them + /updateprojectfiles + Will recursively search for all project files + (.csproj/.vbproj/.fsproj/.sqlproj) files in the git repo and update + them + Note: This is only compatible with the newer Sdk projects + /ensureassemblyinfo + If the assembly info file specified with + /updateassemblyinfo is not + found, it will be created with these attributes: + AssemblyFileVersion, AssemblyVersion and + AssemblyInformationalVersion. + Supports writing version info for: C#, F#, VB + +# Create or update Wix version file + + /updatewixversionfile + All the GitVersion variables are written to + 'GitVersion_WixVersion.wxi'. The variables can then be + referenced in other WiX project files for versioning. + +# Remote repository args + + /url Url to remote git repository. + /b Name of the branch to use on the remote repository, must be + used in combination with /url. + /u Username in case authentication is required. + /p Password in case authentication is required. + /c The commit id to check. If not specified, the latest + available commit on the specified branch will be used. + /dynamicRepoLocation + By default dynamic repositories will be cloned to %tmp%. + Use this switch to override + /nofetch Disables 'git fetch' during version calculation. Might cause + GitVersion to not calculate your version as expected. +``` From dd8f3f0747dd14d5f89be9d74fc69fb9c7d561dc Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Tue, 3 Mar 2026 18:12:17 +0100 Subject: [PATCH 16/24] test(arguments): Update help writer test for legacy parser Ensures HelpWriterTests correctly asserts /switch argument syntax when configured for the legacy parser. --- src/GitVersion.App.Tests/HelpWriterTests.cs | 52 ++++++++++----------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/src/GitVersion.App.Tests/HelpWriterTests.cs b/src/GitVersion.App.Tests/HelpWriterTests.cs index cf8b3a7852..0b1a6e26e2 100644 --- a/src/GitVersion.App.Tests/HelpWriterTests.cs +++ b/src/GitVersion.App.Tests/HelpWriterTests.cs @@ -18,35 +18,35 @@ public void AllArgsAreInHelp() { var lookup = new Dictionary { - { nameof(Arguments.IsHelp), "--help" }, - { nameof(Arguments.IsVersion), "--version" }, + { nameof(Arguments.IsHelp), "/?" }, + { nameof(Arguments.IsVersion), "/version" }, - { nameof(Arguments.TargetUrl), "--url" }, - { nameof(Arguments.TargetBranch), "--branch" }, - { nameof(Arguments.ClonePath), "--dynamic-repo-location" }, - { nameof(Arguments.CommitId), "--commit" }, + { nameof(Arguments.TargetUrl), "/url" }, + { nameof(Arguments.TargetBranch), "/b" }, + { nameof(Arguments.ClonePath), "/dynamicRepoLocation" }, + { nameof(Arguments.CommitId), "/c" }, - { nameof(Arguments.Diag), "--diagnose" }, - { nameof(Arguments.LogFilePath), "--log-file" }, - { "verbosity", "--verbosity" }, - { nameof(Arguments.Output), "--output" }, - { nameof(Arguments.OutputFile), "--output-file" }, - { nameof(Arguments.ShowVariable), "--show-variable" }, - { nameof(Arguments.Format), "--format" }, + { nameof(Arguments.Diag) , "/diag" }, + { nameof(Arguments.LogFilePath) , "/l" }, + { "verbosity", "/verbosity" }, + { nameof(Arguments.Output) , "/output" }, + { nameof(Arguments.OutputFile) , "/outputfile" }, + { nameof(Arguments.ShowVariable), "/showvariable" }, + { nameof(Arguments.Format), "/format" }, - { nameof(Arguments.UpdateWixVersionFile), "--update-wix-version-file" }, - { nameof(Arguments.UpdateProjectFiles), "--update-project-files" }, - { nameof(Arguments.UpdateAssemblyInfo), "--update-assembly-info" }, - { nameof(Arguments.EnsureAssemblyInfo), "--ensure-assembly-info" }, + { nameof(Arguments.UpdateWixVersionFile), "/updatewixversionfile" }, + { nameof(Arguments.UpdateProjectFiles), "/updateprojectfiles" }, + { nameof(Arguments.UpdateAssemblyInfo), "/updateassemblyinfo" }, + { nameof(Arguments.EnsureAssemblyInfo), "/ensureassemblyinfo" }, - { nameof(Arguments.ConfigurationFile), "--config" }, - { nameof(Arguments.ShowConfiguration), "--show-config" }, - { nameof(Arguments.OverrideConfiguration), "--override-config" }, + { nameof(Arguments.ConfigurationFile), "/config" }, + { nameof(Arguments.ShowConfiguration), "/showconfig" }, + { nameof(Arguments.OverrideConfiguration), "/overrideconfig" }, - { nameof(Arguments.NoCache), "--no-cache" }, - { nameof(Arguments.NoFetch), "--no-fetch" }, - { nameof(Arguments.NoNormalize), "--no-normalize" }, - { nameof(Arguments.AllowShallow), "--allow-shallow" } + { nameof(Arguments.NoCache), "/nocache" }, + { nameof(Arguments.NoFetch), "/nofetch" }, + { nameof(Arguments.NoNormalize), "/nonormalize" }, + { nameof(Arguments.AllowShallow), "/allowshallow" } }; var helpText = string.Empty; @@ -69,8 +69,6 @@ private static bool IsNotInHelp(Dictionary lookup, string proper if (lookup.TryGetValue(propertyName, out var value)) return !helpText.Contains(value); - // Fallback: convert PascalCase to kebab-case and check for --option - var kebab = System.Text.RegularExpressions.Regex.Replace(propertyName, "(? Date: Tue, 3 Mar 2026 22:29:03 +0100 Subject: [PATCH 17/24] feat(cli): enhance argument descriptions and add help wrapping Improve clarity and detail of argument descriptions in the CLI parser. Configure help system to wrap text for better readability in large outputs. --- src/GitVersion.App/ArgumentParser.cs | 168 ++++++++++++++++++++++----- 1 file changed, 138 insertions(+), 30 deletions(-) diff --git a/src/GitVersion.App/ArgumentParser.cs b/src/GitVersion.App/ArgumentParser.cs index 9d5fe19e37..f92505e804 100644 --- a/src/GitVersion.App/ArgumentParser.cs +++ b/src/GitVersion.App/ArgumentParser.cs @@ -1,4 +1,5 @@ using System.CommandLine; +using System.CommandLine.Help; using System.IO.Abstractions; using GitVersion.Extensions; using GitVersion.FileSystemGlobbing; @@ -301,38 +302,142 @@ private static bool IsBooleanTrue(string value) => private static (RootCommand Root, CommandOptions Options) BuildCommand() { - var path = new Argument("path") { Description = "The directory containing .git. If not defined current directory is used.", Arity = ArgumentArity.ZeroOrOne }; - var version = new Option("--version") { Description = "Displays the version of GitVersion" }; - var diagnose = new Option("--diagnose", "-d") { Description = "Runs GitVersion with additional diagnostic information" }; - var logFile = new Option("--log-file", "-l") { Description = "Path to logfile; specify 'console' to emit to stdout" }; - var output = new Option("--output", "-o") { Description = "Determines the output to the console. Can be 'json', 'file', 'buildserver' or 'dotenv'", AllowMultipleArgumentsPerToken = true, Arity = ArgumentArity.ZeroOrMore }; - var outputFile = new Option("--output-file") { Description = "Path to output file. Used in combination with --output 'file'" }; - var showVariable = new Option("--show-variable", "-v") { Description = "Output just a particular variable" }; - var format = new Option("--format", "-f") { Description = "Output a format containing version variables" }; - var config = new Option("--config", "-c") { Description = "Path to config file (defaults to GitVersion.yml)" }; - var showConfig = new Option("--show-config") { Description = "Outputs the effective GitVersion config in yaml format" }; - var overrideConfig = new Option("--override-config") { Description = "Overrides GitVersion config values inline (key=value pairs)", AllowMultipleArgumentsPerToken = false, Arity = ArgumentArity.ZeroOrMore }; - var targetPath = new Option("--target-path") { Description = "Same as 'path', but not positional" }; - var noFetch = new Option("--no-fetch") { Description = "Disables 'git fetch' during version calculation" }; - var noCache = new Option("--no-cache") { Description = "Bypasses the cache, result will not be written to the cache" }; - var noNormalize = new Option("--no-normalize") { Description = "Disables normalize step on a build server" }; - var allowShallow = new Option("--allow-shallow") { Description = "Allows GitVersion to run on a shallow clone" }; - var verbosity = new Option("--verbosity") { Description = "Specifies the amount of information to be displayed (Quiet, Minimal, Normal, Verbose, Diagnostic)" }; - var updateAssemblyInfo = new Option("--update-assembly-info") { Description = "Will recursively search for all 'AssemblyInfo.cs' files and update them", AllowMultipleArgumentsPerToken = true, Arity = ArgumentArity.ZeroOrMore }; - var updateProjectFiles = new Option("--update-project-files") { Description = "Will recursively search for all project files and update them", AllowMultipleArgumentsPerToken = true, Arity = ArgumentArity.ZeroOrMore }; - var ensureAssemblyInfo = new Option("--ensure-assembly-info") { Description = "If the assembly info file specified with --update-assembly-info is not found, it will be created" }; - var updateWixVersionFile = new Option("--update-wix-version-file") { Description = "All the GitVersion variables are written to 'GitVersion_WixVersion.wxi'" }; - var url = new Option("--url") { Description = "Url to remote git repository" }; - var branch = new Option("--branch", "-b") { Description = "Name of the branch to use on the remote repository" }; - var username = new Option("--username", "-u") { Description = "Username in case authentication is required" }; - var password = new Option("--password", "-p") { Description = "Password in case authentication is required" }; - var commit = new Option("--commit") { Description = "The commit id to check" }; - var dynamicRepoLocation = new Option("--dynamic-repo-location") { Description = "Override default dynamic repository clone location" }; + var path = new Argument("path") + { + Description = "The directory containing .git. If not defined current directory is used. (Must be first argument)", + Arity = ArgumentArity.ZeroOrOne + }; + var diagnose = new Option("--diagnose", "-d") + { + Description = """ + Runs GitVersion with additional diagnostic information. + Also needs the '--log-file' argument to specify a logfile or stdout (requires git.exe to be installed) + """ + }; + var logFile = new Option("--log-file", "-l") + { + Description = "Path to logfile; specify 'console' to emit to stdout" + }; + var output = new Option("--output", "-o") + { + Description = "Determines the output to the console. Can be either 'json', 'file', 'buildserver' or 'dotenv', will default to 'json'", + AllowMultipleArgumentsPerToken = true, + Arity = ArgumentArity.ZeroOrMore + }; + var outputFile = new Option("--output-file") + { + Description = "Path to output file. It is used in combination with --output 'file'" + }; + var showVariable = new Option("--show-variable", "-v") + { + Description = """ + Used in conjunction with --output json, will output just a particular variable. + E.g. --output json --show-variable SemVer - will output `1.2.3+beta.4` + """ + }; + var format = new Option("--format", "-f") + { + Description = """ + Used in conjunction with --output json, will output a format containing version variables. + Supports C# format strings - see [Format Strings](/docs/reference/custom-formatting) for details. + E.g. --output json --format {SemVer} - will output `1.2.3+beta.4` + --output json --format {Major}.{Minor} - will output `1.2` + """ + }; + var config = new Option("--config", "-c") + { + Description = "Path to config file (defaults to GitVersion.yml, GitVersion.yaml, .GitVersion.yml or .GitVersion.yaml)" + }; + var showConfig = new Option("--show-config") + { + Description = "Outputs the effective GitVersion config (defaults + custom from GitVersion.yml) in yaml format" + }; + var overrideConfig = new Option("--override-config") + { + Description = "Overrides GitVersion config values inline (key=value pairs e.g. --override-config tag-prefix=Foo)", + AllowMultipleArgumentsPerToken = false, + Arity = ArgumentArity.ZeroOrMore + }; + var targetPath = new Option("--target-path") + { + Description = "Same as 'path', but not positional" + }; + var noFetch = new Option("--no-fetch") + { + Description = "Disables 'git fetch' during version calculation. Might cause GitVersion to not calculate your version as expected" + }; + var noCache = new Option("--no-cache") + { + Description = "Bypasses the cache, result will not be written to the cache" + }; + var noNormalize = new Option("--no-normalize") + { + Description = "Disables normalize step on a build server" + }; + var allowShallow = new Option("--allow-shallow") + { + Description = """ + Allows GitVersion to run on a shallow clone. + This is not recommended, but can be used if you are sure that the shallow clone contains all the information needed to calculate the version. + """ + }; + var verbosity = new Option("--verbosity") + { + Description = "Specifies the amount of information to be displayed (Quiet, Minimal, Normal, Verbose, Diagnostic). Default is Normal" + }; + var updateAssemblyInfo = new Option("--update-assembly-info") + { + Description = "Will recursively search for all 'AssemblyInfo.cs' files in the git repo and update them", + AllowMultipleArgumentsPerToken = true, + Arity = ArgumentArity.ZeroOrMore + }; + var updateProjectFiles = new Option("--update-project-files") + { + Description = """ + Will recursively search for all project files (.csproj/.vbproj/.fsproj/.sqlproj) in the git repo and update them (only compatible with Sdk projects) + """, + AllowMultipleArgumentsPerToken = true, + Arity = ArgumentArity.ZeroOrMore + }; + var ensureAssemblyInfo = new Option("--ensure-assembly-info") + { + Description = """ + If the assembly info file specified with --update-assembly-info is not found, it will be created with AssemblyFileVersion, AssemblyVersion and AssemblyInformationalVersion. + Supports C#, F#, VB + """ + }; + var updateWixVersionFile = new Option("--update-wix-version-file") + { + Description = "All the GitVersion variables are written to 'GitVersion_WixVersion.wxi'" + }; + var url = new Option("--url") + { + Description = "Url to remote git repository" + }; + var branch = new Option("--branch", "-b") + { + Description = "Name of the branch to use on the remote repository, must be used in combination with --url" + }; + var username = new Option("--username", "-u") + { + Description = "Username in case authentication is required" + }; + var password = new Option("--password", "-p") + { + Description = "Password in case authentication is required" + }; + var commit = new Option("--commit") + { + Description = "The commit id to check. If not specified, the latest available commit on the specified branch will be used" + }; + var dynamicRepoLocation = new Option("--dynamic-repo-location") + { + Description = "By default dynamic repositories will be cloned to %tmp%. Use this option to override" + }; var rootCommand = new RootCommand("Use convention to derive a SemVer product version from a GitFlow or GitHub based repository.") { path, - version, diagnose, logFile, output, @@ -360,8 +465,12 @@ private static (RootCommand Root, CommandOptions Options) BuildCommand() dynamicRepoLocation }; + // Configure the built-in help system to wrap at 260 characters to avoid too small help messages + var helpOption = rootCommand.Options.SingleOfType(); + helpOption.Action = new HelpAction { MaxWidth = 260 }; + return (rootCommand, new CommandOptions( - Path: path, Version: version, Diagnose: diagnose, LogFile: logFile, + Path: path, Diagnose: diagnose, LogFile: logFile, Output: output, OutputFile: outputFile, ShowVariable: showVariable, Format: format, Config: config, ShowConfig: showConfig, OverrideConfig: overrideConfig, TargetPath: targetPath, NoFetch: noFetch, NoCache: noCache, NoNormalize: noNormalize, AllowShallow: allowShallow, @@ -485,7 +594,6 @@ private static void ParseOverrideConfig(Arguments arguments, IReadOnlyCollection private sealed record CommandOptions( Argument Path, - Option Version, Option Diagnose, Option LogFile, Option Output, From 057604030091b93bfa07f825733797284e012fd2 Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Wed, 4 Mar 2026 11:43:06 +0100 Subject: [PATCH 18/24] refactor(cli): Rename legacy argument help file --- src/GitVersion.App/GitVersion.App.csproj | 2 +- src/GitVersion.App/HelpWriter.cs | 2 +- src/GitVersion.App/{arguments.md => legacy_help.md} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/GitVersion.App/{arguments.md => legacy_help.md} (100%) diff --git a/src/GitVersion.App/GitVersion.App.csproj b/src/GitVersion.App/GitVersion.App.csproj index f9db5ec749..9fe38aa38a 100644 --- a/src/GitVersion.App/GitVersion.App.csproj +++ b/src/GitVersion.App/GitVersion.App.csproj @@ -37,7 +37,7 @@ - + diff --git a/src/GitVersion.App/HelpWriter.cs b/src/GitVersion.App/HelpWriter.cs index 3bc135e449..7f95db3398 100644 --- a/src/GitVersion.App/HelpWriter.cs +++ b/src/GitVersion.App/HelpWriter.cs @@ -27,7 +27,7 @@ public void WriteTo(Action writeAction) private string LegacyArgumentList() { - using var argumentsMarkdownStream = GetType().Assembly.GetManifestResourceStream("GitVersion.arguments.md"); + using var argumentsMarkdownStream = GetType().Assembly.GetManifestResourceStream("GitVersion.legacy_help.md"); argumentsMarkdownStream.NotNull(); using var sr = new StreamReader(argumentsMarkdownStream); var argsMarkdown = sr.ReadToEnd(); diff --git a/src/GitVersion.App/arguments.md b/src/GitVersion.App/legacy_help.md similarity index 100% rename from src/GitVersion.App/arguments.md rename to src/GitVersion.App/legacy_help.md From 2072fb899b337ff035296235ce9e0e21e36bb583 Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Wed, 4 Mar 2026 12:46:37 +0100 Subject: [PATCH 19/24] refactor(cli): Delegate help and version handling to System.CommandLine --- src/.run/cli (help - new).run.xml | 2 +- src/.run/cli (new).run.xml | 2 +- src/.run/cli (old).run.xml | 4 +- src/GitVersion.App/ArgumentParser.cs | 55 ++++++++-------------------- 4 files changed, 19 insertions(+), 44 deletions(-) diff --git a/src/.run/cli (help - new).run.xml b/src/.run/cli (help - new).run.xml index d064788803..8639188b7d 100644 --- a/src/.run/cli (help - new).run.xml +++ b/src/.run/cli (help - new).run.xml @@ -1,7 +1,7 @@ - \ No newline at end of file + diff --git a/src/.run/cli (old).run.xml b/src/.run/cli (old).run.xml index 16e0455cbc..db90572a9c 100644 --- a/src/.run/cli (old).run.xml +++ b/src/.run/cli (old).run.xml @@ -5,7 +5,7 @@