├── .gitattributes ├── .gitignore ├── Build.ps1 ├── LICENSE ├── README.md ├── appveyor.yml ├── dotnet-version-cli.sln ├── src ├── CsProj │ ├── FileSystem │ │ ├── DotNetFileSystemProvider.cs │ │ └── IFileSystemProvider.cs │ ├── ProjectFileDetector.cs │ ├── ProjectFileParser.cs │ ├── ProjectFileProperty.cs │ └── ProjectFileVersionPatcher.cs ├── Model │ ├── ProductOutputInfo.cs │ └── VersionInfo.cs ├── OutputFormat.cs ├── ProductInfo.cs ├── Program.cs ├── Properties │ └── AssemblyInfo.cs ├── Vcs │ ├── Git │ │ └── GitVcs.cs │ ├── IVcs.cs │ └── VcsParser.cs ├── VersionBump.cs ├── VersionCli.cs ├── VersionCliArgs.cs ├── Versioning │ ├── SemVer.cs │ └── SemVerBumper.cs └── dotnet-version.csproj └── test ├── CsProj ├── FileSystem │ └── DotNetFileSystemProviderTests.cs ├── ProjectFileDetectorTest.cs ├── ProjectFileParserTest.cs └── ProjectFileVersionPatcherTest.cs ├── GitVcsTest.cs ├── ProgramTest.cs ├── VersionCliTest.cs ├── Versioning ├── SemVerBumperTests.cs └── SemVerTest.cs └── dotnet-version-test.csproj /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | *.sln merge=binary 26 | *.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | 65 | # Force bash scripts to always use lf line endings so that if a repo is accessed 66 | # in Unix via a file share from Windows, the scripts will work. 67 | *.sh text eol=lf 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .sonarqube/ 2 | *.swp 3 | *.*~ 4 | project.lock.json 5 | .DS_Store 6 | *.pyc 7 | 8 | # Visual Studio Code 9 | .vscode 10 | 11 | # User-specific files 12 | *.suo 13 | *.user 14 | *.userosscache 15 | *.sln.docstates 16 | 17 | # Build results 18 | [Dd]ebug/ 19 | [Dd]ebugPublic/ 20 | [Rr]elease/ 21 | [Rr]eleases/ 22 | x64/ 23 | x86/ 24 | build/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | msbuild.log 29 | msbuild.err 30 | msbuild.wrn 31 | 32 | # Visual Studio 2015 33 | .vs/ 34 | 35 | # Rider 36 | .idea/ 37 | .fake 38 | .ionide 39 | -------------------------------------------------------------------------------- /Build.ps1: -------------------------------------------------------------------------------- 1 | # Taken from psake https://github.com/psake/psake 2 | 3 | <# 4 | .SYNOPSIS 5 | This is a helper function that runs a scriptblock and checks the PS variable $lastexitcode 6 | to see if an error occcured. If an error is detected then an exception is thrown. 7 | This function allows you to run command-line programs without having to 8 | explicitly check the $lastexitcode variable. 9 | .EXAMPLE 10 | exec { svn info $repository_trunk } "Error executing SVN. Please verify SVN command-line client is installed" 11 | #> 12 | function Exec 13 | { 14 | [CmdletBinding()] 15 | param( 16 | [Parameter(Position=0,Mandatory=1)][scriptblock]$cmd, 17 | [Parameter(Position=1,Mandatory=0)][string]$errorMessage = ($msgs.error_bad_command -f $cmd) 18 | ) 19 | & $cmd 20 | if ($lastexitcode -ne 0) { 21 | throw ("Exec: " + $errorMessage) 22 | } 23 | } 24 | 25 | if(Test-Path .\artifacts) { Remove-Item .\artifacts -Force -Recurse } 26 | 27 | $Env:JAVA_HOME="C:\Program Files\Java\jdk17" 28 | $Env:PATH=-join($Env:JAVA_HOME, ";", $Env:PATH) 29 | 30 | $revision = @{ $true = $env:APPVEYOR_BUILD_NUMBER; $false = 1 }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL]; 31 | $revision = "{0:D4}" -f [convert]::ToInt32($revision, 10) 32 | 33 | # 34 | # install Sonar Scanner (from SonarQube) 35 | # 36 | exec { & dotnet tool install --global dotnet-sonarscanner } 37 | 38 | exec { & dotnet restore } 39 | 40 | $sonarProjectKey = "skarpdev_dotnet-version-cli" 41 | $sonarHostUrl = "https://sonarcloud.io" 42 | $openCoveragePaths = "$Env:APPVEYOR_BUILD_FOLDER/test/coverage.*.opencover.xml" 43 | $trxCoveragePahts = "$Env:APPVEYOR_BUILD_FOLDER/test/TestResults/*.trx" 44 | 45 | # initialize Sonar Scanner 46 | # If the environment variable APPVEYOR_PULL_REQUEST_NUMBER is not present, then this is not a pull request 47 | if(-not $env:APPVEYOR_PULL_REQUEST_NUMBER) { 48 | exec { 49 | & dotnet sonarscanner begin ` 50 | /k:$sonarProjectKey ` 51 | /o:skarp ` 52 | /v:$revision ` 53 | /d:sonar.host.url=$sonarHostUrl ` 54 | /d:sonar.cs.opencover.reportsPaths=$openCoveragePaths ` 55 | /d:sonar.cs.vstest.reportsPaths=$trxCoveragePahts ` 56 | /d:sonar.coverage.exclusions="**Test*.cs" ` 57 | /d:sonar.login="$Env:SONARCLOUD_TOKEN" 58 | } 59 | } 60 | elseif ($env:SONARCLOUD_TOKEN) { 61 | exec { 62 | & dotnet sonarscanner begin ` 63 | /k:$sonarProjectKey ` 64 | /o:skarp ` 65 | /v:$revision ` 66 | /d:sonar.host.url=$sonarHostUrl ` 67 | /d:sonar.cs.opencover.reportsPaths=$openCoveragePaths ` 68 | /d:sonar.cs.vstest.reportsPaths=$trxCoveragePahts ` 69 | /d:sonar.coverage.exclusions="**Test*.cs" ` 70 | /d:sonar.login="$env:SONARCLOUD_TOKEN" ` 71 | /d:sonar.pullrequest.branch=$env:APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH ` 72 | /d:sonar.pullrequest.base=$env:APPVEYOR_REPO_BRANCH ` 73 | /d:sonar.pullrequest.key=$env:APPVEYOR_PULL_REQUEST_NUMBER 74 | } 75 | } 76 | 77 | exec { & dotnet build -c Release } 78 | 79 | exec { & dotnet test .\test\dotnet-version-test.csproj -c Release /p:CollectCoverage=true /p:CoverletOutputFormat=opencover --logger trx } 80 | 81 | # trigger Sonar Scanner analysis 82 | if ($env:SONARCLOUD_TOKEN) { 83 | exec { & dotnet sonarscanner end /d:sonar.login="$env:SONARCLOUD_TOKEN" } 84 | } 85 | # pack up everything 86 | exec { & dotnet pack .\src\dotnet-version.csproj -c Release -o ..\artifacts --include-source } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 SKARP ApS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build status](https://ci.appveyor.com/api/projects/status/r50rbldhoil6pqk6/branch/master?svg=true)](https://ci.appveyor.com/project/nover/dotnet-version-cli/branch/master) 2 | [![nuget version][nuget-image]][nuget-url] 3 | [![Sonar Quality][sonarqualitylogo]][sonarqubelink] 4 | [![Code coverage][sonarcoveragelogo]][sonarqubelink] 5 | [![Sonar vulnerabilities][sonarvulnerabilitieslogo]][sonarqubelink] 6 | [![Sonar bugs][sonarbugslogo]][sonarqubelink] 7 | [![Sonar code smells][sonarcodesmellslogo]][sonarqubelink] 8 | 9 | # dotnet-version-cli 10 | 11 | This repository contains the source code for an [npm/yarn version][1] inspired dotnet global tool for dotnet with full [SemVer 2.0][semver2] compatibility! 12 | 13 | This used to be a dotnet csproj installable `cli tool` - if you are not ready for the move to dotnet global tools, please take a look at the last [0.7.0 release that supports csproj installation](https://github.com/skarpdev/dotnet-version-cli/blob/v0.7.0/README.md). 14 | 15 | Once installed it provides a `dotnet version` command which allows you to easily bump `patch`, `minor` and `major` versions on your project. You can also release and manage pre-release 16 | vesions of your packages by using the `prepatch`, `preminor` and `premajor` commands. Once in pre-release mode you can use the `prerelease` option to update the pre-release number. 17 | 18 | Alternatively it allows you to call it with the specific version it should set in the target `csproj`. 19 | 20 | We do not aim to be 100% feature compatible with `npm version` but provide the bare minimum for working with version numbers on your libraries and applications. 21 | 22 | Effectively this means that issuing a `patch` command will 23 | 24 | - bump the patch version of your project with 1 - so 1.0.0 becomes 1.0.1 25 | - Create a commit with the message `v1.0.1` 26 | - Create a tag with the name `v1.0.1` 27 | 28 | Similarly for `minor` and `major`, but changing different parts of the version number. 29 | 30 | When working with pre-releases using the `prepatch`, `preminor` and `premajor` options additional build meta can be passed using the `--build-meta` switch and the default `next` prefix can be changed using `--prefix`. 31 | 32 | To control the output format the `--output-format` switch can be used - currently supported values are `json` and `text`. **Please beware** that output is only reformatted for success-cases, so if something is wrong you will get a non 0 exit code and text output! 33 | Changing output format works for both "version bumping" and the "show version" operations of the cli. 34 | 35 | The commit and tag can be disabled via the `--skip-vcs` option. 36 | 37 | A completely dry run where nothing will be changed but the new version number is output can be enabled with the `--dry-run` switch. Performing a dry run also implies `skip vcs`. 38 | 39 | If the current directory does not contain the `csproj` file to work on the `-f|--project-file` switch can be provided. 40 | 41 | ## Installing the cli tool 42 | 43 | To install the tool simply issue 44 | 45 | ```bash 46 | dotnet tool install -g dotnet-version-cli 47 | ``` 48 | 49 | Now it should be available as 50 | 51 | ```bash 52 | dotnet version 53 | ``` 54 | 55 | It can also be executed directly as `dotnet-version` - both should produce output similar to 56 | 57 | ```text 58 | $ dotnet version 59 | dotnet-version-cli 60 | Project version is: 61 | 1.3.0 62 | ``` 63 | 64 | Using json output will produce 65 | 66 | ```bash 67 | $ dotnet version --output-format=json 68 | {"product":{"name":"dotnet-version-cli","version":"0.7.0.0"},"currentVersion":"1.3.0","projectFile":"C:\\your\\stuff\\project.csproj"} 69 | ``` 70 | 71 | The `product` bit is information about the cli tool itself. 72 | 73 | ## Standard workflow 74 | 75 | You have just merged a PR with a bugfix onto master and you are ready to release a new version of your library / application. The workflow is then 76 | 77 | ```bash 78 | $ git pull 79 | $ dotnet version -f ./src/my.csproj patch 80 | $ git push && git push --tags 81 | ``` 82 | 83 | ## Pre-release workflow 84 | 85 | As mentioned in the introduction the version tool allows working with pre-releases. 86 | Let's assume you have a library in version `1.2.4` and have made merges to master. You are not sure these changes work in the wild and therefore you require a 87 | pre-release. In the simpelest form you can 88 | 89 | ```bash 90 | $ dotnet version preminor 91 | ``` 92 | 93 | To get a preminor out. This new version tag would become `1.2.5-next.0`. 94 | If additional changes are merged you can roll over the pre-release version number by 95 | ```bash 96 | $ dotnet version prerelease 97 | ``` 98 | To make the release `1.2.5-next.1`. 99 | When ready you can snap out of pre-release mode and deploy the final minor version 100 | ```bash 101 | $ dotnet version minor 102 | ``` 103 | Resulting in the version `1.2.5`. 104 | 105 | All other command line flags like `-f` apply, and you can also include `build meta` as per SemVer 2.0 spec, like so: 106 | ```bash 107 | dotnet version --build-meta `git rev-parse --short HEAD` preminor # or prerelease etc. 108 | ``` 109 | To have a resulting version string like `1.2.5-next.1+abcedf` 110 | 111 | If the default `next` prefix is not desired it can easily be changed using the `--prefix` switch like so: 112 | ```bash 113 | dotnet version --prefix beta preminor # or prerelease etc. 114 | ``` 115 | 116 | Resulting in `1.2.4-beta.0`. 117 | 118 | ## Possible CI workflow 119 | 120 | If you do not care that commits and tags are made with the current version of your library, but simply wish to bump the version of your software when building on master, the tool can be used as (powershell example): 121 | 122 | ```powershell 123 | dotnet version "1.0.$env:BUILD_ID" 124 | ``` 125 | 126 | replacing `BUILD_ID` with whatever variable your build environment injects. 127 | The total count of commits in your git repo can also be used as a build number: 128 | 129 | ```powershell 130 | $revCount = & git rev-list HEAD --count | Out-String 131 | dotnet version "1.0.$revCount" 132 | ``` 133 | 134 | ## Change commit message 135 | 136 | If you want to change defaults commit message, you can use the flag `-m` or `--message`. 137 | ```bash 138 | $ dotnet version minor -m "Commit message" 139 | ``` 140 | 141 | There are variables availables to be set in the message 142 | 143 | `$projName` will be replaced for package title (or package id if its not defined) 144 | 145 | `$oldVer` will be replaced for old version of the package 146 | 147 | `$newVer` will be replaced for new version of the package 148 | ```bash 149 | $ dotnet version minor -m "$projName bumped from v$oldVer to v$newVer" 150 | # This will be replaced as 151 | # ProjectName bumped from v1.0.0 to v2.0.0 152 | ``` 153 | 154 | 155 | ## Change tag message 156 | 157 | If you want to change defaults tag message, you can use the flag `-t` or `--tag`. 158 | ```bash 159 | $ dotnet version minor -t "Tag" 160 | ``` 161 | 162 | There are variables availables to be set in the tag 163 | 164 | `$projName` will be replaced for package title (or package id if its not defined) 165 | 166 | `$oldVer` will be replaced for old version of the package 167 | 168 | `$newVer` will be replaced for new version of the package 169 | ```bash 170 | $ dotnet version minor -t "$projName bumped from v$oldVer to v$newVer" 171 | # This will be replaced as 172 | # ProjectName bumped from v1.0.0 to v2.0.0 173 | ``` 174 | 175 | ## Common Version 176 | If you want to share a version across multiple csproj files, you can create a `.targets` file and [import](import) it in the csproj files: 177 | `Common.targets`: 178 | ```xml 179 | 180 | 181 | 2.0.0 182 | 183 | 184 | ``` 185 | 186 | And in your `.csproj` files: 187 | ```xml 188 | 189 | 190 | 191 | ``` 192 | 193 | You can then use `dotnet version` to change the version in `Common.targets`: 194 | ```powershell 195 | dotnet version -f Common.targets 196 | ``` 197 | 198 | [1]: https://docs.npmjs.com/cli/version 199 | [nuget-image]: https://img.shields.io/nuget/v/dotnet-version-cli.svg 200 | [nuget-url]: https://www.nuget.org/packages/dotnet-version-cli 201 | [semver2]: https://semver.org/spec/v2.0.0.html 202 | [sonarqubelink]: https://sonarcloud.io/dashboard?id=skarpdev_dotnet-version-cli 203 | [sonarqualitylogo]: https://sonarcloud.io/api/project_badges/measure?project=skarpdev_dotnet-version-cli&metric=alert_status 204 | [sonarcoveragelogo]: https://sonarcloud.io/api/project_badges/measure?project=skarpdev_dotnet-version-cli&metric=coverage 205 | [sonarvulnerabilitieslogo]: https://sonarcloud.io/api/project_badges/measure?project=skarpdev_dotnet-version-cli&metric=vulnerabilities 206 | [sonarbugslogo]: https://sonarcloud.io/api/project_badges/measure?project=skarpdev_dotnet-version-cli&metric=bugs 207 | [sonarcodesmellslogo]: https://sonarcloud.io/api/project_badges/measure?project=skarpdev_dotnet-version-cli&metric=code_smells 208 | [import]: https://docs.microsoft.com/en-us/visualstudio/msbuild/import-element-msbuild?view=vs-2019 209 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | image: Visual Studio 2022 3 | environment: 4 | SONARCLOUD_TOKEN: 5 | secure: zP3yL8OgEY/gjfooti1esakQ0qHOwPl0GOoGugNS+61PxWOphHIndkOP/G3Glq96 6 | pull_requests: 7 | do_not_increment_build_number: true 8 | branches: 9 | only: 10 | - master 11 | nuget: 12 | disable_publish_on_pr: true 13 | build_script: 14 | - ps: .\Build.ps1 15 | test: off 16 | artifacts: 17 | - path: '**\*.nupkg' 18 | name: NuGet 19 | deploy: 20 | - provider: NuGet 21 | server: https://www.myget.org/F/skarp/api/v2/package 22 | api_key: 23 | secure: QUUDwCiAHecwSEHztB/ANurfoE3BMOpibwyPmr852U3a7VEamjUTfpzv86wVCjLD 24 | skip_symbols: true 25 | on: 26 | branch: master 27 | - provider: NuGet 28 | name: production 29 | api_key: 30 | secure: aYDDu8qdeToBDOLN8U4fL1aAuoO6oHeu33jtSv6Ph7oRzwHjANJUzjHw2T1adXhA 31 | on: 32 | appveyor_repo_tag: true -------------------------------------------------------------------------------- /dotnet-version-cli.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 17 3 | VisualStudioVersion = 17.6.33723.286 4 | MinimumVisualStudioVersion = 15.0.26124.0 5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-version", "src\dotnet-version.csproj", "{1AE7AFF7-E333-4205-AA1B-B8A8A79B4A87}" 6 | EndProject 7 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-version-test", "test\dotnet-version-test.csproj", "{FB420ACF-9E12-42B6-B724-1EEE9CBF251E}" 8 | EndProject 9 | Global 10 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 11 | Debug|Any CPU = Debug|Any CPU 12 | Debug|x64 = Debug|x64 13 | Debug|x86 = Debug|x86 14 | Release|Any CPU = Release|Any CPU 15 | Release|x64 = Release|x64 16 | Release|x86 = Release|x86 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {1AE7AFF7-E333-4205-AA1B-B8A8A79B4A87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {1AE7AFF7-E333-4205-AA1B-B8A8A79B4A87}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {1AE7AFF7-E333-4205-AA1B-B8A8A79B4A87}.Debug|x64.ActiveCfg = Debug|Any CPU 22 | {1AE7AFF7-E333-4205-AA1B-B8A8A79B4A87}.Debug|x64.Build.0 = Debug|Any CPU 23 | {1AE7AFF7-E333-4205-AA1B-B8A8A79B4A87}.Debug|x86.ActiveCfg = Debug|Any CPU 24 | {1AE7AFF7-E333-4205-AA1B-B8A8A79B4A87}.Debug|x86.Build.0 = Debug|Any CPU 25 | {1AE7AFF7-E333-4205-AA1B-B8A8A79B4A87}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {1AE7AFF7-E333-4205-AA1B-B8A8A79B4A87}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {1AE7AFF7-E333-4205-AA1B-B8A8A79B4A87}.Release|x64.ActiveCfg = Release|Any CPU 28 | {1AE7AFF7-E333-4205-AA1B-B8A8A79B4A87}.Release|x64.Build.0 = Release|Any CPU 29 | {1AE7AFF7-E333-4205-AA1B-B8A8A79B4A87}.Release|x86.ActiveCfg = Release|Any CPU 30 | {1AE7AFF7-E333-4205-AA1B-B8A8A79B4A87}.Release|x86.Build.0 = Release|Any CPU 31 | {FB420ACF-9E12-42B6-B724-1EEE9CBF251E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {FB420ACF-9E12-42B6-B724-1EEE9CBF251E}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {FB420ACF-9E12-42B6-B724-1EEE9CBF251E}.Debug|x64.ActiveCfg = Debug|Any CPU 34 | {FB420ACF-9E12-42B6-B724-1EEE9CBF251E}.Debug|x64.Build.0 = Debug|Any CPU 35 | {FB420ACF-9E12-42B6-B724-1EEE9CBF251E}.Debug|x86.ActiveCfg = Debug|Any CPU 36 | {FB420ACF-9E12-42B6-B724-1EEE9CBF251E}.Debug|x86.Build.0 = Debug|Any CPU 37 | {FB420ACF-9E12-42B6-B724-1EEE9CBF251E}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {FB420ACF-9E12-42B6-B724-1EEE9CBF251E}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {FB420ACF-9E12-42B6-B724-1EEE9CBF251E}.Release|x64.ActiveCfg = Release|Any CPU 40 | {FB420ACF-9E12-42B6-B724-1EEE9CBF251E}.Release|x64.Build.0 = Release|Any CPU 41 | {FB420ACF-9E12-42B6-B724-1EEE9CBF251E}.Release|x86.ActiveCfg = Release|Any CPU 42 | {FB420ACF-9E12-42B6-B724-1EEE9CBF251E}.Release|x86.Build.0 = Release|Any CPU 43 | EndGlobalSection 44 | GlobalSection(SolutionProperties) = preSolution 45 | HideSolutionNode = FALSE 46 | EndGlobalSection 47 | EndGlobal 48 | -------------------------------------------------------------------------------- /src/CsProj/FileSystem/DotNetFileSystemProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | 4 | namespace Skarp.Version.Cli.CsProj.FileSystem 5 | { 6 | public class DotNetFileSystemProvider : IFileSystemProvider 7 | { 8 | /// 9 | /// List the files of the given path 10 | /// 11 | /// 12 | /// 13 | public IEnumerable List(string path) 14 | { 15 | return Directory.EnumerateFiles(path); 16 | } 17 | 18 | /// 19 | /// Determines whether the given path is actually a csproj or targets file 20 | /// 21 | /// 22 | /// 23 | public bool IsCsProjectFile(string path) 24 | { 25 | return File.Exists(path) && (path.EndsWith(".csproj") || path.EndsWith(".targets")); 26 | } 27 | 28 | /// 29 | /// Gets the current working directory of the running application 30 | /// 31 | /// 32 | public string Cwd() 33 | { 34 | return Directory.GetCurrentDirectory(); 35 | } 36 | 37 | /// 38 | /// Load content from the given file 39 | /// 40 | /// 41 | /// 42 | public string LoadContent(string filePath) 43 | { 44 | return File.ReadAllText(filePath); 45 | } 46 | 47 | /// 48 | /// Write all text content to the given filepath 49 | /// 50 | /// 51 | /// 52 | public void WriteAllContent(string filePath, string data) 53 | { 54 | File.WriteAllText(filePath, data); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/CsProj/FileSystem/IFileSystemProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Skarp.Version.Cli.CsProj.FileSystem 4 | { 5 | public interface IFileSystemProvider 6 | { 7 | /// 8 | /// List the items in the given path 9 | /// 10 | /// 11 | /// 12 | IEnumerable List(string path); 13 | 14 | /// 15 | /// Determines whether the given path is actually a csproj file or a path 16 | /// 17 | /// 18 | /// 19 | bool IsCsProjectFile(string path); 20 | 21 | /// 22 | /// Get the current working directory 23 | /// 24 | /// 25 | string Cwd(); 26 | 27 | /// 28 | /// Loads all the content from the given file path as a string 29 | /// 30 | /// 31 | string LoadContent(string filePath); 32 | 33 | /// 34 | /// Writes all the content to the given file as a strings 35 | /// 36 | /// 37 | /// 38 | /// 39 | void WriteAllContent(string filePath, string data); 40 | } 41 | } -------------------------------------------------------------------------------- /src/CsProj/ProjectFileDetector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Skarp.Version.Cli.CsProj.FileSystem; 6 | 7 | namespace Skarp.Version.Cli.CsProj 8 | { 9 | public class ProjectFileDetector 10 | { 11 | private readonly IFileSystemProvider _fileSystem; 12 | public ProjectFileDetector( 13 | IFileSystemProvider fileSystem) 14 | { 15 | _fileSystem = fileSystem; 16 | } 17 | 18 | /// 19 | /// Method tries to find the nearest cs project file and loads up the 20 | /// xml and returns it 21 | /// 22 | /// 23 | /// If the given bootstrap path is empty, it will try to detect the nearest (current dir) 24 | /// cs project file. If bootstrapPath is given, and is a csproj file it will load this 25 | /// if bootstrap path is a folder, it will try to search for a csproj file there. 26 | /// 27 | /// 28 | /// 29 | public virtual string FindAndLoadCsProj(string bootstrapPath) 30 | { 31 | var path = bootstrapPath; 32 | string csProjFile; 33 | 34 | if (string.IsNullOrEmpty(bootstrapPath)) 35 | { 36 | path = _fileSystem.Cwd(); 37 | } 38 | if (_fileSystem.IsCsProjectFile(path)) 39 | { 40 | csProjFile = path; 41 | } 42 | else 43 | { 44 | var files = _fileSystem.List(path); 45 | var csProjFiles = files.Where(q => q.EndsWith(".csproj")); 46 | var projFiles = csProjFiles as IList ?? csProjFiles.ToList(); 47 | if (projFiles.Count == 0) 48 | { 49 | throw new OperationCanceledException("No csproj file could be found in path - ensure that you are running `dotnet version` next to the project file, or use -f to specify a target csproj file"); 50 | } 51 | 52 | if (projFiles.Count > 1) 53 | { 54 | var sb = new StringBuilder(); 55 | sb.AppendLine("Multiple csproj files found - aborting:"); 56 | foreach (var project in projFiles) 57 | { 58 | sb.AppendLine($"\t{project}"); 59 | } 60 | 61 | throw new OperationCanceledException(sb.ToString()); 62 | } 63 | 64 | csProjFile = projFiles.Single(); 65 | } 66 | ResolvedCsProjFile = csProjFile; 67 | 68 | var xml = _fileSystem.LoadContent(csProjFile); 69 | return xml; 70 | } 71 | 72 | public virtual string ResolvedCsProjFile { get; private set; } 73 | } 74 | } -------------------------------------------------------------------------------- /src/CsProj/ProjectFileParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Xml.Linq; 5 | 6 | namespace Skarp.Version.Cli.CsProj 7 | { 8 | public class ProjectFileParser 9 | { 10 | public virtual string PackageName { get; private set; } 11 | 12 | public virtual string PackageVersion { get; private set; } 13 | 14 | public virtual string Version { get; private set; } 15 | 16 | public virtual string VersionPrefix { get; private set; } 17 | 18 | public virtual string VersionSuffix { get; private set; } 19 | 20 | public virtual ProjectFileProperty DesiredVersionSource { get; private set; } 21 | 22 | public ProjectFileProperty VersionSource 23 | { 24 | get 25 | { 26 | if (DesiredVersionSource == ProjectFileProperty.Version) 27 | { 28 | return string.IsNullOrWhiteSpace(Version) 29 | ? ProjectFileProperty.VersionPrefix 30 | : ProjectFileProperty.Version; 31 | } 32 | 33 | return ProjectFileProperty.PackageVersion; 34 | } 35 | } 36 | 37 | private IEnumerable _propertyGroup { get; set; } 38 | 39 | protected virtual void Load(string xmlDocument, ProjectFileProperty property) 40 | { 41 | LoadPropertyGroup(xmlDocument); 42 | 43 | XElement propertyElement = LoadProperty(property); 44 | 45 | switch (property) 46 | { 47 | case ProjectFileProperty.Version: 48 | Version = propertyElement?.Value ?? string.Empty; 49 | break; 50 | case ProjectFileProperty.PackageVersion: 51 | PackageVersion = propertyElement?.Value ?? string.Empty; 52 | break; 53 | case ProjectFileProperty.Title: 54 | var defaultPropertyElement = LoadProperty(ProjectFileProperty.PackageId); 55 | PackageName = propertyElement?.Value ?? defaultPropertyElement?.Value ?? string.Empty; 56 | break; 57 | case ProjectFileProperty.VersionPrefix: 58 | VersionPrefix = propertyElement?.Value ?? string.Empty; 59 | break; 60 | case ProjectFileProperty.VersionSuffix: 61 | VersionSuffix = propertyElement?.Value ?? string.Empty; 62 | break; 63 | } 64 | } 65 | 66 | 67 | public virtual void Load(string xmlDocument, ProjectFileProperty versionSource, params ProjectFileProperty[] properties) 68 | { 69 | DesiredVersionSource = versionSource; 70 | 71 | if (properties == null || !properties.Any()) 72 | { 73 | properties = new[] 74 | { 75 | ProjectFileProperty.Title, 76 | ProjectFileProperty.Version, 77 | ProjectFileProperty.PackageId, 78 | ProjectFileProperty.PackageVersion, 79 | ProjectFileProperty.VersionPrefix, 80 | ProjectFileProperty.VersionSuffix 81 | }; 82 | } 83 | 84 | // Try to load xmlDocument even if there is no properties to be loaded 85 | // in order to verify if project file is well formed 86 | LoadPropertyGroup(xmlDocument); 87 | 88 | foreach (var property in properties) 89 | { 90 | Load(xmlDocument, property); 91 | } 92 | } 93 | 94 | public string GetHumanReadableVersionFromSource() 95 | { 96 | return VersionSource switch 97 | { 98 | ProjectFileProperty.Version => Version, 99 | ProjectFileProperty.VersionPrefix => $"{VersionPrefix}-{VersionSuffix}", 100 | ProjectFileProperty.PackageVersion => PackageVersion, 101 | _ => throw new ArgumentOutOfRangeException($"Unknown version source {VersionSource}") 102 | }; 103 | } 104 | 105 | private XElement LoadProperty(ProjectFileProperty property) 106 | { 107 | XElement propertyElement = ( 108 | from prop in _propertyGroup.Elements() 109 | where prop.Name == property.ToString("g") 110 | select prop 111 | ).FirstOrDefault(); 112 | return propertyElement; 113 | } 114 | 115 | private void LoadPropertyGroup(string xmlDocument) 116 | { 117 | // Check if it has been already loaded 118 | if (_propertyGroup != null) return; 119 | 120 | var xml = XDocument.Parse(xmlDocument, LoadOptions.PreserveWhitespace); 121 | 122 | // Project should be root of the document 123 | var project = xml.Elements("Project"); 124 | var xProject = project as IList ?? project.ToList(); 125 | if (!xProject.Any()) 126 | { 127 | throw new ArgumentException( 128 | "The provided csproj file seems malformed - no in the root", 129 | paramName: nameof(xmlDocument) 130 | ); 131 | } 132 | 133 | _propertyGroup = xProject.Elements("PropertyGroup"); 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /src/CsProj/ProjectFileProperty.cs: -------------------------------------------------------------------------------- 1 | namespace Skarp.Version.Cli.CsProj 2 | { 3 | public enum ProjectFileProperty 4 | { 5 | Version, 6 | PackageVersion, 7 | PackageId, 8 | Title, 9 | VersionPrefix, 10 | VersionSuffix 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/CsProj/ProjectFileVersionPatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Xml.Linq; 4 | using Skarp.Version.Cli.CsProj.FileSystem; 5 | 6 | namespace Skarp.Version.Cli.CsProj 7 | { 8 | public class ProjectFileVersionPatcher 9 | { 10 | private readonly IFileSystemProvider _fileSystem; 11 | private XDocument _doc; 12 | 13 | public ProjectFileVersionPatcher(IFileSystemProvider fileSystem) 14 | { 15 | _fileSystem = fileSystem; 16 | } 17 | 18 | public virtual void Load(string xmlDocument) 19 | { 20 | _doc = XDocument.Parse(xmlDocument, LoadOptions.PreserveWhitespace); 21 | } 22 | 23 | /// 24 | /// Replace the existing version number in the csproj xml with the new version 25 | /// 26 | /// The new version number to persist in the csproj file 27 | /// 28 | public virtual void PatchField(string newValue, ProjectFileProperty versionField) 29 | { 30 | PatchGenericField(versionField.ToString(), newValue); 31 | } 32 | 33 | /// 34 | /// Helper method for patching up a generic XML field in the loaded XML 35 | /// 36 | /// The name to find and update or add it to the tree 37 | /// New value 38 | /// 39 | private void PatchGenericField(string elementName, string newVal) 40 | { 41 | if (_doc == null) 42 | { 43 | throw new InvalidOperationException("Please call Load(string xml) before invoking patch operations"); 44 | } 45 | 46 | // If the element is not present, add it to the XML document (csproj file 47 | if (!ContainsElement(elementName)) 48 | { 49 | AddMissingElementToCsProj(elementName, newVal); 50 | } 51 | 52 | var elm = _doc.Descendants(elementName).First(); 53 | elm.Value = newVal; 54 | } 55 | 56 | private bool ContainsElement(string elementName) 57 | { 58 | var nodes = _doc.Descendants(elementName); 59 | return nodes.Any(); 60 | } 61 | 62 | private void AddMissingElementToCsProj(string elementName, string value) 63 | { 64 | // try to locate the PropertyGroup where the element belongs 65 | var node = _doc.Descendants("TargetFramework").FirstOrDefault(); 66 | if (node == null) 67 | { 68 | node = _doc.Descendants("TargetFrameworks").FirstOrDefault(); 69 | 70 | if (node == null) 71 | { 72 | throw new ArgumentException( 73 | $"Given XML does not contain {elementName} and cannot locate existing PropertyGroup to add it to - is this a valid csproj file?"); 74 | } 75 | } 76 | 77 | var propertyGroup = node.Parent; 78 | propertyGroup.Add(new XElement(elementName, value)); 79 | } 80 | 81 | /// 82 | /// Save the csproj changes to disk 83 | /// 84 | /// The csproj xml content 85 | /// The path of the csproj to write to 86 | public virtual void Flush(string filePath) 87 | { 88 | _fileSystem.WriteAllContent(filePath, ToXmlString()); 89 | } 90 | 91 | /// 92 | /// Get the underlying csproj XML back from the patcher as a string 93 | /// 94 | /// 95 | public virtual string ToXmlString() 96 | { 97 | return _doc.ToString(); 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /src/Model/ProductOutputInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Skarp.Version.Cli.Model 2 | { 3 | public class ProductOutputInfo 4 | { 5 | public string Name { get; set; } 6 | public string Version { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /src/Model/VersionInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Skarp.Version.Cli.Model 2 | { 3 | public class VersionInfo 4 | { 5 | public ProductOutputInfo Product { get; set; } 6 | public string OldVersion { get; set; } 7 | public string NewVersion { get; set; } 8 | public string ProjectFile { get; set; } 9 | public string VersionStrategy { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/OutputFormat.cs: -------------------------------------------------------------------------------- 1 | namespace Skarp.Version.Cli 2 | { 3 | public enum OutputFormat 4 | { 5 | /// 6 | /// Regular text output 7 | /// 8 | Text, 9 | 10 | /// 11 | /// json output 12 | /// 13 | Json, 14 | 15 | /// 16 | /// Bare output without extraneous information 17 | /// 18 | Bare 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ProductInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace Skarp.Version.Cli 4 | { 5 | public static class ProductInfo 6 | { 7 | /// 8 | /// The name of the product 9 | /// 10 | public const string Name = "dotnet-version-cli"; 11 | 12 | /// 13 | /// The version of the running product 14 | /// 15 | public static readonly string Version = Assembly.GetEntryAssembly().GetName().Version.ToString(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.Extensions.CommandLineUtils; 5 | using Skarp.Version.Cli.CsProj; 6 | using Skarp.Version.Cli.CsProj.FileSystem; 7 | using Skarp.Version.Cli.Vcs; 8 | using Skarp.Version.Cli.Vcs.Git; 9 | using Skarp.Version.Cli.Versioning; 10 | 11 | namespace Skarp.Version.Cli 12 | { 13 | static class Program 14 | { 15 | private static VersionCli _cli; 16 | 17 | static int Main(string[] args) 18 | { 19 | SetUpDependencies(); 20 | 21 | var commandLineApplication = new CommandLineApplication(throwOnUnexpectedArg: false) 22 | { 23 | Name = "dotnet version", 24 | ExtendedHelpText = 25 | $"{Environment.NewLine}Available commands after [options] to control the version bump are: {Environment.NewLine}\tmajor | minor | patch | premajor | preminor | prepatch | prerelease | " 26 | }; 27 | 28 | commandLineApplication.HelpOption("-? | -h | --help"); 29 | var outputFormatOption = commandLineApplication.Option( 30 | "-o | --output-format ", 31 | "Change output format, allowed values: json, text - default value is text", 32 | CommandOptionType.SingleValue); 33 | 34 | var skipVcsOption = commandLineApplication.Option( 35 | "-s | --skip-vcs", "Disable version control system changes - default is to tag and commit new version", 36 | CommandOptionType.NoValue); 37 | 38 | var doDryRun = commandLineApplication.Option( 39 | "-d | --dry-run", 40 | "Disable all changes to disk and vcs. Use to see what the changes would have been but without changing the csproj file nor committing or tagging.", 41 | CommandOptionType.NoValue); 42 | 43 | var csProjectFileOption = commandLineApplication.Option( 44 | "-f | --project-file ", 45 | "The project file to work on. Defaults to auto-locating in current directory", 46 | CommandOptionType.SingleValue); 47 | 48 | var buildMetaOption = commandLineApplication.Option( 49 | "-b | --build-meta ", 50 | "Additional build metadata to add to a `premajor`, `preminor` or `prepatch` version bump", 51 | CommandOptionType.SingleValue); 52 | 53 | var prefixOption = commandLineApplication.Option( 54 | "-p | --prefix ", 55 | "Override the default next prefix/label for a `premajor`, `preminor` or `prepatch` version bump", 56 | CommandOptionType.SingleValue); 57 | 58 | var commitMessage = commandLineApplication.Option( 59 | "-m | --message ", 60 | "Set commit's message - default is 'v'. Available variables: $projName, $oldVer, $newVer", 61 | CommandOptionType.SingleValue); 62 | 63 | var vcsTag = commandLineApplication.Option( 64 | "-t | --tag ", 65 | "Set tag's name - default is 'v'. Available variables: $projName, $oldVer, $newVer", 66 | CommandOptionType.SingleValue); 67 | 68 | var projectFilePropertyName = commandLineApplication.Option( 69 | "-v | --version-property-name ", 70 | "Specify which tag from to use as the version tag. Default is Version. Available values: Version, PackageVersion.", 71 | CommandOptionType.SingleValue); 72 | 73 | commandLineApplication.OnExecute(() => 74 | { 75 | try 76 | { 77 | var outputFormat = OutputFormat.Text; 78 | if (outputFormatOption.HasValue()) 79 | { 80 | outputFormat = 81 | (OutputFormat) Enum.Parse(typeof(OutputFormat), outputFormatOption.Value(), true); 82 | } 83 | 84 | if (outputFormat == OutputFormat.Text) 85 | { 86 | Console.WriteLine($"{ProductInfo.Name} version {ProductInfo.Version}"); 87 | } 88 | 89 | var doVcs = !skipVcsOption.HasValue(); 90 | var dryRunEnabled = doDryRun.HasValue(); 91 | 92 | if (commandLineApplication.RemainingArguments.Count == 0) 93 | { 94 | _cli.DumpVersion(new VersionCliArgs 95 | { 96 | OutputFormat = outputFormat, 97 | CsProjFilePath = csProjectFileOption.Value(), 98 | ProjectFilePropertyName = Enum.Parse(projectFilePropertyName.Value() ?? "Version", ignoreCase: true), 99 | }); 100 | 101 | return 0; 102 | } 103 | 104 | var cliArgs = GetVersionBumpFromRemainingArgs( 105 | commandLineApplication.RemainingArguments, 106 | outputFormat, 107 | doVcs, 108 | dryRunEnabled, 109 | csProjectFileOption.Value(), 110 | buildMetaOption.Value(), 111 | prefixOption.Value(), 112 | commitMessage.Value(), 113 | vcsTag.Value(), 114 | projectFilePropertyName.Value() 115 | ); 116 | _cli.Execute(cliArgs); 117 | 118 | return 0; 119 | } 120 | catch (ArgumentException ex) 121 | { 122 | Console.Error.WriteLine($"ERR {ex.Message}"); 123 | 124 | commandLineApplication.ShowHelp(); 125 | return 1; 126 | } 127 | 128 | catch (OperationCanceledException oce) 129 | { 130 | Console.Error.WriteLine($"ERR {oce.Message}"); 131 | 132 | commandLineApplication.ShowHelp(); 133 | return 1; 134 | } 135 | catch (Exception e) 136 | { 137 | Console.Error.WriteLine("ERR Something went completely haywire, developer zen:"); 138 | Console.Error.WriteLine($"\t{e.Message} STACK: {Environment.NewLine}{e.StackTrace}"); 139 | return 1; 140 | } 141 | }); 142 | return commandLineApplication.Execute(args); 143 | } 144 | 145 | internal static VersionCliArgs GetVersionBumpFromRemainingArgs( 146 | List remainingArguments, 147 | OutputFormat outputFormat, 148 | bool doVcs, 149 | bool dryRunEnabled, 150 | string userSpecifiedCsProjFilePath, 151 | string userSpecifiedBuildMeta, 152 | string preReleasePrefix, 153 | string commitMessage, 154 | string vcsTag, 155 | string projectFilePropertyName 156 | ) 157 | { 158 | if (remainingArguments == null || !remainingArguments.Any()) 159 | { 160 | var msgEx = 161 | "No version bump specified, please specify one of:\n\tmajor | minor | patch | premajor | preminor | prepatch | prerelease | "; 162 | // ReSharper disable once NotResolvedInText 163 | throw new ArgumentException(msgEx); 164 | } 165 | 166 | var args = new VersionCliArgs 167 | { 168 | OutputFormat = outputFormat, 169 | DoVcs = doVcs, 170 | DryRun = dryRunEnabled, 171 | BuildMeta = userSpecifiedBuildMeta, 172 | PreReleasePrefix = preReleasePrefix, 173 | CommitMessage = commitMessage, 174 | VersionControlTag = vcsTag 175 | }; 176 | 177 | var bump = VersionBump.Patch; 178 | 179 | foreach (var arg in remainingArguments) 180 | { 181 | if (Enum.TryParse(arg, true, out bump)) break; 182 | 183 | var ver = SemVer.FromString(arg); 184 | args.SpecificVersionToApply = ver.ToSemVerVersionString(null); 185 | bump = VersionBump.Specific; 186 | } 187 | 188 | args.VersionBump = bump; 189 | args.CsProjFilePath = userSpecifiedCsProjFilePath; 190 | 191 | if (!string.IsNullOrEmpty(projectFilePropertyName)) 192 | { 193 | args.ProjectFilePropertyName = Enum.Parse(projectFilePropertyName, ignoreCase: true); 194 | } 195 | 196 | return args; 197 | } 198 | 199 | private static void SetUpDependencies() 200 | { 201 | var dotNetFileSystemProvider = new DotNetFileSystemProvider(); 202 | _cli = new VersionCli( 203 | new GitVcs(), 204 | new ProjectFileDetector( 205 | dotNetFileSystemProvider 206 | ), 207 | new ProjectFileParser(), 208 | new VcsParser(), 209 | new ProjectFileVersionPatcher(dotNetFileSystemProvider), 210 | new SemVerBumper() 211 | ); 212 | } 213 | } 214 | } -------------------------------------------------------------------------------- /src/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("dotnet-version-test")] -------------------------------------------------------------------------------- /src/Vcs/Git/GitVcs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace Skarp.Version.Cli.Vcs.Git 5 | { 6 | public class GitVcs : IVcs 7 | { 8 | /// 9 | /// Creates a new commit with the given message 10 | /// 11 | /// Path to the cs project file that was version updated 12 | /// The message to include in the commit 13 | public void Commit(string csProjFilePath, string message) 14 | { 15 | if(!LaunchGitWithArgs($"add \"{csProjFilePath}\"")) 16 | { 17 | throw new OperationCanceledException($"Unable to add cs proj file {csProjFilePath} to git index"); 18 | } 19 | 20 | if(!LaunchGitWithArgs($"commit -m \"{message}\"")) 21 | { 22 | throw new OperationCanceledException("Unable to commit"); 23 | } 24 | } 25 | 26 | /// 27 | /// Determines whether the current repository is clean. 28 | /// 29 | /// 30 | public bool IsRepositoryClean() 31 | { 32 | return LaunchGitWithArgs("diff-index --quiet HEAD --"); 33 | } 34 | 35 | /// 36 | /// Determines whether git is present in PATH on the current computer 37 | /// 38 | /// 39 | public bool IsVcsToolPresent() 40 | { 41 | // launching `git --help` returns exit code 0 where as `git` returns 1 as git wants a cmd line argument 42 | return LaunchGitWithArgs("--help"); 43 | } 44 | 45 | /// 46 | /// Creates a new tag 47 | /// 48 | /// Name of the tag 49 | public void Tag(string tagName) 50 | { 51 | if(!LaunchGitWithArgs($"tag -a {tagName} -m {tagName}")) 52 | { 53 | throw new OperationCanceledException("Unable to create tag"); 54 | } 55 | } 56 | 57 | private static bool LaunchGitWithArgs(string args, int waitForExitTimeMs = 1000, int exitCode = 0) 58 | { 59 | try 60 | { 61 | var startInfo = CreateGitShellStartInfo(args); 62 | var proc = Process.Start(startInfo); 63 | proc.WaitForExit(waitForExitTimeMs); 64 | 65 | return proc.ExitCode == exitCode; 66 | } 67 | catch (Exception ex) 68 | { 69 | Console.Error.WriteLine(ex.Message); 70 | return false; 71 | } 72 | } 73 | 74 | private static ProcessStartInfo CreateGitShellStartInfo(string args) 75 | { 76 | return new ProcessStartInfo("git") 77 | { 78 | Arguments = args, 79 | RedirectStandardError = true, 80 | RedirectStandardInput = true, 81 | RedirectStandardOutput = true, 82 | }; 83 | } 84 | 85 | public string ToolName() 86 | { 87 | return "git"; 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /src/Vcs/IVcs.cs: -------------------------------------------------------------------------------- 1 | namespace Skarp.Version.Cli.Vcs 2 | { 3 | /// 4 | /// Version Control System abstraction interface 5 | /// 6 | public interface IVcs 7 | { 8 | /// 9 | /// When implemented by a concrete class it returns the name of the VCS tool 10 | /// 11 | /// 12 | string ToolName(); 13 | 14 | /// 15 | /// When implemented by a concrete class it determines whether the necessary tools 16 | /// are available in the current CLI contenxt - i.e check that `git` command can be found 17 | /// and executed 18 | /// 19 | /// true if the tool exists, false otherwise 20 | bool IsVcsToolPresent(); 21 | 22 | /// 23 | /// When implemented by a concrete class it returns true if the 24 | /// current HEAD of the local repository is clean - i.e no pending changes 25 | /// 26 | /// 27 | bool IsRepositoryClean(); 28 | 29 | /// 30 | /// When implemented by a concrete class it allows to create a commit with the 31 | /// changed version in the project file 32 | /// 33 | /// Path to the cs project file 34 | /// The message to create the commit message with 35 | void Commit(string csProjFilePath, string message); 36 | 37 | /// 38 | /// When implemented by a concrete class it will tag the latest commit with the 39 | /// given tag name 40 | /// 41 | /// The name of the tag to create - i.e v1.0.2 42 | void Tag(string tagName); 43 | } 44 | } -------------------------------------------------------------------------------- /src/Vcs/VcsParser.cs: -------------------------------------------------------------------------------- 1 | using Skarp.Version.Cli.CsProj; 2 | using Skarp.Version.Cli.Model; 3 | 4 | namespace Skarp.Version.Cli.Vcs 5 | { 6 | public class VcsParser 7 | { 8 | public string Commit(VersionInfo verInfo, ProjectFileParser fileParser, string argMessage) 9 | { 10 | if (string.IsNullOrEmpty(argMessage)) return $"v{verInfo.NewVersion}"; 11 | 12 | return ReplaceVariables(verInfo, fileParser, argMessage); 13 | } 14 | 15 | public string Tag(VersionInfo verInfo, ProjectFileParser fileParser, string argTag) 16 | { 17 | if (string.IsNullOrEmpty(argTag)) return $"v{verInfo.NewVersion}"; 18 | 19 | return ReplaceVariables(verInfo, fileParser, argTag); 20 | } 21 | 22 | private string ReplaceVariables(VersionInfo verInfo, ProjectFileParser fileParser, string dest) 23 | { 24 | return dest 25 | .Replace("$projName", fileParser.PackageName) 26 | .Replace("$oldVer", verInfo.OldVersion) 27 | .Replace("$newVer", verInfo.NewVersion); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/VersionBump.cs: -------------------------------------------------------------------------------- 1 | namespace Skarp.Version.Cli 2 | { 3 | /// 4 | /// Enumerates the possible version bumps 5 | /// 6 | public enum VersionBump 7 | { 8 | // Not supplied or parsing error or something - we can't bump `unknown` 9 | Unknown, 10 | 11 | Major, 12 | 13 | Minor, 14 | 15 | Patch, 16 | 17 | PreMajor, 18 | 19 | PreMinor, 20 | 21 | PrePatch, 22 | 23 | /// 24 | /// Increment the PreRelease indetifier (if it is numeric and rolled by this tool) 25 | /// 26 | PreRelease, 27 | 28 | /// 29 | /// Apply a specific, given, version to the project file 30 | /// 31 | Specific, 32 | 33 | /// 34 | /// Do not apply any changes. 35 | /// 36 | None 37 | } 38 | } -------------------------------------------------------------------------------- /src/VersionCli.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Serialization; 4 | using Skarp.Version.Cli.CsProj; 5 | using Skarp.Version.Cli.Model; 6 | using Skarp.Version.Cli.Vcs; 7 | using Skarp.Version.Cli.Versioning; 8 | 9 | namespace Skarp.Version.Cli 10 | { 11 | public class VersionCli 12 | { 13 | private readonly IVcs _vcsTool; 14 | private readonly ProjectFileDetector _fileDetector; 15 | private readonly ProjectFileParser _fileParser; 16 | private readonly VcsParser _vcsParser; 17 | private readonly ProjectFileVersionPatcher _fileVersionPatcher; 18 | private readonly SemVerBumper _bumper; 19 | 20 | public VersionCli( 21 | IVcs vcsClient, 22 | ProjectFileDetector fileDetector, 23 | ProjectFileParser fileParser, 24 | VcsParser vcsParser, 25 | ProjectFileVersionPatcher fileVersionPatcher, 26 | SemVerBumper bumper 27 | ) 28 | { 29 | _vcsTool = vcsClient; 30 | _fileDetector = fileDetector; 31 | _fileParser = fileParser; 32 | _vcsParser = vcsParser; 33 | _fileVersionPatcher = fileVersionPatcher; 34 | _bumper = bumper; 35 | } 36 | 37 | public VersionInfo Execute(VersionCliArgs args) 38 | { 39 | if (!args.DryRun && args.DoVcs && !_vcsTool.IsVcsToolPresent()) 40 | { 41 | throw new OperationCanceledException( 42 | $"Unable to find the vcs tool {_vcsTool.ToolName()} in your path"); 43 | } 44 | 45 | if (!args.DryRun && args.DoVcs && !_vcsTool.IsRepositoryClean()) 46 | { 47 | throw new OperationCanceledException( 48 | "You currently have uncomitted changes in your repository, please commit these and try again"); 49 | } 50 | 51 | var csProjXml = _fileDetector.FindAndLoadCsProj(args.CsProjFilePath); 52 | _fileParser.Load( 53 | csProjXml, 54 | args.ProjectFilePropertyName, 55 | ProjectFileProperty.Version, ProjectFileProperty.PackageVersion, ProjectFileProperty.VersionSuffix, 56 | ProjectFileProperty.VersionPrefix 57 | ); 58 | 59 | var currentSemVer = GetCurrentSemVerFromSource(); 60 | 61 | var bumpedSemVer = _bumper.Bump( 62 | currentSemVer, 63 | args.VersionBump, 64 | args.SpecificVersionToApply, 65 | args.BuildMeta, 66 | args.PreReleasePrefix 67 | ); 68 | 69 | var theOutput = new VersionInfo 70 | { 71 | Product = new ProductOutputInfo 72 | { 73 | Name = ProductInfo.Name, 74 | Version = ProductInfo.Version 75 | }, 76 | OldVersion = currentSemVer.ToSemVerVersionString(_fileParser), 77 | NewVersion = bumpedSemVer.ToSemVerVersionString(_fileParser), 78 | ProjectFile = _fileDetector.ResolvedCsProjFile, 79 | VersionStrategy = args.VersionBump.ToString().ToLowerInvariant() 80 | }; 81 | 82 | if (!args.DryRun) // if we are not in dry run mode, then we should go ahead 83 | { 84 | _fileVersionPatcher.Load(csProjXml); 85 | 86 | _fileVersionPatcher.PatchField( 87 | bumpedSemVer.ToSemVerVersionString(_fileParser), 88 | _fileParser.VersionSource 89 | ); 90 | 91 | _fileVersionPatcher.Flush( 92 | _fileDetector.ResolvedCsProjFile 93 | ); 94 | 95 | if (args.DoVcs) 96 | { 97 | _fileParser.Load(csProjXml, ProjectFileProperty.Title); 98 | // Run git commands 99 | _vcsTool.Commit(_fileDetector.ResolvedCsProjFile, 100 | _vcsParser.Commit(theOutput, _fileParser, args.CommitMessage)); 101 | _vcsTool.Tag(_vcsParser.Tag(theOutput, _fileParser, args.VersionControlTag)); 102 | } 103 | } 104 | 105 | if (args.OutputFormat == OutputFormat.Json) 106 | { 107 | WriteJsonToStdout(theOutput); 108 | } 109 | else if (args.OutputFormat == OutputFormat.Bare) 110 | { 111 | Console.WriteLine(bumpedSemVer.ToSemVerVersionString(_fileParser)); 112 | } 113 | else 114 | { 115 | Console.WriteLine( 116 | $"Bumped {_fileDetector.ResolvedCsProjFile} to version {bumpedSemVer.ToSemVerVersionString(_fileParser)}"); 117 | } 118 | 119 | return theOutput; 120 | } 121 | 122 | private SemVer GetCurrentSemVerFromSource() 123 | { 124 | return _fileParser.VersionSource switch 125 | { 126 | ProjectFileProperty.Version => SemVer.FromString(string.IsNullOrWhiteSpace(_fileParser.Version) ? "0.0.0" : _fileParser.Version), 127 | ProjectFileProperty.PackageVersion => SemVer.FromString(string.IsNullOrWhiteSpace(_fileParser.PackageVersion) ? "0.0.0" : _fileParser.PackageVersion), 128 | _ => SemVer.FromString(string.IsNullOrWhiteSpace(_fileParser.VersionPrefix) ? "0.0.0" : _fileParser.VersionPrefix) 129 | }; 130 | } 131 | 132 | public void DumpVersion(VersionCliArgs args) 133 | { 134 | var csProjXml = _fileDetector.FindAndLoadCsProj(args.CsProjFilePath); 135 | _fileParser.Load(csProjXml, args.ProjectFilePropertyName); 136 | 137 | switch (args.OutputFormat) 138 | { 139 | case OutputFormat.Json: 140 | var theOutput = new 141 | { 142 | Product = new 143 | { 144 | Name = ProductInfo.Name, 145 | Version = ProductInfo.Version 146 | }, 147 | CurrentVersion = _fileParser.GetHumanReadableVersionFromSource(), 148 | ProjectFile = _fileDetector.ResolvedCsProjFile, 149 | }; 150 | WriteJsonToStdout(theOutput); 151 | break; 152 | case OutputFormat.Bare: 153 | Console.WriteLine(_fileParser.GetHumanReadableVersionFromSource()); 154 | break; 155 | case OutputFormat.Text: 156 | default: 157 | Console.WriteLine("Project version is: {0}\t{1}", Environment.NewLine, 158 | _fileParser.GetHumanReadableVersionFromSource()); 159 | break; 160 | } 161 | } 162 | 163 | private static void WriteJsonToStdout(object theOutput) 164 | { 165 | Console.WriteLine( 166 | JsonConvert.SerializeObject( 167 | theOutput, new JsonSerializerSettings 168 | { 169 | ContractResolver = new CamelCasePropertyNamesContractResolver() 170 | })); 171 | } 172 | } 173 | } -------------------------------------------------------------------------------- /src/VersionCliArgs.cs: -------------------------------------------------------------------------------- 1 | using Skarp.Version.Cli.CsProj; 2 | 3 | namespace Skarp.Version.Cli 4 | { 5 | public class VersionCliArgs 6 | { 7 | public VersionBump VersionBump { get; set; } 8 | 9 | public string SpecificVersionToApply { get; set; } 10 | 11 | public string CsProjFilePath { get; set; } 12 | 13 | public OutputFormat OutputFormat { get; set; } 14 | 15 | /// 16 | /// Whether or not to do version control changes like 17 | /// commit and tag. 18 | /// 19 | public bool DoVcs { get; set; } 20 | 21 | /// 22 | /// Whether dry run is enabled and thus all mutations should be disabled 23 | /// 24 | public bool DryRun { get; set; } 25 | 26 | /// 27 | /// Build meta for a pre-release tag passed via CLI arguments 28 | /// 29 | public string BuildMeta { get; set; } 30 | 31 | /// 32 | /// Override for the default `next` pre-release prefix/label 33 | /// 34 | public string PreReleasePrefix { get; set; } 35 | 36 | /// 37 | /// Set commit's message 38 | /// 39 | public string CommitMessage { get; set; } 40 | 41 | /// 42 | /// Override for the default `v` vcs tag 43 | /// 44 | public string VersionControlTag { get; set; } 45 | 46 | /// 47 | /// Specify the Version-Tag that should be targeted. Default is Version. 48 | /// 49 | public ProjectFileProperty ProjectFilePropertyName { get; set; } = ProjectFileProperty.Version; 50 | } 51 | } -------------------------------------------------------------------------------- /src/Versioning/SemVer.cs: -------------------------------------------------------------------------------- 1 | using Skarp.Version.Cli.CsProj; 2 | using System; 3 | using System.Text; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace Skarp.Version.Cli.Versioning 7 | { 8 | public class SemVer 9 | { 10 | // this lovely little regex comes from the SemVer spec: 11 | // https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string 12 | private static readonly Regex VersionPartRegex = new Regex( 13 | @"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$", 14 | RegexOptions.Compiled, 15 | TimeSpan.FromSeconds(2) 16 | ); 17 | 18 | public bool IsPreRelease => !string.IsNullOrWhiteSpace(PreRelease); 19 | 20 | /// 21 | /// Serialize the parsed version information into a SemVer version string including pre and build meta 22 | /// 23 | /// 24 | public string ToSemVerVersionString(ProjectFileParser projectFileParser) 25 | { 26 | var sb = new StringBuilder(); 27 | sb.Append($"{Major}.{Minor}.{Patch}"); 28 | 29 | if (!string.IsNullOrWhiteSpace(PreRelease)) 30 | { 31 | sb.AppendFormat("-{0}", PreRelease); 32 | if (!string.IsNullOrWhiteSpace(BuildMeta)) 33 | { 34 | sb.AppendFormat("+{0}", BuildMeta); 35 | } 36 | } 37 | 38 | if (projectFileParser != null 39 | && projectFileParser.VersionSource == ProjectFileProperty.VersionPrefix 40 | && !string.IsNullOrWhiteSpace(projectFileParser.VersionSuffix)) 41 | { 42 | sb.AppendFormat("-{0}", projectFileParser.VersionSuffix); 43 | } 44 | 45 | return sb.ToString(); 46 | } 47 | 48 | /// 49 | /// Create a new instance of a SemVer based off the version string 50 | /// 51 | /// The version string to parse into a SemVer instance 52 | /// 53 | public static SemVer FromString(string versionString) 54 | { 55 | var matches = VersionPartRegex.Match(versionString); 56 | if (!matches.Success) 57 | { 58 | throw new ArgumentException($"Invalid SemVer version string: {versionString}", nameof(versionString)); 59 | } 60 | 61 | // Groups [0] is the full string , then we have the version parts after that 62 | return new SemVer 63 | { 64 | Major = Convert.ToInt32(matches.Groups[1].Value), 65 | Minor = Convert.ToInt32(matches.Groups[2].Value), 66 | Patch = Convert.ToInt32(matches.Groups[3].Value), 67 | PreRelease = matches.Groups[4].Value, 68 | BuildMeta = matches.Groups[5].Value 69 | }; 70 | } 71 | 72 | public object Clone() 73 | { 74 | return this.MemberwiseClone(); 75 | } 76 | 77 | /// 78 | /// The parsed major version 79 | /// 80 | /// 81 | public int Major { get; set; } 82 | 83 | /// 84 | /// The parsed minor version 85 | /// 86 | /// 87 | public int Minor { get; set; } 88 | 89 | /// 90 | /// The parsed patch version 91 | /// 92 | /// 93 | public int Patch { get; set; } 94 | 95 | /// 96 | /// Pre-release semver 2 information (the stuff added with a dash after version) 97 | /// 98 | public string PreRelease { get; set; } 99 | 100 | /// 101 | /// Build mtadata semver 2 information (the stuff added with a + sign after PreRelease info) 102 | /// 103 | public string BuildMeta { get; set; } 104 | 105 | } 106 | } -------------------------------------------------------------------------------- /src/Versioning/SemVerBumper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Skarp.Version.Cli.Versioning 4 | { 5 | public class SemVerBumper 6 | { 7 | /// 8 | /// Bump the currently parsed version information with the specified 9 | /// 10 | /// 11 | /// The bump to apply to the version 12 | /// The specific version to apply if bump is Specific 13 | /// Additional build metadata to add to the final version string 14 | /// Override of default `next` pre-release prefix/label 15 | public SemVer Bump( 16 | SemVer currentVersion, 17 | VersionBump bump, 18 | string specificVersionToApply = "", 19 | string buildMeta = "", 20 | string preReleasePrefix = "" 21 | ) 22 | { 23 | var newVersion = (SemVer)currentVersion.Clone(); 24 | newVersion.BuildMeta = buildMeta; 25 | 26 | switch (bump) 27 | { 28 | case VersionBump.Major: 29 | { 30 | HandleMajorBump(newVersion); 31 | break; 32 | } 33 | case VersionBump.PreMajor: 34 | { 35 | HandlePreMajorBump(newVersion, preReleasePrefix); 36 | break; 37 | } 38 | case VersionBump.Minor: 39 | { 40 | HandleMinorBump(newVersion); 41 | break; 42 | } 43 | case VersionBump.PreMinor: 44 | { 45 | HandlePreMinorBump(newVersion, preReleasePrefix); 46 | break; 47 | } 48 | case VersionBump.Patch: 49 | { 50 | HandlePatchBump(newVersion); 51 | break; 52 | } 53 | case VersionBump.PrePatch: 54 | { 55 | HandlePrePatchBump(newVersion, preReleasePrefix); 56 | break; 57 | } 58 | case VersionBump.PreRelease: 59 | { 60 | HandlePreReleaseBump(newVersion); 61 | break; 62 | } 63 | case VersionBump.Specific: 64 | { 65 | HandleSpecificVersion(specificVersionToApply, newVersion); 66 | break; 67 | } 68 | case VersionBump.None: 69 | //Do nothing; 70 | break; 71 | default: 72 | { 73 | throw new ArgumentOutOfRangeException(nameof(bump), $"VersionBump : {bump} not supported"); 74 | } 75 | } 76 | 77 | return newVersion; 78 | } 79 | 80 | private static void HandleSpecificVersion(string specificVersionToApply, SemVer newVersion) 81 | { 82 | if (string.IsNullOrEmpty(specificVersionToApply)) 83 | { 84 | throw new ArgumentException($"When bump is specific, specificVersionToApply must be provided"); 85 | } 86 | 87 | var specific = SemVer.FromString(specificVersionToApply); 88 | newVersion.Major = specific.Major; 89 | newVersion.Minor = specific.Minor; 90 | newVersion.Patch = specific.Patch; 91 | newVersion.PreRelease = specific.PreRelease; 92 | newVersion.BuildMeta = specific.BuildMeta; 93 | } 94 | 95 | private static void HandlePreReleaseBump(SemVer newVersion) 96 | { 97 | if (!newVersion.IsPreRelease) 98 | { 99 | throw new InvalidOperationException( 100 | "Cannot Prerelease bump when not already a prerelease. Please use prepatch, preminor or premajor to prepare"); 101 | } 102 | 103 | string preReleaseLabel = "next"; 104 | 105 | if (!int.TryParse(newVersion.PreRelease, out var preReleaseNumber)) 106 | { 107 | // it was not just a number, let's try to split it (pre-release might look like `next.42`) 108 | var preReleaseSplit = newVersion.PreRelease.Split("."); 109 | if (preReleaseSplit.Length != 2) 110 | { 111 | throw new ArgumentException( 112 | $"Pre-release part invalid. Must be either numeric or `label.number`. Got {newVersion.PreRelease}"); 113 | } 114 | 115 | if (!int.TryParse(preReleaseSplit[1], out preReleaseNumber)) 116 | { 117 | throw new ArgumentException( 118 | "Second part of pre-release is not numeric, cannot apply automatic prerelease roll. Should follow pattern `label.number`"); 119 | } 120 | 121 | preReleaseLabel = preReleaseSplit[0]; 122 | } 123 | 124 | // increment the pre-release number 125 | preReleaseNumber += 1; 126 | newVersion.PreRelease = $"{preReleaseLabel}.{preReleaseNumber}"; 127 | } 128 | 129 | private static void HandlePrePatchBump(SemVer newVersion, string preReleasePrefix) 130 | { 131 | if (string.IsNullOrWhiteSpace(preReleasePrefix)) 132 | { 133 | preReleasePrefix = "next"; 134 | } 135 | newVersion.Patch += 1; 136 | newVersion.PreRelease = $"{preReleasePrefix}.0"; 137 | } 138 | 139 | private void HandlePatchBump(SemVer newVersion) 140 | { 141 | if (!newVersion.IsPreRelease) 142 | { 143 | newVersion.Patch += 1; 144 | } 145 | else 146 | { 147 | newVersion.PreRelease = string.Empty; 148 | newVersion.BuildMeta = string.Empty; 149 | } 150 | } 151 | 152 | private void HandlePreMinorBump(SemVer newVersion, string preReleasePrefix) 153 | { 154 | if (string.IsNullOrWhiteSpace(preReleasePrefix)) 155 | { 156 | preReleasePrefix = "next"; 157 | } 158 | 159 | newVersion.Minor += 1; 160 | newVersion.Patch = 0; 161 | newVersion.PreRelease = $"{preReleasePrefix}.0"; 162 | } 163 | 164 | private void HandleMinorBump(SemVer newVersion) 165 | { 166 | if (newVersion.IsPreRelease) 167 | { 168 | newVersion.PreRelease = string.Empty; 169 | newVersion.BuildMeta = string.Empty; 170 | } 171 | else 172 | { 173 | newVersion.Minor += 1; 174 | newVersion.Patch = 0; 175 | } 176 | } 177 | 178 | private void HandlePreMajorBump(SemVer newVersion, string preReleasePrefix) 179 | { 180 | if (string.IsNullOrWhiteSpace(preReleasePrefix)) 181 | { 182 | preReleasePrefix = "next"; 183 | } 184 | 185 | newVersion.Major += 1; 186 | newVersion.Minor = 0; 187 | newVersion.Patch = 0; 188 | newVersion.PreRelease = $"{preReleasePrefix}.0"; 189 | } 190 | 191 | private void HandleMajorBump(SemVer newVersion) 192 | { 193 | if (newVersion.IsPreRelease) 194 | { 195 | newVersion.PreRelease = string.Empty; 196 | newVersion.BuildMeta = string.Empty; 197 | } 198 | else 199 | { 200 | newVersion.Major += 1; 201 | newVersion.Minor = 0; 202 | newVersion.Patch = 0; 203 | } 204 | } 205 | } 206 | } -------------------------------------------------------------------------------- /src/dotnet-version.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net6.0;net7.0;net8.0 5 | true 6 | dotnet-version 7 | true 8 | Skarp.Version.Cli 9 | 4.0.0 10 | dotnet-version-cli 11 | nover 12 | A dotnet core global tool for changing your csproj version and automatically comitting and tagging - npm version style. 13 | core;version;npm version;version patch; 14 | https://github.com/skarpdev/dotnet-version-cli/ 15 | https://raw.githubusercontent.com/skarpdev/dotnet-version-cli/master/LICENSE 16 | false 17 | git 18 | https://github.com/skarpdev/dotnet-version-cli/ 19 | dotnet-version-cli 20 | SKARP ApS 21 | 1ae7aff7-e333-4205-aa1b-b8a8a79b4a87 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/CsProj/FileSystem/DotNetFileSystemProviderTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using Skarp.Version.Cli.CsProj.FileSystem; 5 | using Xunit; 6 | 7 | namespace Skarp.Version.Cli.Test.CsProj.FileSystem 8 | { 9 | public class DotNetFileSystemProviderTests 10 | { 11 | private readonly DotNetFileSystemProvider _provider; 12 | 13 | public DotNetFileSystemProviderTests() 14 | { 15 | _provider = new DotNetFileSystemProvider(); 16 | } 17 | 18 | [Fact] 19 | public void List_works() 20 | { 21 | // List files in the current directory running from (the build output folder) 22 | var files = _provider.List("./").ToList(); 23 | 24 | Assert.NotEmpty(files); 25 | Assert.Contains("./dotnet-version.dll", files); 26 | } 27 | 28 | [Theory] 29 | [InlineData("./dotnet-version.dll", false)] 30 | [InlineData("../../../dotnet-version-test.csproj", true)] 31 | public void IsCsProjectFile_works(string path, bool isCsProj) 32 | { 33 | Assert.Equal(_provider.IsCsProjectFile(path), isCsProj); 34 | } 35 | 36 | [Fact(Skip = "Not working properly in CI")] 37 | public void Cwd_works() 38 | { 39 | var cwd = _provider.Cwd(); 40 | Assert.Contains($"Release{Path.DirectorySeparatorChar}net", cwd); 41 | } 42 | 43 | [Fact] 44 | public void LoadAllContent_works() 45 | { 46 | var content = _provider.LoadContent("../../../dotnet-version-test.csproj"); 47 | Assert.Contains("fb420acf-9e12-42b6-b724-1eee9cbf251e", content); 48 | } 49 | 50 | [Fact] 51 | public void WriteAllContent_works() 52 | { 53 | var path = "./test-file.txt"; 54 | var content = "this is content"; 55 | _provider.WriteAllContent(path, content); 56 | 57 | var loadedContent = File.ReadAllText(path); 58 | 59 | Assert.Equal(content, loadedContent); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /test/CsProj/ProjectFileDetectorTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FakeItEasy; 4 | using Skarp.Version.Cli.CsProj; 5 | using Skarp.Version.Cli.CsProj.FileSystem; 6 | using Xunit; 7 | 8 | namespace Skarp.Version.Cli.Test.CsProj 9 | { 10 | public class ProjectFileDetectorTest 11 | { 12 | private static string _projectXml = 13 | "" + 14 | "" + 15 | "netstandard1.6" + 16 | "Unit.For.The.Win" + 17 | "Unit.Testing.Library" + 18 | "1.0.0" + 19 | "" + 20 | ""; 21 | 22 | [Fact] 23 | public void CanDetectCsProjFileWithGivenBootstrapFolder() 24 | { 25 | const string rootPath = "/unit-test"; 26 | var theCsProjFile = $"{rootPath}/test.csproj"; 27 | 28 | var fakeFileSystem = A.Fake(opts => opts.Strict()); 29 | 30 | A.CallTo(() => fakeFileSystem.List(A._)).Returns( 31 | new List{ 32 | theCsProjFile, 33 | $"{rootPath}/Test.cs", 34 | } 35 | ); 36 | A.CallTo(() => 37 | fakeFileSystem.IsCsProjectFile( 38 | A.That.Matches(str => str == $"{rootPath}"))) 39 | .Returns(false); 40 | A.CallTo(() => 41 | fakeFileSystem.LoadContent(A.That.Matches(str => str == theCsProjFile)) 42 | ).Returns(_projectXml); 43 | 44 | var detect = new ProjectFileDetector(fakeFileSystem); 45 | var xml = detect.FindAndLoadCsProj(rootPath); 46 | 47 | Assert.Equal(_projectXml, xml); 48 | Assert.Equal(theCsProjFile, detect.ResolvedCsProjFile); 49 | } 50 | 51 | [Fact] 52 | public void AbortsWhenMoreThanOneCsprojFile() 53 | { 54 | const string rootPath = "/unit-test"; 55 | 56 | var fakeFileSystem = A.Fake(opts => opts.Strict()); 57 | A.CallTo(() => fakeFileSystem.List(A._)).Returns( 58 | new List{ 59 | $"{rootPath}/test.csproj", 60 | $"{rootPath}/other.csproj", 61 | $"{rootPath}/Test.cs", 62 | } 63 | ); 64 | A.CallTo(() => 65 | fakeFileSystem.IsCsProjectFile( 66 | A.That.Matches(str => str == $"{rootPath}"))) 67 | .Returns(false); 68 | 69 | 70 | var detect = new ProjectFileDetector(fakeFileSystem); 71 | Assert.Throws(() => detect.FindAndLoadCsProj(rootPath)); 72 | } 73 | 74 | [Fact] 75 | public void Aborts_when_no_csproj_file() 76 | { 77 | const string rootPath = "/unit-test"; 78 | 79 | var fakeFileSystem = A.Fake(opts => opts.Strict()); 80 | A.CallTo(() => fakeFileSystem.List(A._)).Returns( 81 | new List{ 82 | $"{rootPath}/Test.cs", 83 | } 84 | ); 85 | A.CallTo(() => 86 | fakeFileSystem.IsCsProjectFile( 87 | A.That.Matches(str => str == $"{rootPath}"))) 88 | .Returns(false); 89 | 90 | 91 | var detect = new ProjectFileDetector(fakeFileSystem); 92 | Assert.Throws(() => detect.FindAndLoadCsProj(rootPath)); 93 | } 94 | 95 | [Fact] 96 | public void CanDetectCsProjFileWithGivenBootstrapCsProj() 97 | { 98 | const string rootPath = "/unit-test"; 99 | var theCsProjFile = $"{rootPath}/test.csproj"; 100 | 101 | var fakeFileSystem = A.Fake(opts => opts.Strict()); 102 | A.CallTo(() => fakeFileSystem.List(A._)).Returns( 103 | new List{ 104 | theCsProjFile, 105 | $"{rootPath}/Test.cs", 106 | } 107 | ); 108 | A.CallTo(() => 109 | fakeFileSystem.IsCsProjectFile( 110 | A.That.Matches(str => str == theCsProjFile))) 111 | .Returns(true); 112 | A.CallTo(() => 113 | fakeFileSystem.LoadContent(A.That.Matches(str => str == theCsProjFile)) 114 | ).Returns(_projectXml); 115 | 116 | var detect = new ProjectFileDetector(fakeFileSystem); 117 | var xml = detect.FindAndLoadCsProj(theCsProjFile); 118 | 119 | Assert.Equal(_projectXml, xml); 120 | Assert.Equal(theCsProjFile, detect.ResolvedCsProjFile); 121 | } 122 | 123 | [Fact] 124 | public void CanDetectProjectFileWithEmptyBootstrapPath() 125 | { 126 | const string rootPath = "/unit-test"; 127 | var theCsProjFile = $"{rootPath}/test.csproj"; 128 | 129 | var fakeFileSystem = A.Fake(opts => opts.Strict()); 130 | A.CallTo(() => fakeFileSystem.List(A._)).Returns( 131 | new List{ 132 | theCsProjFile, 133 | $"{rootPath}/Test.cs", 134 | } 135 | ); 136 | A.CallTo(() => 137 | fakeFileSystem.Cwd() 138 | ).Returns(rootPath); 139 | A.CallTo(() => 140 | fakeFileSystem.IsCsProjectFile( 141 | A.That.Matches(str => str == $"{rootPath}"))) 142 | .Returns(false); 143 | A.CallTo(() => 144 | fakeFileSystem.LoadContent(A.That.Matches(str => str == theCsProjFile)) 145 | ).Returns(_projectXml); 146 | 147 | var detect = new ProjectFileDetector(fakeFileSystem); 148 | var xml = detect.FindAndLoadCsProj(""); 149 | 150 | Assert.Equal(_projectXml, xml); 151 | Assert.Equal(theCsProjFile, detect.ResolvedCsProjFile); 152 | } 153 | 154 | } 155 | } -------------------------------------------------------------------------------- /test/CsProj/ProjectFileParserTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Skarp.Version.Cli.CsProj; 3 | using Xunit; 4 | 5 | namespace Skarp.Version.Cli.Test.CsProj 6 | { 7 | public class ProjectFileParserTest 8 | { 9 | private readonly ProjectFileParser parser; 10 | 11 | public ProjectFileParserTest() 12 | { 13 | parser = new ProjectFileParser(); 14 | } 15 | 16 | [Fact] 17 | public void CanParseWellFormedProjectFilesWithVersionTag() 18 | { 19 | const string csProjXml = "" + 20 | "" + 21 | "netstandard1.6" + 22 | "Unit.For.The.Win" + 23 | "Unit.Testing.Library" + 24 | "1.0.0" + 25 | "1.0.0-1+master" + 26 | "" + 27 | ""; 28 | 29 | parser.Load(csProjXml, ProjectFileProperty.Version, ProjectFileProperty.Version, ProjectFileProperty.PackageVersion); 30 | Assert.Equal("1.0.0", parser.Version); 31 | Assert.Equal("1.0.0-1+master", parser.PackageVersion); 32 | } 33 | 34 | [Fact] 35 | public void CanParse_when_version_and_package_version_missing() 36 | { 37 | const string csProjXml = "" + 38 | "" + 39 | "netstandard1.6" + 40 | "Unit.For.The.Win" + 41 | "Unit.Testing.Library" + 42 | "" + 43 | ""; 44 | 45 | parser.Load(csProjXml, ProjectFileProperty.Version, ProjectFileProperty.PackageVersion, ProjectFileProperty.Version); 46 | Assert.Empty(parser.Version); 47 | Assert.Empty(parser.PackageVersion); 48 | } 49 | 50 | [Fact] 51 | public void BailsOnMalformedProjectFile() 52 | { 53 | const string csProjXml = "" + 54 | "" + 55 | "netstandard1.6" + 56 | "Unit.For.The.Win" + 57 | "Unit.Testing.Library" + 58 | "" + 59 | ""; 60 | 61 | var ex = Assert.Throws(() => 62 | parser.Load(csProjXml, ProjectFileProperty.Version) 63 | ); 64 | 65 | Assert.Contains($"The provided csproj file seems malformed - no in the root", ex.Message); 66 | Assert.Equal("xmlDocument", ex.ParamName); 67 | } 68 | 69 | [Fact] 70 | public void Works_when_no_packageId_or_title() 71 | { 72 | const string csProjXml = "" + 73 | "" + 74 | "netstandard1.6" + 75 | "Unit.For.The.Win" + 76 | "" + 77 | ""; 78 | 79 | parser.Load(csProjXml, ProjectFileProperty.Version); 80 | Assert.Empty(parser.PackageName); 81 | } 82 | 83 | [Fact] 84 | public void CanParse_when_versionprefix_is_set() 85 | { 86 | const string csProjXml = "" + 87 | "" + 88 | "netstandard1.6" + 89 | "Unit.For.The.Win" + 90 | "1.0.0" + 91 | "" + 92 | ""; 93 | 94 | parser.Load(csProjXml, ProjectFileProperty.Version); 95 | Assert.Empty(parser.PackageName); 96 | Assert.Equal("1.0.0", parser.VersionPrefix); 97 | Assert.Empty(parser.VersionSuffix); 98 | Assert.Empty(parser.Version); 99 | } 100 | 101 | 102 | [Fact] 103 | public void CanParse_when_versionprefix_and_versionsuffix_is_set() 104 | { 105 | const string csProjXml = "" + 106 | "" + 107 | "netstandard1.6" + 108 | "Unit.For.The.Win" + 109 | "1.0.0" + 110 | "SNAPSHOT" + 111 | "" + 112 | ""; 113 | 114 | parser.Load(csProjXml, ProjectFileProperty.Version); 115 | Assert.Empty(parser.PackageName); 116 | Assert.Equal("1.0.0", parser.VersionPrefix); 117 | Assert.Equal("SNAPSHOT", parser.VersionSuffix); 118 | Assert.Empty(parser.Version); 119 | } 120 | 121 | [Fact] 122 | public void Can_get_human_readable_version_from_version() 123 | { 124 | const string csProjXml = "" + 125 | "" + 126 | "netstandard1.6" + 127 | "Unit.For.The.Win" + 128 | "Unit.Testing.Library" + 129 | "1.0.0" + 130 | "1.0.0-1+master" + 131 | "" + 132 | ""; 133 | 134 | parser.Load(csProjXml, ProjectFileProperty.Version); 135 | Assert.Equal("1.0.0", parser.GetHumanReadableVersionFromSource()); 136 | } 137 | 138 | [Fact] 139 | public void Can_get_human_readable_version_from_packageversion() 140 | { 141 | const string csProjXml = "" + 142 | "" + 143 | "netstandard1.6" + 144 | "Unit.For.The.Win" + 145 | "Unit.Testing.Library" + 146 | "1.0.0" + 147 | "1.0.0-1+master" + 148 | "" + 149 | ""; 150 | 151 | parser.Load(csProjXml, ProjectFileProperty.PackageVersion); 152 | Assert.Equal("1.0.0-1+master", parser.GetHumanReadableVersionFromSource()); 153 | } 154 | 155 | [Fact] 156 | public void Can_get_human_readable_version_from_versionprefix() 157 | { 158 | const string csProjXml = "" + 159 | "" + 160 | "netstandard1.6" + 161 | "Unit.For.The.Win" + 162 | "Unit.Testing.Library" + 163 | "1.0.0" + 164 | "master" + 165 | "" + 166 | ""; 167 | 168 | parser.Load(csProjXml, ProjectFileProperty.Version, ProjectFileProperty.Version, ProjectFileProperty.VersionPrefix, 169 | ProjectFileProperty.VersionSuffix, ProjectFileProperty.PackageVersion); 170 | Assert.Equal("1.0.0-master", parser.GetHumanReadableVersionFromSource()); 171 | } 172 | } 173 | } -------------------------------------------------------------------------------- /test/CsProj/ProjectFileVersionPatcherTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FakeItEasy; 3 | using Skarp.Version.Cli.CsProj; 4 | using Skarp.Version.Cli.CsProj.FileSystem; 5 | using Xunit; 6 | 7 | namespace Skarp.Version.Cli.Test.CsProj 8 | { 9 | public class ProjectFileVersionPatcherTest 10 | { 11 | private static string _projectXml = 12 | "" + 13 | "" + 14 | "netstandard1.6" + 15 | "Unit.For.The.Win" + 16 | "Unit.Testing.Library" + 17 | "1.0.0" + 18 | "1.0.0" + 19 | "" + 20 | ""; 21 | 22 | private readonly ProjectFileVersionPatcher _patcher; 23 | private readonly IFileSystemProvider _fileSystem; 24 | 25 | public ProjectFileVersionPatcherTest() 26 | { 27 | _fileSystem = A.Fake(); 28 | _patcher = new ProjectFileVersionPatcher(_fileSystem); 29 | } 30 | 31 | [Fact] 32 | public void Throws_when_load_not_called() 33 | { 34 | var ex = Record.Exception((() => _patcher.PatchField("2.0.0", ProjectFileProperty.Version))); 35 | 36 | Assert.IsAssignableFrom(ex); 37 | } 38 | 39 | [Fact] 40 | public void CanPatchVersionOnWellFormedXml() 41 | { 42 | _patcher.Load(_projectXml); 43 | _patcher.PatchField("1.1.0-0", ProjectFileProperty.Version); 44 | 45 | var newXml = _patcher.ToXmlString(); 46 | Assert.NotEqual(_projectXml, newXml); 47 | Assert.Contains("1.1.0-0", newXml); 48 | } 49 | 50 | [Fact] 51 | public void CanPatchWhenVersionIsMissing() 52 | { 53 | var xml = 54 | "" + 55 | "" + 56 | "netstandard1.6" + 57 | "Unit.For.The.Win" + 58 | "Unit.Testing.Library" + 59 | "" + 60 | ""; 61 | 62 | _patcher.Load(xml); 63 | _patcher.PatchField("2.0.0", ProjectFileProperty.Version); 64 | var newXml = _patcher.ToXmlString(); 65 | Assert.Contains("2.0.0", newXml); 66 | } 67 | 68 | [Fact] 69 | public void PreservesWhiteSpaceWhilePatching() 70 | { 71 | var xml = 72 | "" + 73 | "" + 74 | "1.0.0" + 75 | "" + 76 | $"{Environment.NewLine}{Environment.NewLine}{Environment.NewLine}{Environment.NewLine}" + 77 | ""; 78 | 79 | _patcher.Load(xml); 80 | _patcher.PatchField("2.0.0", ProjectFileProperty.Version); 81 | var newXml = _patcher.ToXmlString(); 82 | Assert.Contains($"{Environment.NewLine}{Environment.NewLine}{Environment.NewLine}{Environment.NewLine}", 83 | newXml); 84 | } 85 | 86 | [Fact] 87 | public void HandlesMissingVersionWhenTargetFrameworksField() 88 | { 89 | var xml = 90 | "" + 91 | "" + 92 | "netstandard1.6;dotnet462" + 93 | "" + 94 | ""; 95 | 96 | _patcher.Load(xml); 97 | _patcher.PatchField("2.0.0", ProjectFileProperty.Version); 98 | var newXml = _patcher.ToXmlString(); 99 | Assert.Contains("2.0.0", newXml); 100 | } 101 | 102 | [Fact] 103 | public void BailsWhenUnableToLocatePropertyGroup() 104 | { 105 | var xml = 106 | "" + 107 | ""; 108 | 109 | _patcher.Load(xml); 110 | var ex = Record.Exception(() => _patcher.PatchField("2.0.0", ProjectFileProperty.Version)); 111 | 112 | var aex = Assert.IsAssignableFrom(ex); 113 | 114 | Assert.Equal( 115 | "Given XML does not contain Version and cannot locate existing PropertyGroup to add it to - is this a valid csproj file?", 116 | aex.Message 117 | ); 118 | } 119 | 120 | [Fact] 121 | public void Flush_calls_filesystem() 122 | { 123 | _patcher.Load(_projectXml); 124 | 125 | var thePath = "/some/path.txt"; 126 | _patcher.Flush(thePath); 127 | 128 | A.CallTo(() => _fileSystem.WriteAllContent(thePath, A._)).MustHaveHappenedOnceExactly(); 129 | } 130 | } 131 | } -------------------------------------------------------------------------------- /test/GitVcsTest.cs: -------------------------------------------------------------------------------- 1 | using Skarp.Version.Cli.Vcs.Git; 2 | using Xunit; 3 | 4 | namespace Skarp.Version.Cli.Test 5 | { 6 | public class GitVcsTest 7 | { 8 | private readonly GitVcs _vcs; 9 | 10 | public GitVcsTest() 11 | { 12 | _vcs = new GitVcs(); 13 | } 14 | 15 | [Fact( 16 | Skip = "Dont run on build servers" 17 | )] 18 | public void DetectingGitOnMachineWorks() 19 | { 20 | Assert.True(_vcs.IsVcsToolPresent()); 21 | } 22 | 23 | [Fact( 24 | Skip = "Dont run on build servers" 25 | )] 26 | public void IsRepositoryCleanWorks() 27 | { 28 | Assert.True(_vcs.IsRepositoryClean()); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /test/ProgramTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Skarp.Version.Cli.CsProj; 4 | using Xunit; 5 | 6 | namespace Skarp.Version.Cli.Test 7 | { 8 | public class ProgramTest 9 | { 10 | [Theory] 11 | [InlineData("major", VersionBump.Major)] 12 | [InlineData("premajor", VersionBump.PreMajor)] 13 | [InlineData("minor", VersionBump.Minor)] 14 | [InlineData("preminor", VersionBump.PreMinor)] 15 | [InlineData("patch", VersionBump.Patch)] 16 | [InlineData("prepatch", VersionBump.PrePatch)] 17 | [InlineData("1.0.1", VersionBump.Specific)] 18 | [InlineData("1.0.1-0", VersionBump.Specific)] 19 | [InlineData("1.0.1-0+master", VersionBump.Specific)] 20 | [InlineData("1.0.1-alpha.43+4432fsd", VersionBump.Specific)] 21 | public void GetVersionBumpFromRemainingArgsWork(string strVersionBump, VersionBump expectedBump) 22 | { 23 | var args = Program.GetVersionBumpFromRemainingArgs( 24 | new List() {strVersionBump}, 25 | OutputFormat.Text, 26 | true, 27 | true, 28 | string.Empty, 29 | string.Empty, 30 | string.Empty, 31 | string.Empty, 32 | string.Empty, 33 | string.Empty 34 | ); 35 | Assert.Equal(expectedBump, args.VersionBump); 36 | if (expectedBump == VersionBump.Specific) 37 | { 38 | Assert.Equal(strVersionBump, args.SpecificVersionToApply); 39 | } 40 | } 41 | 42 | [Fact] 43 | public void Get_version_bump_throws_on_missing_value() 44 | { 45 | var ex = Assert.Throws(() => 46 | Program.GetVersionBumpFromRemainingArgs( 47 | new List(), 48 | OutputFormat.Text, 49 | true, 50 | true, 51 | string.Empty, 52 | string.Empty, 53 | string.Empty, 54 | string.Empty, 55 | string.Empty, 56 | string.Empty 57 | ) 58 | ); 59 | Assert.Contains( 60 | $"No version bump specified, please specify one of:\n\tmajor | minor | patch | premajor | preminor | prepatch | prerelease | ", 61 | ex.Message); 62 | } 63 | 64 | [Fact] 65 | public void Get_version_bump_throws_on_invalid_value() 66 | { 67 | const string invalidVersion = "invalid-version"; 68 | 69 | var ex = Assert.Throws(() => 70 | Program.GetVersionBumpFromRemainingArgs( 71 | new List {invalidVersion}, 72 | OutputFormat.Text, 73 | true, 74 | true, 75 | string.Empty, 76 | string.Empty, 77 | string.Empty, 78 | string.Empty, 79 | string.Empty, 80 | string.Empty 81 | ) 82 | ); 83 | Assert.Contains($"Invalid SemVer version string: {invalidVersion}", 84 | ex.Message); 85 | Assert.Equal("versionString", ex.ParamName); 86 | } 87 | 88 | [Fact] 89 | public void DefaultsToReadingVersionStringFromVersionProperty() 90 | { 91 | var args = Program.GetVersionBumpFromRemainingArgs( 92 | new List() {"patch"}, 93 | OutputFormat.Text, 94 | true, 95 | true, 96 | string.Empty, 97 | string.Empty, 98 | string.Empty, 99 | string.Empty, 100 | string.Empty, 101 | string.Empty 102 | ); 103 | 104 | Assert.Equal(ProjectFileProperty.Version, args.ProjectFilePropertyName); 105 | } 106 | 107 | [Theory] 108 | [InlineData(null, ProjectFileProperty.Version)] 109 | [InlineData("", ProjectFileProperty.Version)] 110 | [InlineData("verSION", ProjectFileProperty.Version)] 111 | [InlineData("packageversion", ProjectFileProperty.PackageVersion)] 112 | public void CanOverrideTheVersionPropertyName(string input, ProjectFileProperty expected) 113 | { 114 | var args = Program.GetVersionBumpFromRemainingArgs( 115 | new List() {"patch"}, 116 | OutputFormat.Text, 117 | true, 118 | true, 119 | string.Empty, 120 | string.Empty, 121 | string.Empty, 122 | string.Empty, 123 | string.Empty, 124 | input 125 | ); 126 | 127 | Assert.Equal(expected, args.ProjectFilePropertyName); 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /test/VersionCliTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FakeItEasy; 3 | using Skarp.Version.Cli.CsProj; 4 | using Skarp.Version.Cli.Vcs; 5 | using Skarp.Version.Cli.Versioning; 6 | using Xunit; 7 | 8 | namespace Skarp.Version.Cli.Test 9 | { 10 | public class VersionCliTest 11 | { 12 | private IVcs _vcsTool; 13 | private ProjectFileDetector _fileDetector; 14 | private ProjectFileParser _fileParser; 15 | private VcsParser _vcsParser; 16 | private ProjectFileVersionPatcher _filePatcher; 17 | private VersionCli _cli; 18 | 19 | public VersionCliTest() 20 | { 21 | _vcsTool = A.Fake(opts => opts.Strict()); 22 | A.CallTo(() => _vcsTool.ToolName()).Returns("_FAKE_"); 23 | 24 | _fileDetector = A.Fake(); 25 | _fileParser = A.Fake(); 26 | _vcsParser = A.Fake(); 27 | _filePatcher = A.Fake(); 28 | 29 | A.CallTo(() => _fileDetector.FindAndLoadCsProj(A._)).Returns(""); 30 | const string csProjFilePath = "/unit-test/test.csproj"; 31 | A.CallTo(() => _fileDetector.ResolvedCsProjFile).Returns(csProjFilePath); 32 | 33 | A.CallTo(() => _fileParser.Load(A._, A._)).DoesNothing(); 34 | A.CallTo(() => _fileParser.Version).Returns("1.2.1"); 35 | A.CallTo(() => _fileParser.VersionPrefix).Returns("2.0.1"); 36 | A.CallTo(() => _fileParser.VersionSuffix).Returns("DEVELOPMENT"); 37 | A.CallTo(() => _fileParser.DesiredVersionSource).Returns(ProjectFileProperty.Version); 38 | 39 | _cli = new VersionCli( 40 | _vcsTool, 41 | _fileDetector, 42 | _fileParser, 43 | _vcsParser, 44 | _filePatcher, 45 | new SemVerBumper() 46 | ); 47 | } 48 | 49 | [Fact] 50 | public void VersionCli_Bump_VersionPrefix() 51 | { 52 | A.CallTo(() => _fileParser.Version).Returns(null); 53 | A.CallTo(() => _fileParser.VersionPrefix).Returns("2.0.1"); 54 | A.CallTo(() => _fileParser.VersionSuffix).Returns("DEVELOPMENT"); 55 | 56 | var output = _cli.Execute(new VersionCliArgs 57 | { OutputFormat = OutputFormat.Bare, VersionBump = VersionBump.None, DryRun = true }); 58 | 59 | Assert.Equal("2.0.1-DEVELOPMENT", output.OldVersion); 60 | } 61 | 62 | [Fact] 63 | public void VersionCli_throws_when_vcs_tool_is_not_present_and_doVcs_is_true() 64 | { 65 | A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(false); 66 | 67 | var ex = Assert.Throws(() => 68 | _cli.Execute(new VersionCliArgs { VersionBump = VersionBump.Major, DoVcs = true })); 69 | Assert.Equal("Unable to find the vcs tool _FAKE_ in your path", ex.Message); 70 | } 71 | 72 | [Fact] 73 | public void VersionCli_doesNotThrow_when_vcs_tool_is_not_present_if_doVcs_is_false() 74 | { 75 | A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(false); 76 | A.CallTo(() => _fileParser.Version).Returns("1.2.1"); 77 | A.CallTo(() => _fileParser.PackageVersion).Returns("1.2.1"); 78 | 79 | _cli.Execute(new VersionCliArgs { VersionBump = VersionBump.Major, DoVcs = false }); 80 | } 81 | 82 | [Fact] 83 | public void VersionCli_throws_when_repo_is_not_clean_and_doVcs_is_true() 84 | { 85 | A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); 86 | A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(false); 87 | 88 | var ex = Assert.Throws(() => 89 | _cli.Execute(new VersionCliArgs { VersionBump = VersionBump.Major, DoVcs = true })); 90 | Assert.Equal("You currently have uncomitted changes in your repository, please commit these and try again", 91 | ex.Message); 92 | } 93 | 94 | [Fact] 95 | public void VersionCli_doesNotThrow_when_repo_is_not_clean_if_doVcs_is_false() 96 | { 97 | A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); 98 | A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(false); 99 | A.CallTo(() => _fileParser.Version).Returns("1.2.1"); 100 | A.CallTo(() => _fileParser.PackageVersion).Returns("1.2.1"); 101 | 102 | _cli.Execute(new VersionCliArgs { VersionBump = VersionBump.Major, DoVcs = false }); 103 | } 104 | 105 | [Fact] 106 | public void VersionCli_can_bump_versions() 107 | { 108 | // Configure 109 | A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(true); 110 | A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); 111 | A.CallTo(() => _vcsTool.Commit(A._, A._)).DoesNothing(); 112 | A.CallTo(() => _vcsTool.Tag(A._)).DoesNothing(); 113 | 114 | A.CallTo(() => _fileDetector.FindAndLoadCsProj(A._)).Returns(""); 115 | const string csProjFilePath = "/unit-test/test.csproj"; 116 | A.CallTo(() => _fileDetector.ResolvedCsProjFile).Returns(csProjFilePath); 117 | 118 | A.CallTo(() => _fileParser.Load(A._, A._)).DoesNothing(); 119 | A.CallTo(() => _fileParser.Version).Returns("1.2.1"); 120 | A.CallTo(() => _fileParser.PackageVersion).Returns("1.2.1"); 121 | 122 | // Act 123 | _cli.Execute(new VersionCliArgs { VersionBump = VersionBump.Major, DoVcs = true, DryRun = false }); 124 | 125 | // Verify 126 | A.CallTo(() => _filePatcher.PatchField( 127 | A.That.Matches(newVer => newVer == "2.0.0"), 128 | ProjectFileProperty.Version 129 | )) 130 | .MustHaveHappened(Repeated.Exactly.Once); 131 | 132 | A.CallTo(() => _filePatcher.Flush( 133 | A.That.Matches(path => path == csProjFilePath))) 134 | .MustHaveHappened(Repeated.Exactly.Once); 135 | A.CallTo(() => _vcsTool.Commit( 136 | A.That.Matches(path => path == csProjFilePath), 137 | A.That.Matches(msg => msg == "v2.0.0"))) 138 | .MustHaveHappened(Repeated.Exactly.Once); 139 | A.CallTo(() => _vcsTool.Tag( 140 | A.That.Matches(tag => tag == "v2.0.0"))) 141 | .MustHaveHappened(Repeated.Exactly.Once); 142 | } 143 | 144 | [Fact] 145 | public void VersionCli_can_bump_pre_release_versions() 146 | { 147 | // Configure 148 | A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(true); 149 | A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); 150 | A.CallTo(() => _vcsTool.Commit(A._, A._)).DoesNothing(); 151 | A.CallTo(() => _vcsTool.Tag(A._)).DoesNothing(); 152 | 153 | A.CallTo(() => _fileDetector.FindAndLoadCsProj(A._)).Returns(""); 154 | const string csProjFilePath = "/unit-test/test.csproj"; 155 | A.CallTo(() => _fileDetector.ResolvedCsProjFile).Returns(csProjFilePath); 156 | 157 | A.CallTo(() => _fileParser.Load(A._, A._)).DoesNothing(); 158 | A.CallTo(() => _fileParser.Version).Returns("1.2.1"); 159 | A.CallTo(() => _fileParser.PackageVersion).Returns("1.2.1"); 160 | 161 | // Act 162 | _cli.Execute(new VersionCliArgs { VersionBump = VersionBump.PreMajor, DoVcs = true, DryRun = false }); 163 | 164 | // Verify 165 | A.CallTo(() => _filePatcher.PatchField( 166 | A.That.Matches(newVer => newVer == "2.0.0-next.0"), 167 | ProjectFileProperty.Version 168 | )) 169 | .MustHaveHappened(Repeated.Exactly.Once); 170 | 171 | A.CallTo(() => _filePatcher.Flush( 172 | A.That.Matches(path => path == csProjFilePath))) 173 | .MustHaveHappened(Repeated.Exactly.Once); 174 | A.CallTo(() => _vcsTool.Commit( 175 | A.That.Matches(path => path == csProjFilePath), 176 | A.That.Matches(msg => msg == "v2.0.0-next.0"))) 177 | .MustHaveHappened(Repeated.Exactly.Once); 178 | A.CallTo(() => _vcsTool.Tag( 179 | A.That.Matches(tag => tag == "v2.0.0-next.0"))) 180 | .MustHaveHappened(Repeated.Exactly.Once); 181 | } 182 | 183 | [Fact] 184 | public void VersionCli_can_bump_pre_release_with_custom_prefix() 185 | { 186 | // Configure 187 | A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(true); 188 | A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); 189 | A.CallTo(() => _vcsTool.Commit(A._, A._)).DoesNothing(); 190 | A.CallTo(() => _vcsTool.Tag(A._)).DoesNothing(); 191 | 192 | A.CallTo(() => _fileDetector.FindAndLoadCsProj(A._)).Returns(""); 193 | const string csProjFilePath = "/unit-test/test.csproj"; 194 | A.CallTo(() => _fileDetector.ResolvedCsProjFile).Returns(csProjFilePath); 195 | 196 | A.CallTo(() => _fileParser.Load(A._, A._)).DoesNothing(); 197 | A.CallTo(() => _fileParser.Version).Returns("1.2.1"); 198 | A.CallTo(() => _fileParser.PackageVersion).Returns("1.2.1"); 199 | 200 | // Act 201 | _cli.Execute(new VersionCliArgs 202 | { VersionBump = VersionBump.PreMajor, DoVcs = true, DryRun = false, PreReleasePrefix = "beta" }); 203 | 204 | // Verify 205 | A.CallTo(() => _filePatcher.PatchField( 206 | "2.0.0-beta.0", 207 | ProjectFileProperty.Version 208 | )) 209 | .MustHaveHappened(Repeated.Exactly.Once); 210 | 211 | A.CallTo(() => _filePatcher.Flush( 212 | csProjFilePath)) 213 | .MustHaveHappened(Repeated.Exactly.Once); 214 | A.CallTo(() => _vcsTool.Commit( 215 | csProjFilePath, 216 | "v2.0.0-beta.0")) 217 | .MustHaveHappened(Repeated.Exactly.Once); 218 | A.CallTo(() => _vcsTool.Tag( 219 | "v2.0.0-beta.0")) 220 | .MustHaveHappened(Repeated.Exactly.Once); 221 | } 222 | 223 | [Fact] 224 | public void VersionCli_can_bump_pre_release_with_build_meta_versions() 225 | { 226 | // Configure 227 | A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(true); 228 | A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); 229 | A.CallTo(() => _vcsTool.Commit(A._, A._)).DoesNothing(); 230 | A.CallTo(() => _vcsTool.Tag(A._)).DoesNothing(); 231 | 232 | A.CallTo(() => _fileDetector.FindAndLoadCsProj(A._)).Returns(""); 233 | const string csProjFilePath = "/unit-test/test.csproj"; 234 | A.CallTo(() => _fileDetector.ResolvedCsProjFile).Returns(csProjFilePath); 235 | 236 | A.CallTo(() => _fileParser.Load(A._, A._)).DoesNothing(); 237 | A.CallTo(() => _fileParser.Version).Returns("1.2.1"); 238 | A.CallTo(() => _fileParser.PackageVersion).Returns("1.2.1"); 239 | 240 | // Act 241 | _cli.Execute(new VersionCliArgs 242 | { VersionBump = VersionBump.PreMajor, DoVcs = true, DryRun = false, BuildMeta = "master" }); 243 | 244 | // Verify 245 | A.CallTo(() => _filePatcher.PatchField( 246 | A.That.Matches(newVer => newVer == "2.0.0-next.0+master"), 247 | ProjectFileProperty.Version 248 | )) 249 | .MustHaveHappened(Repeated.Exactly.Once); 250 | 251 | A.CallTo(() => _filePatcher.Flush( 252 | A.That.Matches(path => path == csProjFilePath))) 253 | .MustHaveHappened(Repeated.Exactly.Once); 254 | A.CallTo(() => _vcsTool.Commit( 255 | A.That.Matches(path => path == csProjFilePath), 256 | A.That.Matches(msg => msg == "v2.0.0-next.0+master"))) 257 | .MustHaveHappened(Repeated.Exactly.Once); 258 | A.CallTo(() => _vcsTool.Tag( 259 | A.That.Matches(tag => tag == "v2.0.0-next.0+master"))) 260 | .MustHaveHappened(Repeated.Exactly.Once); 261 | } 262 | 263 | [Fact] 264 | public void VersionCli_can_bump_versions_can_skip_vcs() 265 | { 266 | // Configure 267 | A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(true); 268 | A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); 269 | A.CallTo(() => _vcsTool.Commit(A._, A._)).DoesNothing(); 270 | A.CallTo(() => _vcsTool.Tag(A._)).DoesNothing(); 271 | 272 | A.CallTo(() => _fileDetector.FindAndLoadCsProj(A._)).Returns(""); 273 | const string csProjFilePath = "/unit-test/test.csproj"; 274 | A.CallTo(() => _fileDetector.ResolvedCsProjFile).Returns(csProjFilePath); 275 | 276 | A.CallTo(() => _fileParser.Load(A._, A._)).DoesNothing(); 277 | A.CallTo(() => _fileParser.Version).Returns("1.2.1"); 278 | A.CallTo(() => _fileParser.PackageVersion).Returns("1.2.1"); 279 | 280 | // Act 281 | _cli.Execute(new VersionCliArgs { VersionBump = VersionBump.Major, DoVcs = false, DryRun = false }); 282 | 283 | // Verify 284 | A.CallTo(() => _filePatcher.PatchField( 285 | A.That.Matches(newVer => newVer == "2.0.0"), 286 | ProjectFileProperty.Version 287 | )) 288 | .MustHaveHappened(Repeated.Exactly.Once); 289 | A.CallTo(() => _filePatcher.Flush( 290 | A.That.Matches(path => path == csProjFilePath))) 291 | .MustHaveHappened(Repeated.Exactly.Once); 292 | A.CallTo(() => _vcsTool.Commit(A._, A._)).MustNotHaveHappened(); 293 | A.CallTo(() => _vcsTool.Tag(A._)).MustNotHaveHappened(); 294 | } 295 | 296 | [Fact] 297 | public void VersionCli_can_bump_versions_can_dry_run() 298 | { 299 | // Configure 300 | A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(true); 301 | A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); 302 | A.CallTo(() => _vcsTool.Commit(A._, A._)).DoesNothing(); 303 | A.CallTo(() => _vcsTool.Tag(A._)).DoesNothing(); 304 | 305 | A.CallTo(() => _fileDetector.FindAndLoadCsProj(A._)).Returns(""); 306 | const string csProjFilePath = "/unit-test/test.csproj"; 307 | A.CallTo(() => _fileDetector.ResolvedCsProjFile).Returns(csProjFilePath); 308 | 309 | A.CallTo(() => _fileParser.Load(A._, A._)).DoesNothing(); 310 | A.CallTo(() => _fileParser.Version).Returns("1.2.1"); 311 | A.CallTo(() => _fileParser.PackageVersion).Returns("1.2.1"); 312 | 313 | // Act 314 | var info = _cli.Execute(new VersionCliArgs 315 | { VersionBump = VersionBump.Major, DoVcs = true, DryRun = true }); 316 | 317 | Assert.NotEqual(info.OldVersion, info.NewVersion); 318 | Assert.Equal("2.0.0", info.NewVersion); 319 | 320 | // Verify 321 | A.CallTo(() => _filePatcher.PatchField( 322 | A.That.Matches(newVer => newVer == "2.0.0"), 323 | ProjectFileProperty.Version 324 | )) 325 | .MustNotHaveHappened(); 326 | 327 | A.CallTo(() => _filePatcher.Flush( 328 | A.That.Matches(path => path == csProjFilePath))) 329 | .MustNotHaveHappened(); 330 | A.CallTo(() => _vcsTool.Commit(A._, A._)).MustNotHaveHappened(); 331 | A.CallTo(() => _vcsTool.Tag(A._)).MustNotHaveHappened(); 332 | } 333 | 334 | [Fact] 335 | public void VersionCli_can_set_vcs_commit_message() 336 | { 337 | // Configure 338 | A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(true); 339 | A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); 340 | A.CallTo(() => _vcsTool.Commit(A._, A._)).DoesNothing(); 341 | A.CallTo(() => _vcsTool.Tag(A._)).DoesNothing(); 342 | 343 | A.CallTo(() => _fileDetector.FindAndLoadCsProj(A._)).Returns(""); 344 | const string csProjFilePath = "/unit-test/test.csproj"; 345 | A.CallTo(() => _fileDetector.ResolvedCsProjFile).Returns(csProjFilePath); 346 | 347 | A.CallTo(() => _fileParser.Load(A._, A._)).DoesNothing(); 348 | A.CallTo(() => _fileParser.Version).Returns("1.2.1"); 349 | A.CallTo(() => _fileParser.PackageVersion).Returns("1.2.1"); 350 | 351 | // Act 352 | _cli.Execute(new VersionCliArgs 353 | { VersionBump = VersionBump.Major, DoVcs = true, DryRun = false, CommitMessage = "commit message" }); 354 | 355 | // Verify 356 | A.CallTo(() => _filePatcher.PatchField( 357 | A.That.Matches(newVer => newVer == "2.0.0"), 358 | ProjectFileProperty.Version 359 | )) 360 | .MustHaveHappened(Repeated.Exactly.Once); 361 | 362 | A.CallTo(() => _filePatcher.Flush( 363 | A.That.Matches(path => path == csProjFilePath))) 364 | .MustHaveHappened(Repeated.Exactly.Once); 365 | A.CallTo(() => _vcsTool.Commit( 366 | A.That.Matches(path => path == csProjFilePath), 367 | A.That.Matches(msg => msg == "commit message"))) 368 | .MustHaveHappened(Repeated.Exactly.Once); 369 | A.CallTo(() => _vcsTool.Tag( 370 | A.That.Matches(tag => tag == "v2.0.0"))) 371 | .MustHaveHappened(Repeated.Exactly.Once); 372 | } 373 | 374 | [Fact] 375 | public void VersionCli_can_set_vcs_commit_message_with_variables() 376 | { 377 | // Configure 378 | A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(true); 379 | A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); 380 | A.CallTo(() => _vcsTool.Commit(A._, A._)).DoesNothing(); 381 | A.CallTo(() => _vcsTool.Tag(A._)).DoesNothing(); 382 | 383 | A.CallTo(() => _fileDetector.FindAndLoadCsProj(A._)).Returns(""); 384 | const string csProjFilePath = "/unit-test/test.csproj"; 385 | A.CallTo(() => _fileDetector.ResolvedCsProjFile).Returns(csProjFilePath); 386 | 387 | A.CallTo(() => _fileParser.Load(A._, A._)).DoesNothing(); 388 | A.CallTo(() => _fileParser.Version).Returns("1.2.1"); 389 | A.CallTo(() => _fileParser.PackageVersion).Returns("1.2.1"); 390 | A.CallTo(() => _fileParser.PackageName).Returns("unit-test"); 391 | 392 | // Act 393 | _cli.Execute(new VersionCliArgs 394 | { 395 | VersionBump = VersionBump.Major, DoVcs = true, DryRun = false, 396 | CommitMessage = "bump from v$oldVer to v$newVer at $projName" 397 | }); 398 | 399 | // Verify 400 | A.CallTo(() => _filePatcher.PatchField( 401 | A.That.Matches(newVer => newVer == "2.0.0"), 402 | ProjectFileProperty.Version 403 | )) 404 | .MustHaveHappened(Repeated.Exactly.Once); 405 | 406 | A.CallTo(() => _filePatcher.Flush( 407 | A.That.Matches(path => path == csProjFilePath))) 408 | .MustHaveHappened(Repeated.Exactly.Once); 409 | A.CallTo(() => _vcsTool.Commit( 410 | A.That.Matches(path => path == csProjFilePath), 411 | A.That.Matches(msg => msg == "bump from v1.2.1 to v2.0.0 at unit-test"))) 412 | .MustHaveHappened(Repeated.Exactly.Once); 413 | A.CallTo(() => _vcsTool.Tag( 414 | A.That.Matches(tag => tag == "v2.0.0"))) 415 | .MustHaveHappened(Repeated.Exactly.Once); 416 | } 417 | 418 | [Fact] 419 | public void VersionCli_can_set_vcs_tag() 420 | { 421 | // Configure 422 | A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(true); 423 | A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); 424 | A.CallTo(() => _vcsTool.Commit(A._, A._)).DoesNothing(); 425 | A.CallTo(() => _vcsTool.Tag(A._)).DoesNothing(); 426 | 427 | A.CallTo(() => _fileDetector.FindAndLoadCsProj(A._)).Returns(""); 428 | const string csProjFilePath = "/unit-test/test.csproj"; 429 | A.CallTo(() => _fileDetector.ResolvedCsProjFile).Returns(csProjFilePath); 430 | 431 | A.CallTo(() => _fileParser.Load(A._, A._)).DoesNothing(); 432 | A.CallTo(() => _fileParser.Version).Returns("1.2.1"); 433 | A.CallTo(() => _fileParser.PackageVersion).Returns("1.2.1"); 434 | 435 | // Act 436 | _cli.Execute(new VersionCliArgs 437 | { VersionBump = VersionBump.Major, DoVcs = true, DryRun = false, VersionControlTag = "vcs tag" }); 438 | 439 | // Verify 440 | A.CallTo(() => _filePatcher.PatchField( 441 | A.That.Matches(newVer => newVer == "2.0.0"), 442 | ProjectFileProperty.Version 443 | )) 444 | .MustHaveHappened(Repeated.Exactly.Once); 445 | 446 | A.CallTo(() => _filePatcher.Flush( 447 | A.That.Matches(path => path == csProjFilePath))) 448 | .MustHaveHappened(Repeated.Exactly.Once); 449 | A.CallTo(() => _vcsTool.Commit( 450 | A.That.Matches(path => path == csProjFilePath), 451 | A.That.Matches(msg => msg == "v2.0.0"))) 452 | .MustHaveHappened(Repeated.Exactly.Once); 453 | A.CallTo(() => _vcsTool.Tag( 454 | A.That.Matches(tag => tag == "vcs tag"))) 455 | .MustHaveHappened(Repeated.Exactly.Once); 456 | } 457 | 458 | [Fact] 459 | public void VersionCli_can_set_vcs_tag_with_variables() 460 | { 461 | // Configure 462 | A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(true); 463 | A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); 464 | A.CallTo(() => _vcsTool.Commit(A._, A._)).DoesNothing(); 465 | A.CallTo(() => _vcsTool.Tag(A._)).DoesNothing(); 466 | 467 | A.CallTo(() => _fileDetector.FindAndLoadCsProj(A._)).Returns(""); 468 | const string csProjFilePath = "/unit-test/test.csproj"; 469 | A.CallTo(() => _fileDetector.ResolvedCsProjFile).Returns(csProjFilePath); 470 | 471 | A.CallTo(() => _fileParser.Load(A._, A._)).DoesNothing(); 472 | A.CallTo(() => _fileParser.Version).Returns("1.2.1"); 473 | A.CallTo(() => _fileParser.PackageVersion).Returns("1.2.1"); 474 | A.CallTo(() => _fileParser.PackageName).Returns("unit-test"); 475 | 476 | // Act 477 | _cli.Execute(new VersionCliArgs 478 | { 479 | VersionBump = VersionBump.Major, DoVcs = true, DryRun = false, 480 | VersionControlTag = "bump from v$oldVer to v$newVer at $projName" 481 | }); 482 | 483 | // Verify 484 | A.CallTo(() => _filePatcher.PatchField( 485 | A.That.Matches(newVer => newVer == "2.0.0"), 486 | ProjectFileProperty.Version 487 | )) 488 | .MustHaveHappened(Repeated.Exactly.Once); 489 | 490 | A.CallTo(() => _filePatcher.Flush( 491 | A.That.Matches(path => path == csProjFilePath))) 492 | .MustHaveHappened(Repeated.Exactly.Once); 493 | A.CallTo(() => _vcsTool.Commit( 494 | A.That.Matches(path => path == csProjFilePath), 495 | A.That.Matches(msg => msg == "v2.0.0"))) 496 | .MustHaveHappened(Repeated.Exactly.Once); 497 | A.CallTo(() => _vcsTool.Tag( 498 | A.That.Matches(tag => tag == "bump from v1.2.1 to v2.0.0 at unit-test"))) 499 | .MustHaveHappened(Repeated.Exactly.Once); 500 | } 501 | 502 | [Fact] 503 | public void VersionCli_can_read_version_from_version_field() 504 | { 505 | // Configure 506 | A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(true); 507 | A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); 508 | A.CallTo(() => _vcsTool.Commit(A._, A._)).DoesNothing(); 509 | A.CallTo(() => _vcsTool.Tag(A._)).DoesNothing(); 510 | 511 | A.CallTo(() => _fileDetector.FindAndLoadCsProj(A._)).Returns(""); 512 | const string csProjFilePath = "/unit-test/test.csproj"; 513 | A.CallTo(() => _fileDetector.ResolvedCsProjFile).Returns(csProjFilePath); 514 | 515 | A.CallTo(() => _fileParser.Load(A._, A._)).DoesNothing(); 516 | A.CallTo(() => _fileParser.Version).Returns("2.0.0"); 517 | A.CallTo(() => _fileParser.PackageVersion).Returns("1.0.0"); 518 | 519 | // Act 520 | var output = _cli.Execute(new VersionCliArgs 521 | { 522 | ProjectFilePropertyName = ProjectFileProperty.Version, 523 | VersionBump = VersionBump.None, 524 | OutputFormat = OutputFormat.Bare, 525 | DoVcs = true, 526 | DryRun = false 527 | }); 528 | 529 | Assert.Equal("2.0.0", output.OldVersion); 530 | } 531 | 532 | [Fact] 533 | public void VersionCli_can_read_version_from_package_version_field() 534 | { 535 | // Configure 536 | A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(true); 537 | A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); 538 | A.CallTo(() => _vcsTool.Commit(A._, A._)).DoesNothing(); 539 | A.CallTo(() => _vcsTool.Tag(A._)).DoesNothing(); 540 | 541 | A.CallTo(() => _fileDetector.FindAndLoadCsProj(A._)).Returns(""); 542 | const string csProjFilePath = "/unit-test/test.csproj"; 543 | A.CallTo(() => _fileDetector.ResolvedCsProjFile).Returns(csProjFilePath); 544 | 545 | A.CallTo(() => _fileParser.Load(A._, A._)).DoesNothing(); 546 | A.CallTo(() => _fileParser.Version).Returns("2.0.0"); 547 | A.CallTo(() => _fileParser.PackageVersion).Returns("1.0.0"); 548 | A.CallTo(() => _fileParser.DesiredVersionSource).Returns(ProjectFileProperty.PackageVersion); 549 | 550 | // Act 551 | var output = _cli.Execute(new VersionCliArgs 552 | { 553 | ProjectFilePropertyName = ProjectFileProperty.PackageVersion, 554 | VersionBump = VersionBump.None, 555 | OutputFormat = OutputFormat.Bare, 556 | DoVcs = true, 557 | DryRun = false 558 | }); 559 | 560 | Assert.Equal("1.0.0", output.OldVersion); 561 | } 562 | } 563 | } -------------------------------------------------------------------------------- /test/Versioning/SemVerBumperTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Skarp.Version.Cli.Versioning; 3 | using Xunit; 4 | 5 | namespace Skarp.Version.Cli.Test.Versioning 6 | { 7 | public class SemVerBumperTests 8 | { 9 | private readonly SemVerBumper _bumper; 10 | 11 | public SemVerBumperTests() 12 | { 13 | _bumper = new SemVerBumper(); 14 | } 15 | 16 | [Theory] 17 | [InlineData("1.1.0", VersionBump.Major, 2, 0, 0, "", "")] 18 | [InlineData("1.1.0", VersionBump.PreMajor, 2, 0, 0, "next.0", "")] 19 | [InlineData("4.1.3", VersionBump.Minor, 4, 2, 0, "", "")] 20 | [InlineData("4.1.3", VersionBump.PreMinor, 4, 2, 0, "next.0", "")] 21 | [InlineData("2.1.0", VersionBump.Patch, 2, 1, 1, "", "")] 22 | [InlineData("2.1.0", VersionBump.PrePatch, 2, 1, 1, "next.0", "")] 23 | [InlineData("3.2.1", VersionBump.Specific, 3, 2, 1, "", "")] 24 | [InlineData("3.2.1-0+master", VersionBump.Specific, 3, 2, 1, "0", "master")] 25 | [InlineData("6.0.2-1-g609a472", VersionBump.Specific, 6, 0, 2, "1-g609a472", "")] 26 | public void CanBumpVersions( 27 | string version, 28 | VersionBump bump, 29 | int expectedMajor, 30 | int expectedMinor, 31 | int expectedPatch, 32 | string expectedPreRelease, 33 | string expectedBuildMeta 34 | ) 35 | { 36 | var semver = _bumper.Bump( 37 | SemVer.FromString(version), 38 | bump, 39 | version 40 | ); 41 | 42 | Assert.Equal(expectedMajor, semver.Major); 43 | Assert.Equal(expectedMinor, semver.Minor); 44 | Assert.Equal(expectedPatch, semver.Patch); 45 | Assert.Equal(expectedPreRelease, semver.PreRelease); 46 | Assert.Equal(expectedBuildMeta, semver.BuildMeta); 47 | } 48 | 49 | [Theory] 50 | [InlineData("1.0.0", VersionBump.Major, "2.0.0")] 51 | [InlineData("1.0.0", VersionBump.PreMajor, "2.0.0-next.0")] 52 | [InlineData("4.1.3", VersionBump.Minor, "4.2.0")] 53 | [InlineData("4.1.3", VersionBump.PreMinor, "4.2.0-next.0")] 54 | [InlineData("2.1.0", VersionBump.Patch, "2.1.1")] 55 | [InlineData("2.1.0", VersionBump.PrePatch, "2.1.1-next.0")] 56 | // snap out of pre-release mode 57 | [InlineData("2.0.0-next.2", VersionBump.Major, "2.0.0")] 58 | [InlineData("1.1.1-42", VersionBump.Patch, "1.1.1")] 59 | [InlineData("1.1.1-42+master", VersionBump.Patch, "1.1.1")] 60 | [InlineData("4.1.3-next.1", VersionBump.Minor, "4.1.3")] 61 | 62 | // increment prerelease number 63 | [InlineData("1.1.1-42", VersionBump.PreRelease, "1.1.1-next.43")] 64 | [InlineData("1.1.1-next.42", VersionBump.PreRelease, "1.1.1-next.43")] 65 | public void CanBumpAndSerializeStringVersion(string version, VersionBump bump, string expectedVersion) 66 | { 67 | var semver = _bumper.Bump(SemVer.FromString(version), bump); 68 | Assert.Equal(expectedVersion, semver.ToSemVerVersionString(null)); 69 | } 70 | 71 | [Theory] 72 | [InlineData("1.0.0", VersionBump.PreMajor, "2.0.0-alpha.0", "alpha")] 73 | [InlineData("1.0.0", VersionBump.PreMinor, "1.1.0-beta.0", "beta")] 74 | [InlineData("1.0.0", VersionBump.PrePatch, "1.0.1-pre.0", "pre")] 75 | public void Respects_custom_pre_release_prefix( 76 | string version, 77 | VersionBump bump, 78 | string expectedVersion, 79 | string prefix 80 | ) 81 | { 82 | var semver = _bumper.Bump(SemVer.FromString(version), bump, preReleasePrefix: prefix); 83 | Assert.Equal(expectedVersion, semver.ToSemVerVersionString(null)); 84 | } 85 | 86 | [Fact] 87 | public void Bails_when_bump_is_not_supported() 88 | { 89 | var ex = Record.Exception(() => _bumper.Bump(SemVer.FromString("1.0.0"), VersionBump.Unknown)); 90 | var aex = Assert.IsAssignableFrom(ex); 91 | Assert.Contains( 92 | "VersionBump : Unknown not supported", 93 | aex.Message 94 | ); 95 | } 96 | 97 | [Fact] 98 | public void Bails_when_specific_version_empty() 99 | { 100 | var ex = Record.Exception(() => _bumper.Bump(SemVer.FromString("1.0.0"), VersionBump.Specific, "")); 101 | Assert.IsAssignableFrom(ex); 102 | } 103 | 104 | [Fact] 105 | public void Complains_about_prerelease_bump_if_not_already_pre() 106 | { 107 | var semver = SemVer.FromString("2.0.0"); 108 | var ex = Record.Exception((() => _bumper.Bump(semver, VersionBump.PreRelease))); 109 | 110 | var iex = Assert.IsAssignableFrom(ex); 111 | Assert.Contains("Cannot Prerelease bump when not", iex.Message); 112 | } 113 | 114 | [Theory] 115 | [InlineData("1.0.0-alpha-1")] 116 | [InlineData("1.0.0-alpha-notANumber")] 117 | [InlineData("1.0.0-alpha.notANumber")] 118 | public void Bails_when_prerelease_label_is_messy(string version) 119 | { 120 | var semver = SemVer.FromString(version); 121 | 122 | var ex = Record.Exception((() => _bumper.Bump(semver, VersionBump.PreRelease))); 123 | var aex = Assert.IsAssignableFrom(ex); 124 | Assert.Contains("`label.number`", aex.Message); 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /test/Versioning/SemVerTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Skarp.Version.Cli.Versioning; 3 | using Xunit; 4 | 5 | namespace Skarp.Version.Cli.Test.Versioning 6 | { 7 | public class SemVerTest 8 | { 9 | [Theory] 10 | // valid cases - Semver 2.0 is crazy! 11 | [InlineData("1.0.0", 1, 0, 0, "", "", true)] 12 | [InlineData("0.10.0", 0, 10, 0, "", "", true)] 13 | [InlineData("1.12.99", 1, 12, 99, "", "", true)] 14 | [InlineData("4.99.43245", 4, 99, 43245, "", "", true)] 15 | [InlineData("4.1.3", 4, 1, 3, "", "", true)] 16 | [InlineData("42.4.554", 42, 4, 554, "", "", true)] 17 | [InlineData("3.1.554-alpha.33", 3, 1, 554, "alpha.33", "", true)] 18 | [InlineData("3.1.554-alpha.33+master", 3, 1, 554, "alpha.33", "master", true)] 19 | public void CanParseValidSemVers( 20 | string version, 21 | int expectedMajor, 22 | int expectedMinor, 23 | int expectedPatch, 24 | string expectedPreReleaseBuildInfo, 25 | string expectedBuildMeta, 26 | bool isValid 27 | ) 28 | { 29 | if (!isValid) 30 | { 31 | var ex = Record.Exception(() => SemVer.FromString(version)); 32 | Assert.IsAssignableFrom(ex); 33 | 34 | return; 35 | } 36 | 37 | var semver = SemVer.FromString(version); 38 | 39 | Assert.Equal(expectedMajor, semver.Major); 40 | Assert.Equal(expectedMinor, semver.Minor); 41 | Assert.Equal(expectedPatch, semver.Patch); 42 | Assert.Equal(expectedPreReleaseBuildInfo, semver.PreRelease); 43 | Assert.Equal(expectedBuildMeta, semver.BuildMeta); 44 | 45 | if (!string.IsNullOrWhiteSpace(semver.PreRelease)) 46 | { 47 | Assert.True(semver.IsPreRelease); 48 | } 49 | } 50 | 51 | [Theory] 52 | [InlineData("is-this-a-version")] 53 | [InlineData("1,0")] 54 | [InlineData("2")] 55 | [InlineData("2.0")] 56 | [InlineData("2.0.1.2.3.4")] 57 | public void BailsOnInvalidSemVers(string version) 58 | { 59 | var ex = Assert.Throws(() => SemVer.FromString(version)); 60 | Assert.Equal("versionString", ex.ParamName); 61 | Assert.Contains($"Invalid SemVer version string: {version}", ex.Message); 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /test/dotnet-version-test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0;net7.0;net8.0 4 | Skarp.Version.Cli.Test 5 | fb420acf-9e12-42b6-b724-1eee9cbf251e 6 | 7 | 8 | 9 | runtime; build; native; contentfiles; analyzers; buildtransitive 10 | all 11 | 12 | 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | all 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | --------------------------------------------------------------------------------