[Feature] New CLI
Detailed Description
This issue is our explorations of how the new CLI of GitVersion ~~6~~ 7 is going to look like.
Design
The new CLI is going to have a command-based style, allowing for more laser-focused and composable execution. We want stdin and stdout to become the default way to both read and write version variables, logging instead to stderr so stdout can be kept as valid JSON throughout.
Since we want to reduce the amount of work being done for each command, GitVersion may have to be executed several times for certain builds, perhaps introducing some latency, but also making it much easier to pinpoint exactly during which step an error occurs so we can fix more easily and users of GitVersion can circumvent and monkey-patch the bugs much more efficiently.
Architecture
The new and more laser-focused command-based CLI allows us to refactor the GitVersion codebase into smaller, more compartementalized parts. This should lead to an almost dependency-free Core with a few optional addon projects that can live their own lives. The API between GitVersion.Core and the addons will be based on the CLI infrastructure we choose. Our current bet is on System.CommandLine.
Addons will be able to add their own commands to GitVersion and besides the Command implementations and arguments they can add to it, they will be able to read the version variables produced by GitVersion as JSON and then perform whatever task they wish. The output from an addon should be JSON written to stdout so different commands can be composed with piping.
Possible Implementation
Below is an example of some of the commands the new CLI may support, with some of their corresponding arguments.
# Write version to stdout
gitversion --version
# Write help to stdout
gitversion --help
# Normalize the repository to its required state:
gitversion normalize
# Normalize the repository inside `./project/` to its required state:
gitversion normalize --repository ./project/
# Initialize GitVersion.yml
gitversion config init
# Write the effective GitVersion configuration (defaults + custom from GitVersion.yml) in yaml format to stdout
gitversion config show
# Calculate the version number and output to stdout. Only the JSON with the version variables will go to stdout, errors and warnings will be logged to stderr
gitversion calculate
# Calculate the version number and write it gitversion.json
gitversion calculate > gitversion.json
# Calculate the version number and output to stdout. Include logging information depending on the verbosity level in the logging to stderr.
gitversion calculate --verbosity verbose
# Calculate the version number and output to stdout. Include diagnostics info in the logging to stderr (requires `git` executable on PATH).
gitversion [diag] calculate
# Calculate the version number and log to the file `/var/logs/gitversion.log`
gitversion calculate --logfile /var/logs/gitversion.log
# Calculate the version number based on the configuration file `/etc/gitversion.yml`
gitversion calculate --configfile /etc/gitversion.yml
# Calculate the version and override the `tag-prefix` configuration.
gitversion calculate --override-config tag-prefix=foo
# Calculate the version with caching disabled.
gitversion calculate --no-cache
# Read version variables from stdin and write to globbed AssemblyInfo.cs files
cat gitversion.json | gitversion output --type assemblyinfo --path ./**/AssemblyInfo.cs
# Read version variables from stdin and write to globbed .csproj files
cat gitversion.json | gitversion output --type projectfiles --path ./**/*.csproj
# Read version variables from stdin and write to an auto-detected build server. Without an `--in` argument, stdin is the default input.
cat gitversion.json | gitversion output --type buildserver
# Read version variables from stdin and write to Jenkins.
cat gitversion.json | gitversion output --type buildserver --buildserver Jenkins
# Read version variables from stdin and write to globbed .wxi files.
cat gitversion.json | gitversion output --type wix --path ./**/*.wxi
# Read version variables from stdin and output them to environment variables
cat gitversion.json | gitversion output --type environment
# Read version variables from stdin and output only the `FullSemVer` property to stdout.
cat gitversion.json | gitversion output --property FullSemVer
# Pipe the output of calculate to gitversion output
gitversion calculate | gitversion output --type assemblyinfo --path ./**/AssemblyInfo.cs
#NOTES [diag] can be used only with calculate command
Related Issue
#358, #428, #598, #572.
Motivation and Context
The CLI of GitVersion is a road that has been built as we have walked it, with no planning and no idea of scope or feature set before implementation was started. It has proved difficult to support POSIX file systems due to forward slash / being chosen as the argument separator originally, the argument system doesn't allow for easy documentation generation and perhaps most importantly: Argument parsing is not a core concern of GitVersion, so we're best served to outsource this entire thing to a third-party library.
For this one, what would overriding multiple config values look like?
gitversion calculate --override-config tag-prefix=foo
I think the cleanest is to repeat the argument:
gitversion calculate --override-config tag-prefix=foo --override-config next-version=1.2.3 --override-config mode=Mainline
We might go for colon : instead of equals = to align more with the YAML syntax.
gitversion calculate --override-config tag-prefix:foo --override-config next-version:1.2.3 --override-config mode:Mainline
Not sure.
@asbjornu I think System.CommandLine supports both : and = https://github.com/dotnet/command-line-api/wiki/Syntax-Concepts-and-Parser#delimiters
it's up to the user to decide which syntax they prefer. For our docs we'll need to choose one syntax to be consistent
Sorry if I wan't entirely clear on where the separator occurs. I meant the separator between the key and the value in the value for the argument --override-config.
Afaict, the separator being discussed is between the argument and its value, i.e. the space between --override-config and tag-prefix:foo. So we could write --override-config:tag-prefix=foo or --override-config=tag-prefix:foo.
But I think tag-prefix:foo and tag-prefix=foo is just seen as one opaque value to System.CommandLine and thus we would have to split the key and value by a chosen delimiter.
Actually I remember they had a way to parse that too, as the intent is to be able to have something similar to the dotnet build --property:name=value, the way they pass properties to msbuild
Neat. Perhaps we should just drop --override-config and go with --tag-prefix=foo anyway? We just need to be sure that none of the configuration properties collide with other arguments. I don't think we have anny collisions now, but if we do, we should take our chance to rename them for v6.
I just discovered docopt which may be worth considering for the argument parsing.
The current POC is done in the new-cli folder
btw, instead of docopt, i would suggest CommandLineParser library: https://github.com/commandlineparser/commandline
I help manage that library.
@ericnewton76 we decided to go with System.Commandline, and on the mentioned branch we're already working on that
We want stdin and stdout to become the default way to both read and write version variables, logging instead to stderr so stdout can be kept as valid JSON throughout.
BTW, I suggested in #3184 to add an option for flat output as opposed to JSON, via something like /output:flat instead of the current implied /output:json Reason is due to having to use cmd script parsing (yuck) and using for (delims) gets complicated with json ironically (ie, is the lead-in for each variable 2 spaces or 4 spaces or tabs???)
Also, git uses a --porcelain option that ensures the output is stable for parsing. I would suggest, In the beginning --porcelain could be the implied option, but I would immediately establish that as a required and encouraged option for stable output going forward. So for example, --porcelain --output=json would be the current implied standard and would output json in standard pretty print with 2 spaces ident. ...which btw still makes it difficult to parse via cmd for (delims)
UPDATE 2022-09-22: I think I read the porcelain stuff wrong in git docs, --porcelain seems to imply a user interface and might change so not good for scripting. I happen to be reading this doc: https://www.git-scm.com/docs/git/2.36.0#_low_level_commands_plumbing and it seems to imply porcelain is UX and might change.
BTW, I was just thinking, its been mentioned that certain operations might require multiple runs which makes sense for certain types of operations.
In doing so, I would setup the scenario where, by default, the output of the version resolution is stored (as the environment variable, GITVERSION_SEMVER?) and subsequent runs could skip the version resolution because this environment variable is present... from the previous run
Unless an option like --always-calculate-version is present. Reasoning is that typically GitVersion is run on a build server/build agent using an environment that is mostly persistent per-build. The tricky part is testing and the reverse might be needed (--no-persist) in that scenario (or $env.GITVERSION_NOPERSIST=true)
This would support additional scenarios where updating particular types of files can be separated. For instance, we had a VersionStamp.py script that could inject the already set env.MAJOR_MINOR_PATCH into various files that we displayed the version to the user, like MainTemplate.master, docs/MyProgram.html, etc. My thought was a more generic file updater (considering AssemblyVersionUpdater) could be implemented with a more generic use of tokens. So for example, inside MainTemplate.master we see <div>Version $(GITVERSION_SEMVER)</div> and similar files specified.
$ echo $GITVERSION_SEMVER
$ gitversion --output:none --set-environment
$ echo $GITVERSION_SEMVER
1.0.0
$ GITVERSION_SEMVER=
$ echo $GITVERSION_SEMVER
$ gitversion update assemblyinfo.cs ## (first run sets env.GITVERSION_SEMVER, etc)
Performing version resolution.
Detected standard AssemblyInfo.cs. Using AssemblyInfoUpdater.
Updated [AssemblyVersionInfo] in AssemblyInfo.cs
$ echo $GITVERSION_SEMVER
1.0.0
$ gitversion update --use=AssembyInfoUpdater assemblyinfo.cs
Version resolution skipped. GITVERSION_SEMVER already set.
Using AssemblyInfoUpdater.
Updated [AssemblyVersionInfo] in AssemblyInfo.cs
$ gitversion update MainTemplate.master docs/index.html
Version resolution skipped. GITVERSION_SEMVER already set.
Using TokenizedFileUpdater.
Updated $(GITVERSION_SEMVER) in MainTemplate.master
Updated $(GITVERSION_SEMVER) in docs/index.html
$ gitversion update --use=TokenizedFileUpdater --quiet **/*
$
If you read my examples, I've already suggested using gitversion.json or something similar to persist variables so they only have to be calculated once. Persisting them to one or more environment variables is probably also an option. GitVersion already does that on build servers.
Might be worth adding one more - the ability to pass the gitversion config as STDIN to gitversion calculate so that if present gitversion calculate doesn't have to look got the config file on disk - examples:
gitversion config show | gitversion calculate`
cat gitversion.config | gitversion calculate`
Might be worth adding one more - the ability to pass the gitversion config as STDIN to
gitversion calculateso that if present gitversion calculate doesn't have to look got the config file on disk - examples:gitversion config show | gitversion calculate` cat gitversion.config | gitversion calculate`
I think we have this one instead
gitversion calculate --configfile gitversion.yml
@arturcic fair.
I don't mind either way but just wanted to elaborate that, as gitversion config command manages the config, it may be nice to keep the logic around where the config file is initialised and how it's resolved in that command.. then If we passed the yaml config as STDIN to calculate command (and any others that may need it) those commands are freed from having to source it. The user could also then pipe in the config from other "non file" sources should they wish to - such as:
- a variable in their devops / build pipeline shared library
- a team wide azure blob storage blob read inline
Not an important scenario for me personally, not sure if this flexibility is particularly sought after! The above is still possible with the way the command is currently proposed ofcourse but it would require the config be saved by the user to a file on disk first and then the path of that config passed to the calculate command, not a huge deal but not as nice imho.
I wanted to throw in my thoughts here. If we use the Microsoft recommended Dockerfile for modern .NET, they load the entire source repo into the docker context. This is primarily to build the application, but the .git directory could be excluded if GitVersionTask read from a file, as @asbjornu suggests. In addition, the gittools/actions/gitversion/execute@v0 Github Action (in my scenario) could spit out a gitversion.json file to some predetermined directory that gets transparently pulled into the docker context when it does COPY . ., thus eliminating the need for .git/. It would also pull in my GitVersion.yml. I can't think of more that it would need.
This is mostly a performance concern for me. There's no other reason to have the .git/ directory loaded into the docker context.