diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 00000000..151689b6 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,30 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "powershell": { + "version": "7.3.4", + "commands": [ + "pwsh" + ] + }, + "dotnet-format": { + "version": "5.1.250801", + "commands": [ + "dotnet-format" + ] + }, + "dotnet-coverage": { + "version": "17.7.0", + "commands": [ + "dotnet-coverage" + ] + }, + "nbgv": { + "version": "3.5.119", + "commands": [ + "nbgv" + ] + } + } +} diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..01c94a90 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,14 @@ +# Refer to https://hub.docker.com/_/microsoft-dotnet-sdk for available versions +FROM mcr.microsoft.com/dotnet/sdk:7.0.203-jammy + +# Installing mono makes `dotnet test` work without errors even for net472. +# But installing it takes a long time, so it's excluded by default. +#RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF +#RUN echo "deb https://download.mono-project.com/repo/ubuntu stable-bionic main" | tee /etc/apt/sources.list.d/mono-official-stable.list +#RUN apt-get update +#RUN DEBIAN_FRONTEND=noninteractive apt-get install -y mono-devel + +# Clear the NUGET_XMLDOC_MODE env var so xml api doc files get unpacked, allowing a rich experience in Intellisense. +# See https://github.com/dotnet/dotnet-docker/issues/2790 for a discussion on this, where the prioritized use case +# was *not* devcontainers, sadly. +ENV NUGET_XMLDOC_MODE= diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..f4e3b31a --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,20 @@ +{ + "name": "Dev space", + "dockerFile": "Dockerfile", + "settings": { + "terminal.integrated.shell.linux": "/usr/bin/pwsh" + }, + "postCreateCommand": "./init.ps1 -InstallLocality machine", + "extensions": [ + "ms-azure-devops.azure-pipelines", + "ms-dotnettools.csharp", + "k--kato.docomment", + "editorconfig.editorconfig", + "pflannery.vscode-versionlens", + "davidanson.vscode-markdownlint", + "dotjoshjohnson.xml", + "ms-vscode-remote.remote-containers", + "ms-azuretools.vscode-docker", + "ms-vscode.powershell" + ] +} diff --git a/.editorconfig b/.editorconfig index fb31b8c2..e0b8f033 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,32 +6,36 @@ root = true # Don't use tabs for indentation. [*] indent_style = space + # (Please don't specify an indent_size here; that has too many unintended consequences.) [*.yml] indent_size = 2 +indent_style = space # Code files -[*.{cs,csx,vb,vbx}] +[*.{cs,csx,vb,vbx,h,cpp,idl}] indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true -# Xml project files -[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +# MSBuild project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,msbuildproj,props,targets}] indent_size = 2 # Xml config files -[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +[*.{ruleset,config,nuspec,resx,vsixmanifest,vsct,runsettings}] indent_size = 2 # JSON files [*.json] indent_size = 2 +indent_style = space # Dotnet code style settings: [*.{cs,vb}] # Sort using and Import directives with System.* appearing first dotnet_sort_system_directives_first = true -# Use "this." and "Me." everywhere dotnet_style_qualification_for_field = true:warning dotnet_style_qualification_for_property = true:warning dotnet_style_qualification_for_method = true:warning @@ -48,22 +52,96 @@ dotnet_style_coalesce_expression = true:suggestion dotnet_style_null_propagation = true:suggestion dotnet_style_explicit_tuple_names = true:suggestion +# Non-private static fields are PascalCase +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style + +dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected internal, private protected +dotnet_naming_symbols.non_private_static_fields.required_modifiers = static + +dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case + +# Constants are PascalCase +dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants +dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style + +dotnet_naming_symbols.constants.applicable_kinds = field, local +dotnet_naming_symbols.constants.required_modifiers = const + +dotnet_naming_style.constant_style.capitalization = pascal_case + +# Static fields are camelCase +dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion +dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields +dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style + +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static + +dotnet_naming_style.static_field_style.capitalization = camel_case + +# Instance fields are camelCase +dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion +dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields +dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style + +dotnet_naming_symbols.instance_fields.applicable_kinds = field + +dotnet_naming_style.instance_field_style.capitalization = camel_case + +# Locals and parameters are camelCase +dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion +dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters +dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style + +dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local + +dotnet_naming_style.camel_case_style.capitalization = camel_case + +# Local functions are PascalCase +dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions +dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style + +dotnet_naming_symbols.local_functions.applicable_kinds = local_function + +dotnet_naming_style.local_function_style.capitalization = pascal_case + +# By default, name items with PascalCase +dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members +dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style + +dotnet_naming_symbols.all_members.applicable_kinds = * + +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + # CSharp code style settings: [*.cs] +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left + # Prefer "var" everywhere -csharp_style_var_for_built_in_types = false:none -csharp_style_var_when_type_is_apparent = true:none -csharp_style_var_elsewhere = false:none +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = false:warning # Prefer method-like constructs to have a block body -csharp_style_expression_bodied_methods = true:suggestion -csharp_style_expression_bodied_constructors = true:suggestion -csharp_style_expression_bodied_operators = true:suggestion +csharp_style_expression_bodied_methods = false:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none # Prefer property-like constructs to have an expression-body -csharp_style_expression_bodied_properties = true:suggestion -csharp_style_expression_bodied_indexers = true:suggestion -csharp_style_expression_bodied_accessors = true:suggestion +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none # Suggest more modern language features when available csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion @@ -85,3 +163,24 @@ dotnet_diagnostic.CSIsNull001.severity = warning # CSIsNull002: Use `is object` for non-null checks dotnet_diagnostic.CSIsNull002.severity = warning + +# Blocks are allowed +csharp_prefer_braces = true:silent + +# SA1130: Use lambda syntax +dotnet_diagnostic.SA1130.severity = silent + +# IDE1006: Naming Styles - StyleCop handles these for us +dotnet_diagnostic.IDE1006.severity = none + +dotnet_diagnostic.DOC100.severity = silent +dotnet_diagnostic.DOC104.severity = warning +dotnet_diagnostic.DOC105.severity = warning +dotnet_diagnostic.DOC106.severity = warning +dotnet_diagnostic.DOC107.severity = warning +dotnet_diagnostic.DOC108.severity = warning +dotnet_diagnostic.DOC200.severity = warning +dotnet_diagnostic.DOC202.severity = warning + +[*.sln] +indent_style = tab diff --git a/.gitattributes b/.gitattributes index e9ae1b43..1f35e683 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,7 +2,13 @@ # Set default behavior to automatically normalize line endings. ############################################################################### * text=auto -*.sh text eol=lf + +# Ensure shell scripts use LF line endings (linux only accepts LF) +*.sh eol=lf +*.ps1 eol=lf + +# The macOS codesign tool is extremely picky, and requires LF line endings. +*.plist eol=lf ############################################################################### # Set default behavior for command prompt diff. diff --git a/.gitignore b/.gitignore index 41470811..69599b87 100644 --- a/.gitignore +++ b/.gitignore @@ -1,57 +1,88 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files +*.rsuser *.suo *.user +*.userosscache *.sln.docstates -.vs/ +*.lutconfig launchSettings.json +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ +[Rr]eleases/ x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ +[Ll]og/ -# Roslyn cache directories -*.ide/ +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* -#NUNIT +# NUnit *.VisualState.xml TestResult.xml +nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio *_i.c *_p.c -*_i.h +*_h.h *.ilk *.meta *.obj +*.iobj *.pch *.pdb +*.ipdb *.pgc *.pgd *.rsp +!Directory.Build.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj +*_wpftmp.csproj *.log -*.binlog *.vspscc *.vssscc .builds @@ -66,14 +97,21 @@ _Chutzpah* ipch/ *.aps *.ncb +*.opendb *.opensdf *.sdf *.cachefile +*.VC.db +*.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx +*.sap + +# Visual Studio Trace Files +*.e2e # TFS 2012 Local Workspace $tf/ @@ -86,7 +124,7 @@ _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user -# JustCode is a .NET coding addin-in +# JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in @@ -95,9 +133,19 @@ _TeamCity* # DotCover is a Code Coverage Tool *.dotCover +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml +/coveragereport/ + # NCrunch _NCrunch_* .*crunch*.local.xml +nCrunchTemp_* # MightyMoose *.mm.* @@ -125,49 +173,71 @@ publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml -## TODO: Comment the next line if you want to checkin your -## web deploy settings but do note that will include unencrypted -## passwords -#*.pubxml - -# NuGet Packages Directory -packages +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets -project.lock.json - -# NPM -package-lock.json - -## TODO: If the tool you use requires repositories.config -## uncomment the next line -#!packages/repositories.config -# Enable "build/" folder in the NuGet Packages folder since -# NuGet packages use it for MSBuild targets. -# This line needs to be after the ignore of the build folder -# (and the packages folder if the line above has been uncommented) -!packages/build/ - -# Windows Azure Build Output +# Microsoft Azure Build Output csx/ *.build.csdef -# Windows Store app package directory +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ # Others -sql/ -*.Cache ClientBin/ -[Ss]tyle[Cc]op.* ~$* *~ *.dbmdl *.dbproj.schemaview +*.jfm +*.pfx *.publishsettings -node_modules/ -bower_components/ +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ # RIA/Silverlight projects Generated_Code/ @@ -179,20 +249,106 @@ _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak # SQL Server files *.mdf *.ldf +*.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ -# LightSwitch generated files -GeneratedArtifacts/ -_Pvt_Extensions/ -ModelManifest.xml \ No newline at end of file +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# dotnet tool local install directory +.store/ + +# mac-created file to track user view preferences for a directory +.DS_Store diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 9335c88f..c4ddc1a7 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,20 @@ { + // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. + // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp + // List of extensions which should be recommended for users of this workspace. "recommendations": [ - "formulahendry.dotnet-test-explorer", + "ms-azure-devops.azure-pipelines", "ms-dotnettools.csharp", - ] + "k--kato.docomment", + "editorconfig.editorconfig", + "formulahendry.dotnet-test-explorer", + "pflannery.vscode-versionlens", + "davidanson.vscode-markdownlint", + "dotjoshjohnson.xml", + "ms-vscode-remote.remote-containers", + "ms-azuretools.vscode-docker", + "tintoy.msbuild-project-tools" + ], + // List of extensions recommended by VS Code that should not be recommended for users of this workspace. + "unwantedRecommendations": [] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 83730d6f..9fccf55b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,9 @@ { - "dotnet-test-explorer.testProjectPath": "src/NerdBank.GitVersioning.Tests/NerdBank.GitVersioning.Tests.csproj" -} \ No newline at end of file + "dotnet-test-explorer.testProjectPath": "src/Nerdbank.GitVersioning.Tests/Nerdbank.GitVersioning.Tests.csproj", + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + "files.trimFinalNewlines": true, + "omnisharp.enableEditorConfigSupport": true, + "omnisharp.enableImportCompletion": true, + "omnisharp.enableRoslynAnalyzers": true +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 09656a68..86ea3ae2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ It is highly recommended that anyone contributing to this library use the same software. 1. [Visual Studio 2019][VS] -2. [Node.js][NodeJs] +2. [Node.js][NodeJs] v16 (v18 breaks our build) ### Optional additional software diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..22caf1d1 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,81 @@ + + + Debug + $(MSBuildThisFileDirectory) + $(RepoRootPath)obj\$([MSBuild]::MakeRelative($(RepoRootPath), $(MSBuildProjectDirectory)))\ + $(RepoRootPath)bin\$(MSBuildProjectName)\ + $(RepoRootPath)bin\Packages\$(Configuration)\ + $(MSBuildThisFileDirectory)..\wiki\api + latest + + enable + latest + true + true + true + + + true + + + + false + + + $(MSBuildThisFileDirectory) + + embedded + + true + $(MSBuildThisFileDirectory)strongname.snk + + https://github.com/dotnet/Nerdbank.GitVersioning + Andrew Arnott + git commit versioning version assemblyinfo + Copyright (c) .NET Foundation and Contributors + MIT + true + true + + + + + 2.0.315-alpha.0.9 + + + + + + + + + + + + + + + + $(PackageProjectUrl)/releases/tag/v$(Version) + + + + + false + true + + + + + false + false + false + false + + diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 00000000..ea7b6e6f --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,11 @@ + + + + false + + + + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 00000000..c27742d9 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,51 @@ + + + + true + true + 0.13.5 + 16.9.0 + 15.9.20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Nerdbank.GitVersioning.sln b/Nerdbank.GitVersioning.sln similarity index 54% rename from src/Nerdbank.GitVersioning.sln rename to Nerdbank.GitVersioning.sln index 9077863c..09148d66 100644 --- a/src/Nerdbank.GitVersioning.sln +++ b/Nerdbank.GitVersioning.sln @@ -5,34 +5,50 @@ VisualStudioVersion = 17.0.31411.2 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4BD1A7CD-6F52-4F5A-825B-50E4D8C3ECFF}" ProjectSection(SolutionItems) = preProject - ..\.editorconfig = ..\.editorconfig - ..\.gitignore = ..\.gitignore - ..\3rdPartyNotices.txt = ..\3rdPartyNotices.txt - ..\azure-pipelines.yml = ..\azure-pipelines.yml - ..\build.ps1 = ..\build.ps1 + .editorconfig = .editorconfig + .gitignore = .gitignore + 3rdPartyNotices.txt = 3rdPartyNotices.txt + azure-pipelines.yml = azure-pipelines.yml + build.ps1 = build.ps1 Directory.Build.props = Directory.Build.props - ..\global.json = ..\global.json - ..\init.ps1 = ..\init.ps1 + Directory.Build.targets = Directory.Build.targets + Directory.Packages.props = Directory.Packages.props + global.json = global.json + init.ps1 = init.ps1 nuget.config = nuget.config - ..\README.md = ..\README.md - ..\version.json = ..\version.json + README.md = README.md + version.json = version.json EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NerdBank.GitVersioning.Tests", "NerdBank.GitVersioning.Tests\NerdBank.GitVersioning.Tests.csproj", "{C54F9EC8-FDA7-4D22-BCB2-7D97523BD91E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nerdbank.GitVersioning.Tests", "test\Nerdbank.GitVersioning.Tests\Nerdbank.GitVersioning.Tests.csproj", "{C54F9EC8-FDA7-4D22-BCB2-7D97523BD91E}" EndProject -Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "nb-gv", "nerdbank-gitversioning.npm\nb-gv.njsproj", "{F79DD916-27B3-4CD0-97FF-5021B3CF9934}" +Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "nb-gv", "src\nerdbank-gitversioning.npm\nb-gv.njsproj", "{F79DD916-27B3-4CD0-97FF-5021B3CF9934}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NerdBank.GitVersioning", "NerdBank.GitVersioning\NerdBank.GitVersioning.csproj", "{C7FA7B7A-0469-4B1C-8657-E274C4CD8ABB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nerdbank.GitVersioning", "src\NerdBank.GitVersioning\Nerdbank.GitVersioning.csproj", "{C7FA7B7A-0469-4B1C-8657-E274C4CD8ABB}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nerdbank.GitVersioning.Tasks", "Nerdbank.GitVersioning.Tasks\Nerdbank.GitVersioning.Tasks.csproj", "{B2454569-6EDC-4FD4-9936-D2B2F2E10409}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nerdbank.GitVersioning.Tasks", "src\Nerdbank.GitVersioning.Tasks\Nerdbank.GitVersioning.Tasks.csproj", "{B2454569-6EDC-4FD4-9936-D2B2F2E10409}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "nbgv", "nbgv\nbgv.csproj", "{EF4DAF23-6CE9-48C5-84C5-80AC80D3D07D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "nbgv", "src\nbgv\nbgv.csproj", "{EF4DAF23-6CE9-48C5-84C5-80AC80D3D07D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cake.GitVersioning", "Cake.GitVersioning\Cake.GitVersioning.csproj", "{1F267A97-DFE3-4166-83B1-9D236B7A09BD}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cake.GitVersioning", "src\Cake.GitVersioning\Cake.GitVersioning.csproj", "{1F267A97-DFE3-4166-83B1-9D236B7A09BD}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nerdbank.GitVersioning.Benchmarks", "NerdBank.GitVersioning.Benchmarks\Nerdbank.GitVersioning.Benchmarks.csproj", "{B0B7955D-E51F-4091-BF7F-55D07D381D15}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nerdbank.GitVersioning.Benchmarks", "test\Nerdbank.GitVersioning.Benchmarks\Nerdbank.GitVersioning.Benchmarks.csproj", "{B0B7955D-E51F-4091-BF7F-55D07D381D15}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cake.GitVersioning.Tests", "Cake.GitVersioning.Tests\Cake.GitVersioning.Tests.csproj", "{D68829FE-24D8-4ADD-8525-17F47C6FE257}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cake.GitVersioning.Tests", "test\Cake.GitVersioning.Tests\Cake.GitVersioning.Tests.csproj", "{D68829FE-24D8-4ADD-8525-17F47C6FE257}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0A4BEA3B-268B-464F-9C0F-6DC2EFA884F8}" + ProjectSection(SolutionItems) = preProject + src\.editorconfig = src\.editorconfig + src\Directory.Build.props = src\Directory.Build.props + src\Directory.Build.targets = src\Directory.Build.targets + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{A603FD3B-1A40-4605-B044-5DDAFDEDCD90}" + ProjectSection(SolutionItems) = preProject + test\.editorconfig = test\.editorconfig + test\Directory.Build.props = test\Directory.Build.props + test\Directory.Build.targets = test\Directory.Build.targets + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -74,6 +90,16 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {C54F9EC8-FDA7-4D22-BCB2-7D97523BD91E} = {A603FD3B-1A40-4605-B044-5DDAFDEDCD90} + {F79DD916-27B3-4CD0-97FF-5021B3CF9934} = {0A4BEA3B-268B-464F-9C0F-6DC2EFA884F8} + {C7FA7B7A-0469-4B1C-8657-E274C4CD8ABB} = {0A4BEA3B-268B-464F-9C0F-6DC2EFA884F8} + {B2454569-6EDC-4FD4-9936-D2B2F2E10409} = {0A4BEA3B-268B-464F-9C0F-6DC2EFA884F8} + {EF4DAF23-6CE9-48C5-84C5-80AC80D3D07D} = {0A4BEA3B-268B-464F-9C0F-6DC2EFA884F8} + {1F267A97-DFE3-4166-83B1-9D236B7A09BD} = {0A4BEA3B-268B-464F-9C0F-6DC2EFA884F8} + {B0B7955D-E51F-4091-BF7F-55D07D381D15} = {A603FD3B-1A40-4605-B044-5DDAFDEDCD90} + {D68829FE-24D8-4ADD-8525-17F47C6FE257} = {A603FD3B-1A40-4605-B044-5DDAFDEDCD90} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4CF7AA29-5BBD-4294-9C92-DA689CF57200} EndGlobalSection diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..b35d55b5 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,32 @@ +# Security + +The maintainers of this project take security seriously, and the reporting of potential security issues. If you believe you have found a security vulnerability in code from this repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. + +We are using Microsoft's vulnerability definition as it is public and familiar to .NET users. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the project maintainers using [keybase](https://keybase.io/aarnott) or [email](mailto:andrewarnott@live.com?subject=OSS%20project%20vulnerability). +You will typically receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + +* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) +* Full paths of source file(s) related to the manifestation of the issue +* The location of the affected source code (tag/branch/commit or direct URL) +* Any special configuration required to reproduce the issue +* Step-by-step instructions to reproduce the issue +* Proof-of-concept or exploit code (if possible) +* Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +This project follows the [GitHub CVD process](https://github.blog/2022-02-09-coordinated-vulnerability-disclosure-cvd-open-source-projects/) and will use [GHSA](https://docs.github.com/code-security/security-advisories/about-github-security-advisories) to manage discussion of the vulnerability and fixes, and create a public CVE advisory through github if applicable. diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 914908dd..8ab8348e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -2,301 +2,67 @@ trigger: batch: true branches: include: - - master - - 'v*.*' - - 'validate/*' + - main + - 'v*.*' + - 'validate/*' paths: exclude: - - doc + - doc/ - '*.md' + - .vscode/ + - .github/ - azure-pipelines/release.yml +parameters: +- name: RunTests + displayName: Run tests + type: boolean + default: true + resources: containers: - - container: xenial - image: andrewarnott/linux-buildagent - - container: bionic - image: mcr.microsoft.com/dotnet/core/sdk:3.1-bionic - container: focal - image: mcr.microsoft.com/dotnet/core/sdk:3.1-focal - - container: archlinux - image: andrewarnott/archlinux + image: mcr.microsoft.com/dotnet/sdk:6.0-focal + - container: jammy60 + image: mcr.microsoft.com/dotnet/sdk:6.0-jammy + - container: jammy70 + image: mcr.microsoft.com/dotnet/sdk:7.0-jammy + - container: debian + image: mcr.microsoft.com/dotnet/sdk:latest variables: - TreatWarningsAsErrors: true + MSBuildTreatWarningsAsErrors: true DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true BuildConfiguration: Release - BuildPlatform: Any CPU + codecov_token: 92266a45-648d-454e-8fec-beffae2e6553 + ci_feed: https://pkgs.dev.azure.com/andrewarnott/OSS/_packaging/PublicCI/nuget/v3/index.json + ci_npm_feed: https://pkgs.dev.azure.com/andrewarnott/OSS/_packaging/PublicCI/npm/registry/ + NUGET_PACKAGES: $(Agent.TempDirectory)/.nuget/packages/ stages: - stage: Build jobs: - - job: Build - strategy: - matrix: - linux: - imageName: 'ubuntu-20.04' - testModifier: -f net5.0 - windows: - imageName: 'windows-2019' - testModifier: - dotnet32: "\"C:\\Program Files (x86)\\dotnet\\dotnet.exe\"" - variables: - - ${{ if eq(variables['System.TeamFoundationCollectionUri'], 'https://dev.azure.com/andrewarnott/') }}: - - group: dotnetfoundation code signing - pool: - vmImage: $(imageName) - steps: - - checkout: self - clean: true - submodules: true # keep the warnings quiet about the wiki not being enlisted - - script: | - git config --global user.name ci - git config --global user.email me@ci.com - displayName: Configure git commit author for testing - - task: UseDotNet@2 - displayName: Install .NET Core 3.1 runtime - inputs: - packageType: runtime - version: 3.1.x - - - task: UseDotNet@2 - displayName: Install .NET 5.0 SDK - inputs: - packageType: sdk # necessary for msbuild tests to run on .NET 5 runtime - version: 5.0.x - - - task: UseDotNet@2 - displayName: Install .NET 6.0 SDK - inputs: - packageType: sdk - version: 6.0.100 - - - - pwsh: | - Invoke-WebRequest -Uri "https://dot.net/v1/dotnet-install.ps1" -OutFile dotnet-install.ps1 - & .\dotnet-install.ps1 -Architecture x86 -Channel 3.1 -InstallDir "C:\Program Files (x86)\dotnet\" -NoPath -Verbose -Runtime dotnet - & .\dotnet-install.ps1 -Architecture x86 -Channel 5.0 -InstallDir "C:\Program Files (x86)\dotnet\" -NoPath -Verbose - & .\dotnet-install.ps1 -Architecture x86 -Version 6.0.100 -InstallDir "C:\Program Files (x86)\dotnet\" -NoPath -Verbose - displayName: Install 32-bit .NET SDK and runtimes - condition: ne(variables['dotnet32'], '') - - - script: dotnet --info - displayName: Show dotnet SDK info - - - pwsh: | - dotnet tool install --tool-path . nbgv - ./nbgv cloud -a - displayName: Set build number - - - task: DotNetCoreCLI@2 - displayName: Restore NuGet packages - inputs: - command: restore - verbosityRestore: normal # detailed, normal, minimal - projects: src/**/*.sln - feedsToUse: config - nugetConfigPath: src/nuget.config - workingDirectory: src - - - script: npm i -g yarn@">=1.22 <2.0" - displayName: Installing yarn - - - script: yarn --cwd src/nerdbank-gitversioning.npm - displayName: Installing NPM packages - - - script: dotnet build -c $(BuildConfiguration) --no-restore /t:build,pack /bl:"$(Build.ArtifactStagingDirectory)/build_logs/msbuild.binlog" - displayName: Build NuGet package and tests - workingDirectory: src - - - script: dotnet pack -c $(BuildConfiguration) --no-build -p:PackLKG=true /bl:"$(Build.ArtifactStagingDirectory)/build_logs/msbuild_lkg.binlog" - displayName: Build LKG package - workingDirectory: src/Nerdbank.GitVersioning.Tasks - - - script: dotnet publish -c $(BuildConfiguration) -o ../nerdbank-gitversioning.npm/out/nbgv.cli/tools/netcoreapp3.1/any /bl:"$(Build.ArtifactStagingDirectory)/build_logs/nbgv_publish.binlog" - displayName: Publish nbgv tool - workingDirectory: src/nbgv - - - task: gulp@0 - displayName: Build nerdbank-gitversioning NPM package - inputs: - gulpfile: src/nerdbank-gitversioning.npm/gulpfile.js - - - script: > - dotnet test NerdBank.GitVersioning.Tests - --no-build $(testModifier) - -c $(BuildConfiguration) - --filter "TestCategory!=FailsOnAzurePipelines" - --logger "trx;LogFileName=$(Build.ArtifactStagingDirectory)/TestLogs/TestResults.x64.trx" - --results-directory $(Build.ArtifactStagingDirectory)/CodeCoverage/ - --collect:"XPlat Code Coverage" - -- - RunConfiguration.DisableAppDomain=true - displayName: Run x64 tests - workingDirectory: src - - - script: > - $(dotnet32) test NerdBank.GitVersioning.Tests - --no-build $(testModifier) - -c $(BuildConfiguration) - --filter "TestCategory!=FailsOnAzurePipelines" - --logger "trx;LogFileName=$(Build.ArtifactStagingDirectory)/TestLogs/TestResults.x86.trx" - --results-directory $(Build.ArtifactStagingDirectory)/CodeCoverage/ - --collect:"XPlat Code Coverage" - -- - RunConfiguration.DisableAppDomain=true - displayName: Run x86 tests - workingDirectory: src - condition: ne(variables['dotnet32'], '') - - - script: > - dotnet test Cake.GitVersioning.Tests - --no-build $(testModifier) - -c $(BuildConfiguration) - --filter "TestCategory!=FailsOnAzurePipelines" - --logger "trx;LogFileName=$(Build.ArtifactStagingDirectory)/TestLogs/TestResults.cake.trx" - --results-directory $(Build.ArtifactStagingDirectory)/CodeCoverage/ - --collect:"XPlat Code Coverage" - -- - RunConfiguration.DisableAppDomain=true - displayName: Run cake tests - workingDirectory: src - - - task: PublishCodeCoverageResults@1 - displayName: Publish code coverage results - inputs: - codeCoverageTool: 'cobertura' - summaryFileLocation: $(Build.ArtifactStagingDirectory)/CodeCoverage/**/coverage.cobertura.xml - - - task: PublishTestResults@2 - displayName: Publish test results - inputs: - testResultsFormat: VSTest - testResultsFiles: '*.trx' - searchFolder: $(Build.ArtifactStagingDirectory)/TestLogs - buildPlatform: $(BuildPlatform) - buildConfiguration: $(BuildConfiguration) - publishRunAttachments: false - condition: always() - - - task: CopyFiles@1 - inputs: - sourceFolder: $(System.DefaultWorkingDirectory)/bin - Contents: | - **\*.nupkg - !**\*.LKG* - js\*.tgz - TargetFolder: $(Build.ArtifactStagingDirectory)/deployables - flattenFolders: true - displayName: Collecting deployable artifacts - - - task: CopyFiles@1 - inputs: - sourceFolder: $(System.DefaultWorkingDirectory)/bin - Contents: | - **\*.LKG*.nupkg - TargetFolder: $(Build.ArtifactStagingDirectory)/deployables-lkg - flattenFolders: true - displayName: Collecting LKG artifacts - - - pwsh: > - dotnet tool install --tool-path obj SignClient - - obj/SignClient sign - --baseDirectory '$(Build.ArtifactStagingDirectory)/deployables' - --input '**/*' - --config '$(System.DefaultWorkingDirectory)/azure-pipelines/SignClient.json' - --filelist '$(System.DefaultWorkingDirectory)/azure-pipelines/signfiles.txt' - --user '$(codesign_username)' - --secret '$(codesign_secret)' - --name 'Nerdbank.GitVersioning' - --descriptionUrl 'https://github.com/dotnet/Nerdbank.GitVersioning' - displayName: Code sign - condition: and(succeeded(), eq(variables['System.TeamFoundationCollectionUri'], 'https://dev.azure.com/andrewarnott/'), eq(variables['Agent.OS'], 'Windows_NT'), ne(variables['Build.Reason'], 'PullRequest')) - - - pwsh: > - obj/SignClient sign - --baseDirectory '$(Build.ArtifactStagingDirectory)/deployables-lkg' - --input '**/*' - --config '$(System.DefaultWorkingDirectory)/azure-pipelines/SignClient.json' - --filelist '$(System.DefaultWorkingDirectory)/azure-pipelines/signfiles.txt' - --user '$(codesign_username)' - --secret '$(codesign_secret)' - --name 'Nerdbank.GitVersioning' - --descriptionUrl 'https://github.com/dotnet/Nerdbank.GitVersioning' - displayName: Code sign LKG - condition: and(succeeded(), eq(variables['System.TeamFoundationCollectionUri'], 'https://dev.azure.com/andrewarnott/'), eq(variables['Agent.OS'], 'Windows_NT'), ne(variables['Build.Reason'], 'PullRequest')) - - - task: PublishBuildArtifacts@1 - inputs: - PathtoPublish: $(Build.ArtifactStagingDirectory)/deployables - ArtifactName: deployables - ArtifactType: Container - displayName: Publish deployables artifacts - # Only deploy when from a single build in the build matrix - condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) - - - task: PublishBuildArtifacts@1 - inputs: - PathtoPublish: $(Build.ArtifactStagingDirectory)/deployables-lkg - ArtifactName: deployables-lkg - ArtifactType: Container - displayName: Publish deployables-lkg artifact - # Only deploy when from a single build in the build matrix - condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) - - - task: PublishBuildArtifacts@1 - inputs: - PathtoPublish: $(Build.ArtifactStagingDirectory)/build_logs - ArtifactName: build_logs - ArtifactType: Container - displayName: Publish build_logs artifacts - condition: succeededOrFailed() - - task: NuGetCommand@2 - displayName: Pushing package to PublicCI feed - inputs: - command: push - packagesToPush: $(Build.ArtifactStagingDirectory)/deployables/*.*nupkg - nuGetFeedType: internal - publishVstsFeed: OSS/PublicCI - allowPackageConflicts: true - condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['System.TeamFoundationCollectionUri'], 'https://dev.azure.com/andrewarnott/'), ne(variables['Build.Reason'], 'PullRequest')) - - - pwsh: Set-Content -Path "$(Agent.TempDirectory)/.npmrc" -Value "registry=https://pkgs.dev.azure.com/andrewarnott/OSS/_packaging/PublicCI/npm/registry/`nalways-auth=true" - displayName: Prepare to push to PublicCI - condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['System.TeamFoundationCollectionUri'], 'https://dev.azure.com/andrewarnott/'), ne(variables['Build.Reason'], 'PullRequest')) - - task: npmAuthenticate@0 - displayName: Authenticate to PublicCI - inputs: - workingFile: $(Agent.TempDirectory)/.npmrc - condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['System.TeamFoundationCollectionUri'], 'https://dev.azure.com/andrewarnott/'), ne(variables['Build.Reason'], 'PullRequest')) - - pwsh: | - $tgz = (Get-ChildItem "$(Build.ArtifactStagingDirectory)/deployables/*.tgz")[0].FullName - Write-Host "Will publish $tgz" - npm publish $tgz - workingDirectory: $(Agent.TempDirectory) - displayName: npm publish to PublicCI feed - condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['System.TeamFoundationCollectionUri'], 'https://dev.azure.com/andrewarnott/'), ne(variables['Build.Reason'], 'PullRequest')) - continueOnError: true + - template: azure-pipelines/build.yml + parameters: + RunTests: ${{ parameters.RunTests }} - stage: Test displayName: Functional testing + condition: and(succeeded(), ${{ parameters.RunTests }}) jobs: - job: linux strategy: matrix: - # xenial: - # containerImage: xenial - # configureContainerCommand: 'sudo apt update && sudo apt-get install -y git' - Ubuntu_Bionic: - containerImage: bionic Ubuntu_Focal: containerImage: focal - # Arch_Linux: - # containerImage: archlinux - # configureContainerCommand: 'sudo pacman -Sy --noconfirm git dotnet-sdk openssl-1.0' + Ubuntu_Jammy_60: + containerImage: jammy60 + Ubuntu_Jammy_70: + containerImage: jammy70 + Debian: + containerImage: debian pool: - vmImage: ubuntu-20.04 + vmImage: ubuntu-22.04 container: $[ variables['containerImage'] ] steps: - bash: $(configureContainerCommand) @@ -305,8 +71,14 @@ stages: - template: azure-pipelines/xplattest-pipeline.yml - job: macOS + strategy: + matrix: + macOS_Catalina: + vmImage: macOS-12 + macOS_Monterey: + vmImage: macOS-12 pool: - vmImage: macOS-10.15 + vmImage: $[ variables['vmImage'] ] steps: - template: azure-pipelines/xplattest-pipeline.yml @@ -318,32 +90,26 @@ stages: strategy: matrix: ubuntu: - imageName: ubuntu-18.04 + imageName: ubuntu-22.04 repoDir: '~/git' windows: - imageName: windows-2019 + imageName: windows-2022 repoDir: '${USERPROFILE}/source/repos' macOS: - imageName: macOS-10.15 + imageName: macOS-12 repoDir: '~/git' pool: vmImage: $(imageName) steps: + - checkout: self + fetchDepth: 0 # avoid shallow clone so nbgv can do its work. + clean: true + submodules: true # keep the warnings quiet about the wiki not being enlisted - task: UseDotNet@2 - displayName: Install .NET Core 3.1 runtime - inputs: - packageType: runtime - version: 3.1.x - - task: UseDotNet@2 - displayName: Install .NET 5.0 runtime - inputs: - packageType: runtime - version: 5.0.x - - task: UseDotNet@2 - displayName: Install .NET 6.0.100 SDK + displayName: Install .NET 7.0.203 SDK inputs: packageType: sdk - version: 6.0.100 + version: 7.0.203 - script: dotnet --info displayName: Show dotnet SDK info - bash: | @@ -351,15 +117,14 @@ stages: git clone https://github.com/xunit/xunit $(repoDir)/xunit git clone https://github.com/gimlichael/Cuemon $(repoDir)/Cuemon git clone https://github.com/kerryjiang/SuperSocket $(repoDir)/SuperSocket - git clone https://github.com/dotnet/NerdBank.GitVersioning $(repoDir)/NerdBank.GitVersioning + git clone https://github.com/dotnet/Nerdbank.GitVersioning $(repoDir)/Nerdbank.GitVersioning displayName: Clone test repositories - script: | dotnet build -c Release - workingDirectory: src/ displayName: Build in Release mode - script: | - dotnet run -c Release -f netcoreapp3.1 -- --filter GetVersionBenchmarks --artifacts $(Build.ArtifactStagingDirectory)/benchmarks/packed/$(imageName) - workingDirectory: src/NerdBank.GitVersioning.Benchmarks + dotnet run -c Release -f net7.0 -- --filter *GetVersionBenchmarks* --artifacts $(Build.ArtifactStagingDirectory)/benchmarks/packed/$(imageName) + workingDirectory: test/Nerdbank.GitVersioning.Benchmarks displayName: Run benchmarks (packed) - bash: | cd $(repoDir)/xunit @@ -371,12 +136,12 @@ stages: cd $(repoDir)/SuperSocket git unpack-objects < .git/objects/pack/*.pack - cd $(repoDir)/NerdBank.GitVersioning + cd $(repoDir)/Nerdbank.GitVersioning git unpack-objects < .git/objects/pack/*.pack displayName: Unpack Git repositories - script: | - dotnet run -c Release -f netcoreapp3.1 -- --filter GetVersionBenchmarks --artifacts $(Build.ArtifactStagingDirectory)/benchmarks/unpacked/$(imageName) - workingDirectory: src/NerdBank.GitVersioning.Benchmarks + dotnet run -c Release -f net7.0 -- --filter '*GetVersionBenchmarks*' --artifacts $(Build.ArtifactStagingDirectory)/benchmarks/unpacked/$(imageName) + workingDirectory: test/Nerdbank.GitVersioning.Benchmarks displayName: Run benchmarks (unpacked) - task: PublishBuildArtifacts@1 inputs: diff --git a/azure-pipelines/Get-ArtifactsStagingDirectory.ps1 b/azure-pipelines/Get-ArtifactsStagingDirectory.ps1 new file mode 100644 index 00000000..391e5713 --- /dev/null +++ b/azure-pipelines/Get-ArtifactsStagingDirectory.ps1 @@ -0,0 +1,15 @@ +Param( + [switch]$CleanIfLocal +) +if ($env:BUILD_ARTIFACTSTAGINGDIRECTORY) { + $ArtifactStagingFolder = $env:BUILD_ARTIFACTSTAGINGDIRECTORY +} elseif ($env:RUNNER_TEMP) { + $ArtifactStagingFolder = "$env:RUNNER_TEMP\_artifacts" +} else { + $ArtifactStagingFolder = [System.IO.Path]::GetFullPath("$PSScriptRoot/../obj/_artifacts") + if ($CleanIfLocal -and (Test-Path $ArtifactStagingFolder)) { + Remove-Item $ArtifactStagingFolder -Recurse -Force + } +} + +$ArtifactStagingFolder diff --git a/azure-pipelines/Get-CodeCovTool.ps1 b/azure-pipelines/Get-CodeCovTool.ps1 new file mode 100644 index 00000000..ca580b4d --- /dev/null +++ b/azure-pipelines/Get-CodeCovTool.ps1 @@ -0,0 +1,86 @@ +<# +.SYNOPSIS + Downloads the CodeCov.io uploader tool and returns the path to it. +.PARAMETER AllowSkipVerify + Allows skipping signature verification of the downloaded tool if gpg is not installed. +#> +[CmdletBinding()] +Param( + [switch]$AllowSkipVerify +) + +if ($IsMacOS) { + $codeCovUrl = "https://uploader.codecov.io/latest/macos/codecov" + $toolName = 'codecov' +} +elseif ($IsLinux) { + $codeCovUrl = "https://uploader.codecov.io/latest/linux/codecov" + $toolName = 'codecov' +} +else { + $codeCovUrl = "https://uploader.codecov.io/latest/windows/codecov.exe" + $toolName = 'codecov.exe' +} + +$shaSuffix = ".SHA256SUM" +$sigSuffix = $shaSuffix + ".sig" + +Function Get-FileFromWeb([Uri]$Uri, $OutDir) { + $OutFile = Join-Path $OutDir $Uri.Segments[-1] + if (!(Test-Path $OutFile)) { + Write-Verbose "Downloading $Uri..." + if (!(Test-Path $OutDir)) { New-Item -ItemType Directory -Path $OutDir | Out-Null } + try { + (New-Object System.Net.WebClient).DownloadFile($Uri, $OutFile) + } finally { + # This try/finally causes the script to abort + } + } + + $OutFile +} + +$toolsPath = & "$PSScriptRoot\Get-TempToolsPath.ps1" +$binaryToolsPath = Join-Path $toolsPath codecov +$testingPath = Join-Path $binaryToolsPath unverified +$finalToolPath = Join-Path $binaryToolsPath $toolName + +if (!(Test-Path $finalToolPath)) { + if (Test-Path $testingPath) { + Remove-Item -Recurse -Force $testingPath # ensure we download all matching files + } + $tool = Get-FileFromWeb $codeCovUrl $testingPath + $sha = Get-FileFromWeb "$codeCovUrl$shaSuffix" $testingPath + $sig = Get-FileFromWeb "$codeCovUrl$sigSuffix" $testingPath + $key = Get-FileFromWeb https://keybase.io/codecovsecurity/pgp_keys.asc $testingPath + + if ((Get-Command gpg -ErrorAction SilentlyContinue)) { + Write-Host "Importing codecov key" -ForegroundColor Yellow + gpg --import $key + Write-Host "Verifying signature on codecov hash" -ForegroundColor Yellow + gpg --verify $sig $sha + } else { + if ($AllowSkipVerify) { + Write-Warning "gpg not found. Unable to verify hash signature." + } else { + throw "gpg not found. Unable to verify hash signature. Install gpg or add -AllowSkipVerify to override." + } + } + + Write-Host "Verifying hash on downloaded tool" -ForegroundColor Yellow + $actualHash = (Get-FileHash -Path $tool -Algorithm SHA256).Hash + $expectedHash = (Get-Content $sha).Split()[0] + if ($actualHash -ne $expectedHash) { + # Validation failed. Delete the tool so we can't execute it. + #Remove-Item $codeCovPath + throw "codecov uploader tool failed signature validation." + } + + Copy-Item $tool $finalToolPath + + if ($IsMacOS -or $IsLinux) { + chmod u+x $finalToolPath + } +} + +return $finalToolPath diff --git a/azure-pipelines/Get-NuGetTool.ps1 b/azure-pipelines/Get-NuGetTool.ps1 new file mode 100644 index 00000000..3097c873 --- /dev/null +++ b/azure-pipelines/Get-NuGetTool.ps1 @@ -0,0 +1,22 @@ +<# +.SYNOPSIS + Downloads the NuGet.exe tool and returns the path to it. +.PARAMETER NuGetVersion + The version of the NuGet tool to acquire. +#> +Param( + [Parameter()] + [string]$NuGetVersion='6.4.0' +) + +$toolsPath = & "$PSScriptRoot\Get-TempToolsPath.ps1" +$binaryToolsPath = Join-Path $toolsPath $NuGetVersion +if (!(Test-Path $binaryToolsPath)) { $null = mkdir $binaryToolsPath } +$nugetPath = Join-Path $binaryToolsPath nuget.exe + +if (!(Test-Path $nugetPath)) { + Write-Host "Downloading nuget.exe $NuGetVersion..." -ForegroundColor Yellow + (New-Object System.Net.WebClient).DownloadFile("https://dist.nuget.org/win-x86-commandline/v$NuGetVersion/NuGet.exe", $nugetPath) +} + +return (Resolve-Path $nugetPath).Path diff --git a/azure-pipelines/Get-ProcDump.ps1 b/azure-pipelines/Get-ProcDump.ps1 new file mode 100644 index 00000000..1493fe4b --- /dev/null +++ b/azure-pipelines/Get-ProcDump.ps1 @@ -0,0 +1,14 @@ +<# +.SYNOPSIS +Downloads 32-bit and 64-bit procdump executables and returns the path to where they were installed. +#> +$version = '0.0.1' +$baseDir = "$PSScriptRoot\..\obj\tools" +$procDumpToolPath = "$baseDir\procdump.$version\bin" +if (-not (Test-Path $procDumpToolPath)) { + if (-not (Test-Path $baseDir)) { New-Item -Type Directory -Path $baseDir | Out-Null } + $baseDir = (Resolve-Path $baseDir).Path # Normalize it + & (& $PSScriptRoot\Get-NuGetTool.ps1) install procdump -version $version -PackageSaveMode nuspec -OutputDirectory $baseDir -Source https://api.nuget.org/v3/index.json | Out-Null +} + +(Resolve-Path $procDumpToolPath).Path diff --git a/azure-pipelines/Get-SymbolFiles.ps1 b/azure-pipelines/Get-SymbolFiles.ps1 new file mode 100644 index 00000000..0ce229fc --- /dev/null +++ b/azure-pipelines/Get-SymbolFiles.ps1 @@ -0,0 +1,61 @@ +<# +.SYNOPSIS + Collect the list of PDBs built in this repo. +.PARAMETER Path + The directory to recursively search for PDBs. +.PARAMETER Tests + A switch indicating to find PDBs only for test binaries instead of only for shipping shipping binaries. +#> +[CmdletBinding()] +param ( + [parameter(Mandatory=$true)] + [string]$Path, + [switch]$Tests +) + +$ActivityName = "Collecting symbols from $Path" +Write-Progress -Activity $ActivityName -CurrentOperation "Discovery PDB files" +$PDBs = Get-ChildItem -rec "$Path/*.pdb" + +# Filter PDBs to product OR test related. +$testregex = "unittest|tests|\.test\." + +Write-Progress -Activity $ActivityName -CurrentOperation "De-duplicating symbols" +$PDBsByHash = @{} +$i = 0 +$PDBs |% { + Write-Progress -Activity $ActivityName -CurrentOperation "De-duplicating symbols" -PercentComplete (100 * $i / $PDBs.Length) + $hash = Get-FileHash $_ + $i++ + Add-Member -InputObject $_ -MemberType NoteProperty -Name Hash -Value $hash.Hash + Write-Output $_ +} | Sort-Object CreationTime |% { + # De-dupe based on hash. Prefer the first match so we take the first built copy. + if (-not $PDBsByHash.ContainsKey($_.Hash)) { + $PDBsByHash.Add($_.Hash, $_.FullName) + Write-Output $_ + } +} |? { + if ($Tests) { + $_.FullName -match $testregex + } else { + $_.FullName -notmatch $testregex + } +} |% { + # Collect the DLLs/EXEs as well. + $dllPath = "$($_.Directory)/$($_.BaseName).dll" + $exePath = "$($_.Directory)/$($_.BaseName).exe" + if (Test-Path $dllPath) { + $BinaryImagePath = $dllPath + } elseif (Test-Path $exePath) { + $BinaryImagePath = $exePath + } else { + Write-Warning "`"$_`" found with no matching binary file." + $BinaryImagePath = $null + } + + if ($BinaryImagePath) { + Write-Output $BinaryImagePath + Write-Output $_.FullName + } +} diff --git a/azure-pipelines/Get-TempToolsPath.ps1 b/azure-pipelines/Get-TempToolsPath.ps1 new file mode 100644 index 00000000..bb3da8e3 --- /dev/null +++ b/azure-pipelines/Get-TempToolsPath.ps1 @@ -0,0 +1,13 @@ +if ($env:AGENT_TEMPDIRECTORY) { + $path = "$env:AGENT_TEMPDIRECTORY\$env:BUILD_BUILDID" +} elseif ($env:localappdata) { + $path = "$env:localappdata\gitrepos\tools" +} else { + $path = "$PSScriptRoot\..\obj\tools" +} + +if (!(Test-Path $path)) { + New-Item -ItemType Directory -Path $Path | Out-Null +} + +(Resolve-Path $path).Path diff --git a/azure-pipelines/Merge-CodeCoverage.ps1 b/azure-pipelines/Merge-CodeCoverage.ps1 new file mode 100644 index 00000000..5ecabbc9 --- /dev/null +++ b/azure-pipelines/Merge-CodeCoverage.ps1 @@ -0,0 +1,51 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Merges code coverage reports. +.PARAMETER Path + The path(s) to search for Cobertura code coverage reports. +.PARAMETER Format + The format for the merged result. The default is Cobertura +.PARAMETER OutputDir + The directory the merged result will be written to. The default is `coveragereport` in the root of this repo. +#> +[CmdletBinding()] +Param( + [Parameter(Mandatory=$true)] + [string[]]$Path, + [ValidateSet('Badges', 'Clover', 'Cobertura', 'CsvSummary', 'Html', 'Html_Dark', 'Html_Light', 'HtmlChart', 'HtmlInline', 'HtmlInline_AzurePipelines', 'HtmlInline_AzurePipelines_Dark', 'HtmlInline_AzurePipelines_Light', 'HtmlSummary', 'JsonSummary', 'Latex', 'LatexSummary', 'lcov', 'MarkdownSummary', 'MHtml', 'PngChart', 'SonarQube', 'TeamCitySummary', 'TextSummary', 'Xml', 'XmlSummary')] + [string]$Format='Cobertura', + [string]$OutputFile=("$PSScriptRoot/../coveragereport/merged.cobertura.xml") +) + +$RepoRoot = [string](Resolve-Path $PSScriptRoot/..) +Push-Location $RepoRoot +try { + Write-Verbose "Searching $Path for *.cobertura.xml files" + $reports = Get-ChildItem -Recurse $Path -Filter *.cobertura.xml + + if ($reports) { + $reports |% { $_.FullName } |% { + # In addition to replacing {reporoot}, we also normalize on one kind of slash so that the report aggregates data for a file whether data was collected on Windows or not. + $xml = [xml](Get-Content -Path $_) + $xml.coverage.packages.package.classes.class |? { $_.filename} |% { + $_.filename = $_.filename.Replace('{reporoot}', $RepoRoot).Replace([IO.Path]::AltDirectorySeparatorChar, [IO.Path]::DirectorySeparatorChar) + } + + $xml.Save($_) + } + + $Inputs = $reports |% { Resolve-Path -relative $_.FullName } + + if ((Split-Path $OutputFile) -and -not (Test-Path (Split-Path $OutputFile))) { + New-Item -Type Directory -Path (Split-Path $OutputFile) | Out-Null + } + + & dotnet tool run dotnet-coverage merge $Inputs -o $OutputFile -f cobertura + } else { + Write-Error "No reports found to merge." + } +} finally { + Pop-Location +} diff --git a/azure-pipelines/artifacts/Variables.ps1 b/azure-pipelines/artifacts/Variables.ps1 new file mode 100644 index 00000000..4bc6d216 --- /dev/null +++ b/azure-pipelines/artifacts/Variables.ps1 @@ -0,0 +1,43 @@ +# This artifact captures all variables defined in the ..\variables folder. +# It "snaps" the values of these variables where we can compute them during the build, +# and otherwise captures the scripts to run later during an Azure Pipelines environment release. + +$RepoRoot = [System.IO.Path]::GetFullPath("$PSScriptRoot/../..") +$ArtifactBasePath = "$RepoRoot/obj/_artifacts" +$VariablesArtifactPath = Join-Path $ArtifactBasePath variables +if (-not (Test-Path $VariablesArtifactPath)) { New-Item -ItemType Directory -Path $VariablesArtifactPath | Out-Null } + +# Copy variables, either by value if the value is calculable now, or by script +Get-ChildItem "$PSScriptRoot/../variables" |% { + $value = $null + if (-not $_.BaseName.StartsWith('_')) { # Skip trying to interpret special scripts + # First check the environment variables in case the variable was set in a queued build + # Always use all caps for env var access because Azure Pipelines converts variables to upper-case for env vars, + # and on non-Windows env vars are case sensitive. + $envVarName = $_.BaseName.ToUpper() + if (Test-Path env:$envVarName) { + $value = Get-Content "env:$envVarName" + } + + # If that didn't give us anything, try executing the script right now from its original position + if (-not $value) { + $value = & $_.FullName + } + + if ($value) { + # We got something, so wrap it with quotes so it's treated like a literal value. + $value = "'$value'" + } + } + + # If that didn't get us anything, just copy the script itself + if (-not $value) { + $value = Get-Content -Path $_.FullName + } + + Set-Content -Path "$VariablesArtifactPath/$($_.Name)" -Value $value +} + +@{ + "$VariablesArtifactPath" = (Get-ChildItem $VariablesArtifactPath -Recurse); +} diff --git a/azure-pipelines/artifacts/_all.ps1 b/azure-pipelines/artifacts/_all.ps1 new file mode 100644 index 00000000..9a22a1d0 --- /dev/null +++ b/azure-pipelines/artifacts/_all.ps1 @@ -0,0 +1,72 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + This script returns all the artifacts that should be collected after a build. + Each powershell artifact is expressed as an object with these properties: + Source - the full path to the source file + ArtifactName - the name of the artifact to upload to + ContainerFolder - the relative path within the artifact in which the file should appear + Each artifact aggregating .ps1 script should return a hashtable: + Key = path to the directory from which relative paths within the artifact should be calculated + Value = an array of paths (absolute or relative to the BaseDirectory) to files to include in the artifact. + FileInfo objects are also allowed. +.PARAMETER Force + Executes artifact scripts even if they have already been staged. +#> + +[CmdletBinding(SupportsShouldProcess = $true)] +param ( + [string]$ArtifactNameSuffix, + [switch]$Force +) + +Function EnsureTrailingSlash($path) { + if ($path.length -gt 0 -and !$path.EndsWith('\') -and !$path.EndsWith('/')) { + $path = $path + [IO.Path]::DirectorySeparatorChar + } + + $path.Replace('\', [IO.Path]::DirectorySeparatorChar) +} + +Function Test-ArtifactStaged($artifactName) { + $varName = "ARTIFACTSTAGED_$($artifactName.ToUpper())" + Test-Path "env:$varName" +} + +Get-ChildItem "$PSScriptRoot\*.ps1" -Exclude "_*" -Recurse | % { + $ArtifactName = $_.BaseName + if ($Force -or !(Test-ArtifactStaged($ArtifactName + $ArtifactNameSuffix))) { + $totalFileCount = 0 + Write-Verbose "Collecting file list for artifact $($_.BaseName)" + $fileGroups = & $_ + if ($fileGroups) { + $fileGroups.GetEnumerator() | % { + $BaseDirectory = New-Object Uri ((EnsureTrailingSlash $_.Key.ToString()), [UriKind]::Absolute) + $_.Value | ? { $_ } | % { + if ($_.GetType() -eq [IO.FileInfo] -or $_.GetType() -eq [IO.DirectoryInfo]) { + $_ = $_.FullName + } + + $artifact = New-Object -TypeName PSObject + Add-Member -InputObject $artifact -MemberType NoteProperty -Name ArtifactName -Value $ArtifactName + + $SourceFullPath = New-Object Uri ($BaseDirectory, $_) + Add-Member -InputObject $artifact -MemberType NoteProperty -Name Source -Value $SourceFullPath.LocalPath + + $RelativePath = [Uri]::UnescapeDataString($BaseDirectory.MakeRelative($SourceFullPath)) + Add-Member -InputObject $artifact -MemberType NoteProperty -Name ContainerFolder -Value (Split-Path $RelativePath) + + Write-Output $artifact + $totalFileCount += 1 + } + } + } + + if ($totalFileCount -eq 0) { + Write-Warning "No files found for the `"$ArtifactName`" artifact." + } + } else { + Write-Host "Skipping $ArtifactName because it has already been staged." -ForegroundColor DarkGray + } +} diff --git a/azure-pipelines/artifacts/_pipelines.ps1 b/azure-pipelines/artifacts/_pipelines.ps1 new file mode 100644 index 00000000..2d3338b2 --- /dev/null +++ b/azure-pipelines/artifacts/_pipelines.ps1 @@ -0,0 +1,44 @@ +<# +.SYNOPSIS + This script translates all the artifacts described by _all.ps1 + into commands that instruct Azure Pipelines to actually collect those artifacts. +#> + +[CmdletBinding()] +param ( + [string]$ArtifactNameSuffix, + [switch]$StageOnly +) + +Function Set-PipelineVariable($name, $value) { + if ((Test-Path "Env:\$name") -and (Get-Item "Env:\$name").Value -eq $value) { + return # already set + } + + #New-Item -Path "Env:\$name".ToUpper() -Value $value -Force | Out-Null + Write-Host "##vso[task.setvariable variable=$name]$value" +} + +Function Test-ArtifactUploaded($artifactName) { + $varName = "ARTIFACTUPLOADED_$($artifactName.ToUpper())" + Test-Path "env:$varName" +} + +& "$PSScriptRoot/_stage_all.ps1" -ArtifactNameSuffix $ArtifactNameSuffix |% { + # Set a variable which will out-live this script so that a subsequent attempt to collect and upload artifacts + # will skip this one from a check in the _all.ps1 script. + Set-PipelineVariable "ARTIFACTSTAGED_$($_.Name.ToUpper())" 'true' + Write-Host "Staged artifact $($_.Name) to $($_.Path)" + + if (!$StageOnly) { + if (Test-ArtifactUploaded $_.Name) { + Write-Host "Skipping $($_.Name) because it has already been uploaded." -ForegroundColor DarkGray + } else { + Write-Host "##vso[artifact.upload containerfolder=$($_.Name);artifactname=$($_.Name);]$($_.Path)" + + # Set a variable which will out-live this script so that a subsequent attempt to collect and upload artifacts + # will skip this one from a check in the _all.ps1 script. + Set-PipelineVariable "ARTIFACTUPLOADED_$($_.Name.ToUpper())" 'true' + } + } +} diff --git a/azure-pipelines/artifacts/_stage_all.ps1 b/azure-pipelines/artifacts/_stage_all.ps1 new file mode 100644 index 00000000..d81d16d4 --- /dev/null +++ b/azure-pipelines/artifacts/_stage_all.ps1 @@ -0,0 +1,60 @@ +<# +.SYNOPSIS + This script links all the artifacts described by _all.ps1 + into a staging directory, reading for uploading to a cloud build artifact store. + It returns a sequence of objects with Name and Path properties. +#> + +[CmdletBinding()] +param ( + [string]$ArtifactNameSuffix +) + +$ArtifactStagingFolder = & "$PSScriptRoot/../Get-ArtifactsStagingDirectory.ps1" -CleanIfLocal + +function Create-SymbolicLink { + param ( + $Link, + $Target + ) + + if ($Link -eq $Target) { + return + } + + if (Test-Path $Link) { Remove-Item $Link } + $LinkContainer = Split-Path $Link -Parent + if (!(Test-Path $LinkContainer)) { mkdir $LinkContainer } + if ($IsMacOS -or $IsLinux) { + ln $Target $Link | Out-Null + } else { + cmd /c "mklink `"$Link`" `"$Target`"" | Out-Null + } +} + +# Stage all artifacts +$Artifacts = & "$PSScriptRoot\_all.ps1" -ArtifactNameSuffix $ArtifactNameSuffix +$Artifacts |% { + $DestinationFolder = [System.IO.Path]::GetFullPath("$ArtifactStagingFolder/$($_.ArtifactName)$ArtifactNameSuffix/$($_.ContainerFolder)").TrimEnd('\') + $Name = "$(Split-Path $_.Source -Leaf)" + + #Write-Host "$($_.Source) -> $($_.ArtifactName)\$($_.ContainerFolder)" -ForegroundColor Yellow + + if (-not (Test-Path $DestinationFolder)) { New-Item -ItemType Directory -Path $DestinationFolder | Out-Null } + if (Test-Path -PathType Leaf $_.Source) { # skip folders + Create-SymbolicLink -Link (Join-Path $DestinationFolder $Name) -Target $_.Source + } +} + +$ArtifactNames = $Artifacts |% { "$($_.ArtifactName)$ArtifactNameSuffix" } +$ArtifactNames += Get-ChildItem env:ARTIFACTSTAGED_* |% { + # Return from ALLCAPS to the actual capitalization used for the artifact. + $artifactNameAllCaps = "$($_.Name.Substring('ARTIFACTSTAGED_'.Length))" + (Get-ChildItem $ArtifactStagingFolder\$artifactNameAllCaps* -Filter $artifactNameAllCaps).Name +} +$ArtifactNames | Get-Unique |% { + $artifact = New-Object -TypeName PSObject + Add-Member -InputObject $artifact -MemberType NoteProperty -Name Name -Value $_ + Add-Member -InputObject $artifact -MemberType NoteProperty -Name Path -Value (Join-Path $ArtifactStagingFolder $_) + Write-Output $artifact +} diff --git a/azure-pipelines/artifacts/build_logs.ps1 b/azure-pipelines/artifacts/build_logs.ps1 new file mode 100644 index 00000000..f05358e0 --- /dev/null +++ b/azure-pipelines/artifacts/build_logs.ps1 @@ -0,0 +1,7 @@ +$ArtifactStagingFolder = & "$PSScriptRoot/../Get-ArtifactsStagingDirectory.ps1" + +if (!(Test-Path $ArtifactStagingFolder/build_logs)) { return } + +@{ + "$ArtifactStagingFolder/build_logs" = (Get-ChildItem -Recurse "$ArtifactStagingFolder/build_logs") +} diff --git a/azure-pipelines/artifacts/coverageResults.ps1 b/azure-pipelines/artifacts/coverageResults.ps1 new file mode 100644 index 00000000..280ff9ae --- /dev/null +++ b/azure-pipelines/artifacts/coverageResults.ps1 @@ -0,0 +1,23 @@ +$RepoRoot = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..") + +$coverageFiles = @(Get-ChildItem "$RepoRoot/test/*.cobertura.xml" -Recurse | Where {$_.FullName -notlike "*/In/*" -and $_.FullName -notlike "*\In\*" }) + +# Prepare code coverage reports for merging on another machine +if ($env:SYSTEM_DEFAULTWORKINGDIRECTORY) { + Write-Host "Substituting $env:SYSTEM_DEFAULTWORKINGDIRECTORY with `"{reporoot}`"" + $coverageFiles |% { + $content = Get-Content -Path $_ |% { $_ -Replace [regex]::Escape($env:SYSTEM_DEFAULTWORKINGDIRECTORY), "{reporoot}" } + Set-Content -Path $_ -Value $content -Encoding UTF8 + } +} else { + Write-Warning "coverageResults: Azure Pipelines not detected. Machine-neutral token replacement skipped." +} + +if (!((Test-Path $RepoRoot\bin) -and (Test-Path $RepoRoot\obj))) { return } + +@{ + $RepoRoot = ( + $coverageFiles + + (Get-ChildItem "$RepoRoot\obj\*.cs" -Recurse) + ); +} diff --git a/azure-pipelines/artifacts/deployables-LKG.ps1 b/azure-pipelines/artifacts/deployables-LKG.ps1 new file mode 100644 index 00000000..8d47a2a6 --- /dev/null +++ b/azure-pipelines/artifacts/deployables-LKG.ps1 @@ -0,0 +1,13 @@ +$RepoRoot = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..") +$BuildConfiguration = $env:BUILDCONFIGURATION +if (!$BuildConfiguration) { + $BuildConfiguration = 'Debug' +} + +$PackagesRoot = "$RepoRoot/bin/Packages/$BuildConfiguration" + +if (!(Test-Path $PackagesRoot)) { return } + +@{ + "$PackagesRoot" = (Get-ChildItem $PackagesRoot *.LKG.*.nupkg) +} diff --git a/azure-pipelines/artifacts/deployables.ps1 b/azure-pipelines/artifacts/deployables.ps1 new file mode 100644 index 00000000..315a2532 --- /dev/null +++ b/azure-pipelines/artifacts/deployables.ps1 @@ -0,0 +1,15 @@ +$RepoRoot = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..") +$BuildConfiguration = $env:BUILDCONFIGURATION +if (!$BuildConfiguration) { + $BuildConfiguration = 'Debug' +} + +$PackagesRoot = "$RepoRoot/bin/Packages/$BuildConfiguration" +$JsRoot = "$RepoRoot/bin/js" + +if (!(Test-Path $PackagesRoot)) { return } + +@{ + "$PackagesRoot" = (Get-ChildItem $PackagesRoot -Recurse -Exclude *.LKG.*.nupkg); + "$JsRoot" = (Get-ChildItem $JsRoot *.tgz); +} diff --git a/azure-pipelines/artifacts/projectAssetsJson.ps1 b/azure-pipelines/artifacts/projectAssetsJson.ps1 new file mode 100644 index 00000000..d2e85ffb --- /dev/null +++ b/azure-pipelines/artifacts/projectAssetsJson.ps1 @@ -0,0 +1,9 @@ +$ObjRoot = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..\obj") + +if (!(Test-Path $ObjRoot)) { return } + +@{ + "$ObjRoot" = ( + (Get-ChildItem "$ObjRoot\project.assets.json" -Recurse) + ); +} diff --git a/azure-pipelines/artifacts/symbols.ps1 b/azure-pipelines/artifacts/symbols.ps1 new file mode 100644 index 00000000..9e2c7bd5 --- /dev/null +++ b/azure-pipelines/artifacts/symbols.ps1 @@ -0,0 +1,7 @@ +$BinPath = [System.IO.Path]::GetFullPath("$PSScriptRoot/../../bin") +if (!(Test-Path $BinPath)) { return } +$symbolfiles = & "$PSScriptRoot/../Get-SymbolFiles.ps1" -Path $BinPath | Get-Unique + +@{ + "$BinPath" = $SymbolFiles; +} diff --git a/azure-pipelines/artifacts/testResults.ps1 b/azure-pipelines/artifacts/testResults.ps1 new file mode 100644 index 00000000..301a4376 --- /dev/null +++ b/azure-pipelines/artifacts/testResults.ps1 @@ -0,0 +1,15 @@ +[CmdletBinding()] +Param( +) + +$result = @{} + +$testRoot = Resolve-Path "$PSScriptRoot\..\..\test" +$result[$testRoot] = (Get-ChildItem "$testRoot\TestResults" -Recurse -Directory | Get-ChildItem -Recurse -File) + +$testlogsPath = "$env:BUILD_ARTIFACTSTAGINGDIRECTORY\test_logs" +if (Test-Path $testlogsPath) { + $result[$testlogsPath] = Get-ChildItem "$testlogsPath\*"; +} + +$result diff --git a/azure-pipelines/artifacts/test_symbols.ps1 b/azure-pipelines/artifacts/test_symbols.ps1 new file mode 100644 index 00000000..ce2b6481 --- /dev/null +++ b/azure-pipelines/artifacts/test_symbols.ps1 @@ -0,0 +1,7 @@ +$BinPath = [System.IO.Path]::GetFullPath("$PSScriptRoot/../../bin") +if (!(Test-Path $BinPath)) { return } +$symbolfiles = & "$PSScriptRoot/../Get-SymbolFiles.ps1" -Path $BinPath -Tests | Get-Unique + +@{ + "$BinPath" = $SymbolFiles; +} diff --git a/azure-pipelines/build.yml b/azure-pipelines/build.yml new file mode 100644 index 00000000..021256ae --- /dev/null +++ b/azure-pipelines/build.yml @@ -0,0 +1,63 @@ +parameters: +- name: windowsPool + type: object + default: + vmImage: windows-2022 +- name: RunTests + type: boolean + default: true + +jobs: +- job: Windows + pool: ${{ parameters.windowsPool }} + variables: + - name: testModifier + value: + - ${{ if eq(variables['System.TeamFoundationCollectionUri'], 'https://dev.azure.com/andrewarnott/') }}: + - group: dotnetfoundation code signing + steps: + - checkout: self + fetchDepth: 0 # avoid shallow clone so nbgv can do its work. + clean: true + submodules: true # keep the warnings quiet about the wiki not being enlisted + - template: install-dependencies.yml + - pwsh: | + Invoke-WebRequest -Uri "https://dot.net/v1/dotnet-install.ps1" -OutFile dotnet-install.ps1 + & .\dotnet-install.ps1 -Architecture x86 -Version 7.0.203 -InstallDir "C:\Program Files (x86)\dotnet\" -NoPath -Verbose + displayName: ⚙ Install 32-bit .NET SDK and runtimes + + - template: dotnet.yml + parameters: + RunTests: ${{ parameters.RunTests }} + +- job: Linux + pool: + vmImage: Ubuntu 20.04 + steps: + - checkout: self + fetchDepth: 0 # avoid shallow clone so nbgv can do its work. + clean: true + submodules: true # keep the warnings quiet about the wiki not being enlisted + - template: install-dependencies.yml + - powershell: dotnet tool run nbgv cloud -c + displayName: ⚙ Set build number + - template: dotnet.yml + parameters: + RunTests: ${{ parameters.RunTests }} + +- job: WrapUp + dependsOn: + - Windows + - Linux + pool: ${{ parameters.windowsPool }} # Use Windows agent because PublishSymbols task requires it (https://github.com/microsoft/azure-pipelines-tasks/issues/13821). + condition: succeededOrFailed() + steps: + - checkout: self + fetchDepth: 0 # avoid shallow clone so nbgv can do its work. + clean: true + - template: install-dependencies.yml + parameters: + initArgs: -NoRestore + - ${{ if parameters.RunTests }}: + - template: publish-codecoverage.yml + - template: publish-deployables.yml diff --git a/azure-pipelines/dotnet-test-cloud.ps1 b/azure-pipelines/dotnet-test-cloud.ps1 new file mode 100644 index 00000000..24bf812a --- /dev/null +++ b/azure-pipelines/dotnet-test-cloud.ps1 @@ -0,0 +1,83 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Runs tests as they are run in cloud test runs. +.PARAMETER Configuration + The configuration within which to run tests +.PARAMETER Agent + The name of the agent. This is used in preparing test run titles. +.PARAMETER PublishResults + A switch to publish results to Azure Pipelines. +.PARAMETER x86 + A switch to run the tests in an x86 process. +.PARAMETER dotnet32 + The path to a 32-bit dotnet executable to use. +#> +[CmdletBinding()] +Param( + [string]$Configuration='Debug', + [string]$Agent='Local', + [switch]$PublishResults, + [switch]$x86, + [string]$dotnet32 +) + +$RepoRoot = (Resolve-Path "$PSScriptRoot/..").Path +$ArtifactStagingFolder = & "$PSScriptRoot/Get-ArtifactsStagingDirectory.ps1" + +$dotnet = 'dotnet' +if ($x86) { + $x86RunTitleSuffix = ", x86" + if ($dotnet32) { + $dotnet = $dotnet32 + } else { + $dotnet32Possibilities = "$PSScriptRoot\../obj/tools/x86/.dotnet/dotnet.exe", "$env:AGENT_TOOLSDIRECTORY/x86/dotnet/dotnet.exe", "${env:ProgramFiles(x86)}\dotnet\dotnet.exe" + $dotnet32Matches = $dotnet32Possibilities |? { Test-Path $_ } + if ($dotnet32Matches) { + $dotnet = Resolve-Path @($dotnet32Matches)[0] + Write-Host "Running tests using `"$dotnet`"" -ForegroundColor DarkGray + } else { + Write-Error "Unable to find 32-bit dotnet.exe" + return 1 + } + } +} + +& $dotnet test $RepoRoot ` + --no-build ` + -c $Configuration ` + --filter "TestCategory!=FailsInCloudTest" ` + --collect "Code Coverage;Format=cobertura" ` + --settings "$PSScriptRoot/test.runsettings" ` + --blame-hang-timeout 60s ` + --blame-crash ` + -bl:"$ArtifactStagingFolder/build_logs/test.binlog" ` + --diag "$ArtifactStagingFolder/test_logs/diag.log;TraceLevel=info" ` + --logger trx ` + +$unknownCounter = 0 +Get-ChildItem -Recurse -Path $RepoRoot\test\*.trx |% { + Copy-Item $_ -Destination $ArtifactStagingFolder/test_logs/ + + if ($PublishResults) { + $x = [xml](Get-Content -Path $_) + $runTitle = $null + if ($x.TestRun.TestDefinitions -and $x.TestRun.TestDefinitions.GetElementsByTagName('UnitTest')) { + $storage = $x.TestRun.TestDefinitions.GetElementsByTagName('UnitTest')[0].storage -replace '\\','/' + if ($storage -match '/(?net[^/]+)/(?:(?[^/]+)/)?(?[^/]+)\.dll$') { + if ($matches.rid) { + $runTitle = "$($matches.lib) ($($matches.tfm), $($matches.rid), $Agent)" + } else { + $runTitle = "$($matches.lib) ($($matches.tfm)$x86RunTitleSuffix, $Agent)" + } + } + } + if (!$runTitle) { + $unknownCounter += 1; + $runTitle = "unknown$unknownCounter ($Agent$x86RunTitleSuffix)"; + } + + Write-Host "##vso[results.publish type=VSTest;runTitle=$runTitle;publishRunAttachments=true;resultFiles=$_;failTaskOnFailedTests=true;testRunSystem=VSTS - PTR;]" + } +} diff --git a/azure-pipelines/dotnet.yml b/azure-pipelines/dotnet.yml new file mode 100644 index 00000000..92ffd970 --- /dev/null +++ b/azure-pipelines/dotnet.yml @@ -0,0 +1,84 @@ +parameters: + RunTests: + +steps: + +- script: | + git config --global user.name ci + git config --global user.email me@ci.com + displayName: ⚙️ Configure git commit author for testing + +- script: dotnet build -t:build,pack --no-restore -c $(BuildConfiguration) /bl:"$(Build.ArtifactStagingDirectory)/build_logs/build.binlog" + displayName: 🛠 dotnet build + +- script: dotnet pack -c $(BuildConfiguration) --no-build -p:PackLKG=true /bl:"$(Build.ArtifactStagingDirectory)/build_logs/msbuild_lkg.binlog" + displayName: 🛠️ Build LKG package + workingDirectory: src/Nerdbank.GitVersioning.Tasks + +- script: dotnet publish -c $(BuildConfiguration) -o ../nerdbank-gitversioning.npm/out/nbgv.cli/tools/net6.0/any /bl:"$(Build.ArtifactStagingDirectory)/build_logs/nbgv_publish.binlog" + displayName: 📢 Publish nbgv tool + workingDirectory: src/nbgv + +- script: yarn build + displayName: 🛠️ Build nerdbank-gitversioning NPM package + workingDirectory: src/nerdbank-gitversioning.npm + +- powershell: azure-pipelines/dotnet-test-cloud.ps1 -Configuration $(BuildConfiguration) -Agent $(Agent.JobName) -PublishResults + displayName: 🧪 dotnet test + condition: and(succeeded(), ${{ parameters.RunTests }}) + +- powershell: azure-pipelines/dotnet-test-cloud.ps1 -Configuration $(BuildConfiguration) -Agent $(Agent.JobName) -PublishResults -X86 + displayName: 🧪 dotnet test x86 + condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) + +- powershell: azure-pipelines/artifacts/_pipelines.ps1 -ArtifactNameSuffix "-$(Agent.JobName)" -StageOnly + failOnStderr: true + displayName: 🗃️ Stage artifacts + condition: succeededOrFailed() + +- pwsh: > + dotnet tool install --tool-path obj SignClient + + obj/SignClient sign + --baseDirectory '$(Build.ArtifactStagingDirectory)/deployables-Windows' + --input '**/*' + --config '$(System.DefaultWorkingDirectory)/azure-pipelines/SignClient.json' + --filelist '$(System.DefaultWorkingDirectory)/azure-pipelines/signfiles.txt' + --user '$(codesign_username)' + --secret '$(codesign_secret)' + --name 'Nerdbank.GitVersioning' + --descriptionUrl 'https://github.com/dotnet/Nerdbank.GitVersioning' + displayName: 🔏 Code sign + condition: and(succeeded(), eq(variables['System.TeamFoundationCollectionUri'], 'https://dev.azure.com/andrewarnott/'), eq(variables['Agent.OS'], 'Windows_NT'), ne(variables['Build.Reason'], 'PullRequest')) + +- pwsh: > + obj/SignClient sign + --baseDirectory '$(Build.ArtifactStagingDirectory)/deployables-LKG-Windows' + --input '**/*' + --config '$(System.DefaultWorkingDirectory)/azure-pipelines/SignClient.json' + --filelist '$(System.DefaultWorkingDirectory)/azure-pipelines/signfiles.txt' + --user '$(codesign_username)' + --secret '$(codesign_secret)' + --name 'Nerdbank.GitVersioning' + --descriptionUrl 'https://github.com/dotnet/Nerdbank.GitVersioning' + displayName: 🔏 Code sign LKG + condition: and(succeeded(), eq(variables['System.TeamFoundationCollectionUri'], 'https://dev.azure.com/andrewarnott/'), eq(variables['Agent.OS'], 'Windows_NT'), ne(variables['Build.Reason'], 'PullRequest')) + +- powershell: azure-pipelines/variables/_pipelines.ps1 + failOnStderr: true + displayName: ⚙ Update pipeline variables based on build outputs + condition: succeededOrFailed() + +- powershell: azure-pipelines/artifacts/_pipelines.ps1 -ArtifactNameSuffix "-$(Agent.JobName)" -Verbose + failOnStderr: true + displayName: 📢 Publish artifacts + condition: succeededOrFailed() + +- ${{ if and(ne(variables['codecov_token'], ''), parameters.RunTests) }}: + - powershell: | + $ArtifactStagingFolder = & "azure-pipelines/Get-ArtifactsStagingDirectory.ps1" + $CoverageResultsFolder = Join-Path $ArtifactStagingFolder "coverageResults-$(Agent.JobName)" + azure-pipelines/publish-CodeCov.ps1 -CodeCovToken "$(codecov_token)" -PathToCodeCoverage "$CoverageResultsFolder" -Name "$(Agent.JobName) Coverage Results" -Flags "$(Agent.JobName)Host,$(BuildConfiguration)" + displayName: 📢 Publish code coverage results to codecov.io + timeoutInMinutes: 3 + continueOnError: true diff --git a/azure-pipelines/install-dependencies.yml b/azure-pipelines/install-dependencies.yml new file mode 100644 index 00000000..761d1800 --- /dev/null +++ b/azure-pipelines/install-dependencies.yml @@ -0,0 +1,30 @@ +parameters: + initArgs: + +steps: + +- task: NodeTool@0 + inputs: + versionSpec: 16.x + displayName: ⚙️ Install Node.js + +- task: NuGetAuthenticate@1 + displayName: 🔏 Authenticate NuGet feeds + inputs: + forceReinstallCredentialProvider: true + +- powershell: | + $AccessToken = '$(System.AccessToken)' # Avoid specifying the access token directly on the init.ps1 command line to avoid it showing up in errors + .\init.ps1 -AccessToken $AccessToken ${{ parameters['initArgs'] }} -UpgradePrerequisites -NoNuGetCredProvider + dotnet --info + + # Print mono version if it is present. + if (Get-Command mono -ErrorAction SilentlyContinue) { + mono --version + } + displayName: ⚙ Install prerequisites + +- powershell: azure-pipelines/variables/_pipelines.ps1 + failOnStderr: true + displayName: ⚙ Set pipeline variables based on source + name: SetPipelineVariables diff --git a/azure-pipelines/justnugetorg.nuget.config b/azure-pipelines/justnugetorg.nuget.config new file mode 100644 index 00000000..765346e5 --- /dev/null +++ b/azure-pipelines/justnugetorg.nuget.config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/azure-pipelines/publish-CodeCov.ps1 b/azure-pipelines/publish-CodeCov.ps1 new file mode 100644 index 00000000..9926f018 --- /dev/null +++ b/azure-pipelines/publish-CodeCov.ps1 @@ -0,0 +1,30 @@ +<# +.SYNOPSIS + Uploads code coverage to codecov.io +.PARAMETER CodeCovToken + Code coverage token to use +.PARAMETER PathToCodeCoverage + Path to root of code coverage files +.PARAMETER Name + Name to upload with codecoverge +.PARAMETER Flags + Flags to upload with codecoverge +#> +[CmdletBinding()] +Param ( + [Parameter(Mandatory=$true)] + [string]$CodeCovToken, + [Parameter(Mandatory=$true)] + [string]$PathToCodeCoverage, + [string]$Name, + [string]$Flags +) + +$RepoRoot = (Resolve-Path "$PSScriptRoot/..").Path + +Get-ChildItem -Recurse -Path $PathToCodeCoverage -Filter "*.cobertura.xml" | % { + $relativeFilePath = Resolve-Path -relative $_.FullName + + Write-Host "Uploading: $relativeFilePath" -ForegroundColor Yellow + & (& "$PSScriptRoot/Get-CodeCovTool.ps1") -t $CodeCovToken -f $relativeFilePath -R $RepoRoot -F $Flags -n $Name +} diff --git a/azure-pipelines/publish-codecoverage.yml b/azure-pipelines/publish-codecoverage.yml new file mode 100644 index 00000000..403b769f --- /dev/null +++ b/azure-pipelines/publish-codecoverage.yml @@ -0,0 +1,17 @@ +steps: +- download: current + artifact: coverageResults-Windows + displayName: 🔻 Download Windows code coverage results + continueOnError: true +- download: current + artifact: coverageResults-Linux + displayName: 🔻 Download Linux code coverage results + continueOnError: true +- powershell: azure-pipelines/Merge-CodeCoverage.ps1 -Path '$(Pipeline.Workspace)' -OutputFile coveragereport/merged.cobertura.xml -Format Cobertura -Verbose + displayName: ⚙ Merge coverage +- task: PublishCodeCoverageResults@1 + displayName: 📢 Publish code coverage results to Azure DevOps + inputs: + codeCoverageTool: cobertura + summaryFileLocation: coveragereport/merged.cobertura.xml + failIfCoverageEmpty: true diff --git a/azure-pipelines/publish-deployables.yml b/azure-pipelines/publish-deployables.yml new file mode 100644 index 00000000..6e5e3803 --- /dev/null +++ b/azure-pipelines/publish-deployables.yml @@ -0,0 +1,25 @@ +steps: +- download: current + displayName: 🔻 Download deployables + artifact: deployables-Windows + +- powershell: dotnet nuget push "$(Resolve-Path '$(Pipeline.Workspace)\deployables-Windows\')*.nupkg" -s $(ci_feed) -k azdo --skip-duplicate + displayName: 📦 Push packages to CI feed + condition: and(succeeded(), ne(variables['ci_feed'], ''), ne(variables['Build.Reason'], 'PullRequest')) + +- pwsh: Set-Content -Path "$(Agent.TempDirectory)/.npmrc" -Value "registry=$(ci_npm_feed)`nalways-auth=true" + displayName: ⚙️ Prepare to push to PublicCI + condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['System.TeamFoundationCollectionUri'], 'https://dev.azure.com/andrewarnott/'), ne(variables['Build.Reason'], 'PullRequest')) +- task: npmAuthenticate@0 + displayName: 🔐 Authenticate to PublicCI + inputs: + workingFile: $(Agent.TempDirectory)/.npmrc + condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['System.TeamFoundationCollectionUri'], 'https://dev.azure.com/andrewarnott/'), ne(variables['Build.Reason'], 'PullRequest')) +- pwsh: | + $tgz = (Get-ChildItem "$(Pipeline.Workspace)/deployables-Windows/*.tgz")[0].FullName + Write-Host "Will publish $tgz" + npm publish $tgz + workingDirectory: $(Agent.TempDirectory) + displayName: 📦 npm publish to PublicCI feed + continueOnError: true + condition: and(succeeded(), ne(variables['ci_npm_feed'], ''), ne(variables['Build.Reason'], 'PullRequest')) diff --git a/azure-pipelines/release.yml b/azure-pipelines/release.yml index 613b925c..e89ac645 100644 --- a/azure-pipelines/release.yml +++ b/azure-pipelines/release.yml @@ -9,96 +9,66 @@ resources: tags: - auto-release -stages: -- stage: GitHubRelease - displayName: GitHub Release - jobs: - - deployment: create - pool: - vmImage: ubuntu-latest - environment: No-Approval # Approval is already granted in the release stage. - strategy: - runOnce: - deploy: - steps: - - download: none - - powershell: | - Write-Host "##vso[build.updatebuildnumber]$(resources.pipeline.CI.runName)" - displayName: Set pipeline name - - task: GitHubRelease@1 - displayName: GitHub release (create) - inputs: - gitHubConnection: github.com_AArnott_OAuth - repositoryName: $(Build.Repository.Name) - target: $(resources.pipeline.CI.sourceCommit) - tagSource: userSpecifiedTag - tag: v$(resources.pipeline.CI.runName) - title: v$(resources.pipeline.CI.runName) - isDraft: true - changeLogCompareToRelease: lastNonDraftRelease - changeLogType: issueBased - changeLogLabels: | - [ - { "label" : "breaking changes", "displayName" : "Breaking changes", "state" : "closed" }, - { "label" : "bug", "displayName" : "Fixes", "state" : "closed" }, - { "label" : "enhancement", "displayName": "Enhancements", "state" : "closed" } - ] +variables: +- group: Publishing secrets -- stage: nuget_org - displayName: nuget.org - dependsOn: GitHubRelease - jobs: - - deployment: push - pool: - vmImage: ubuntu-latest - environment: No-Approval # Approval is already granted in the release stage. - strategy: - runOnce: - deploy: - steps: - - download: CI - artifact: deployables - displayName: Download deployables artifact - patterns: '**/*.*nupkg' - - task: NuGetToolInstaller@1 - displayName: Use NuGet 5.x - inputs: - versionSpec: 5.x - - task: NuGetCommand@2 - displayName: NuGet push - inputs: - command: push - packagesToPush: $(Pipeline.Workspace)/CI/deployables/*.nupkg - nuGetFeedType: external - publishFeedCredentials: nuget.org +jobs: +- job: release + pool: + vmImage: ubuntu-latest + steps: + - checkout: none + - powershell: | + Write-Host "##vso[build.updatebuildnumber]$(resources.pipeline.CI.runName)" + if ('$(resources.pipeline.CI.runName)'.Contains('-')) { + Write-Host "##vso[task.setvariable variable=IsPrerelease]true" + } else { + Write-Host "##vso[task.setvariable variable=IsPrerelease]false" + } + displayName: ⚙ Set up pipeline + - task: UseDotNet@2 + displayName: ⚙ Install .NET SDK + inputs: + packageType: sdk + version: 6.x + - download: CI + artifact: deployables-Windows + displayName: 🔻 Download deployables-Windows artifact + patterns: 'deployables-Windows/*' + - task: GitHubRelease@1 + displayName: 📢 GitHub release (create) + inputs: + gitHubConnection: github.com_AArnott_OAuth + repositoryName: $(Build.Repository.Name) + target: $(resources.pipeline.CI.sourceCommit) + tagSource: userSpecifiedTag + tag: v$(resources.pipeline.CI.runName) + title: v$(resources.pipeline.CI.runName) + isDraft: true # After running this step, visit the new draft release, edit, and publish. + isPreRelease: $(IsPrerelease) + assets: $(Pipeline.Workspace)/CI/deployables-Windows/*.nupkg + changeLogCompareToRelease: lastNonDraftRelease + changeLogType: issueBased + changeLogLabels: | + [ + { "label" : "breaking change", "displayName" : "Breaking changes", "state" : "closed" }, + { "label" : "bug", "displayName" : "Fixes", "state" : "closed" }, + { "label" : "enhancement", "displayName": "Enhancements", "state" : "closed" } + ] + - script: dotnet nuget push $(Pipeline.Workspace)/CI/deployables-Windows/*.nupkg -s https://api.nuget.org/v3/index.json --api-key $(NuGetOrgApiKey) --skip-duplicate + displayName: 📦 Push packages to nuget.org + condition: and(succeeded(), ne(variables['NuGetOrgApiKey'], '')) + - powershell: | + $tgz = (Get-ChildItem "$(Pipeline.Workspace)/CI/deployables-Windows/*.tgz")[0].FullName -- stage: npmjs_org - displayName: npmjs.org - dependsOn: GitHubRelease - jobs: - - deployment: push - pool: - vmImage: ubuntu-latest - environment: No-Approval # Approval is already granted in the release stage. - strategy: - runOnce: - deploy: - steps: - - download: CI - artifact: deployables - displayName: Download deployables artifact - patterns: '**/*.tgz' - - powershell: | - $tgz = (Get-ChildItem "$(Pipeline.Workspace)/CI/deployables/*.tgz")[0].FullName - - npm init -y - npm install $tgz - workingDirectory: $(Agent.TempDirectory) - displayName: Prepare to publish NPM package - - task: Npm@1 - displayName: npm publish - inputs: - command: publish - workingDir: $(Agent.TempDirectory)/node_modules/nerdbank-gitversioning - verbose: false - publishEndpoint: npmjs.org + npm init -y + npm install $tgz + workingDirectory: $(Agent.TempDirectory) + displayName: ⚙️ Prepare to publish NPM package + - task: Npm@1 + displayName: 📦 npm publish + inputs: + command: publish + workingDir: $(Agent.TempDirectory)/node_modules/nerdbank-gitversioning + verbose: false + publishEndpoint: npmjs.org diff --git a/azure-pipelines/test.runsettings b/azure-pipelines/test.runsettings new file mode 100644 index 00000000..4e24a0a6 --- /dev/null +++ b/azure-pipelines/test.runsettings @@ -0,0 +1,44 @@ + + + + + + + + + \.dll$ + \.exe$ + + + xunit\..* + + + + + ^System\.Diagnostics\.DebuggerHiddenAttribute$ + ^System\.Diagnostics\.DebuggerNonUserCodeAttribute$ + ^System\.CodeDom\.Compiler\.GeneratedCodeAttribute$ + ^System\.Diagnostics\.CodeAnalysis\.ExcludeFromCodeCoverageAttribute$ + + + + + True + + True + + True + + False + + False + + False + + True + + + + + + diff --git a/azure-pipelines/variables/_all.ps1 b/azure-pipelines/variables/_all.ps1 new file mode 100644 index 00000000..cc6e8810 --- /dev/null +++ b/azure-pipelines/variables/_all.ps1 @@ -0,0 +1,20 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + This script returns a hashtable of build variables that should be set + at the start of a build or release definition's execution. +#> + +[CmdletBinding(SupportsShouldProcess = $true)] +param ( +) + +$vars = @{} + +Get-ChildItem "$PSScriptRoot\*.ps1" -Exclude "_*" |% { + Write-Host "Computing $($_.BaseName) variable" + $vars[$_.BaseName] = & $_ +} + +$vars diff --git a/azure-pipelines/variables/_pipelines.ps1 b/azure-pipelines/variables/_pipelines.ps1 new file mode 100644 index 00000000..11748b81 --- /dev/null +++ b/azure-pipelines/variables/_pipelines.ps1 @@ -0,0 +1,31 @@ +<# +.SYNOPSIS + This script translates the variables returned by the _all.ps1 script + into commands that instruct Azure Pipelines to actually set those variables for other pipeline tasks to consume. + + The build or release definition may have set these variables to override + what the build would do. So only set them if they have not already been set. +#> + +[CmdletBinding()] +param ( +) + +(& "$PSScriptRoot\_all.ps1").GetEnumerator() |% { + # Always use ALL CAPS for env var names since Azure Pipelines converts variable names to all caps and on non-Windows OS, env vars are case sensitive. + $keyCaps = $_.Key.ToUpper() + if ((Test-Path "env:$keyCaps") -and (Get-Content "env:$keyCaps")) { + Write-Host "Skipping setting $keyCaps because variable is already set to '$(Get-Content env:$keyCaps)'." -ForegroundColor Cyan + } else { + Write-Host "$keyCaps=$($_.Value)" -ForegroundColor Yellow + if ($env:TF_BUILD) { + # Create two variables: the first that can be used by its simple name and accessible only within this job. + Write-Host "##vso[task.setvariable variable=$keyCaps]$($_.Value)" + # and the second that works across jobs and stages but must be fully qualified when referenced. + Write-Host "##vso[task.setvariable variable=$keyCaps;isOutput=true]$($_.Value)" + } elseif ($env:GITHUB_ACTIONS) { + Add-Content -Path $env:GITHUB_ENV -Value "$keyCaps=$($_.Value)" + } + Set-Item -Path "env:$keyCaps" -Value $_.Value + } +} diff --git a/azure-pipelines/xplattest-pipeline.yml b/azure-pipelines/xplattest-pipeline.yml index 0841124a..0b2e29f9 100644 --- a/azure-pipelines/xplattest-pipeline.yml +++ b/azure-pipelines/xplattest-pipeline.yml @@ -7,7 +7,7 @@ steps: - task: DownloadBuildArtifacts@0 displayName: Download Build Artifacts inputs: - artifactName: deployables + artifactName: deployables-Windows downloadPath: $(System.DefaultWorkingDirectory) - script: | @@ -17,11 +17,11 @@ steps: displayName: Set up git username and email address - script: > - PkgFileName=$(ls deployables/Nerdbank.GitVersioning.*nupkg) + PkgFileName=$(ls deployables-Windows/Nerdbank.GitVersioning.*nupkg) NBGV_NuGetPackageVersion=$([[ $PkgFileName =~ Nerdbank.GitVersioning\.(.*)\.nupkg ]] && echo "${BASH_REMATCH[1]}") - echo "" > nuget.config && + echo "" > nuget.config && dotnet new classlib -o lib && cd lib && echo '{"version":"42.42"}' > version.json && @@ -43,19 +43,7 @@ steps: failOnStderr: true - script: > - # Uses dotnet commands that require at least 3.x - - DNVERSION=$(dotnet --version) - - if [[ $DNVERSION == 2.* ]] ; - then - echo "Skipping .NET Core $DNVERSION" - exit 0 - else - echo ".NET Core $DNVERSION" - fi - - PkgFileName=$(ls deployables/Cake.GitVersioning.*nupkg) + PkgFileName=$(ls deployables-Windows/Cake.GitVersioning.*nupkg) NBGV_NuGetPackageVersion=$([[ $PkgFileName =~ Cake.GitVersioning\.(.*)\.nupkg ]] && echo "${BASH_REMATCH[1]}") @@ -80,11 +68,11 @@ steps: - script: > echo DOTNET_ROOT=$DOTNET_ROOT - PkgFileName=$(ls deployables/Nerdbank.GitVersioning.*nupkg) + PkgFileName=$(ls deployables-Windows/Nerdbank.GitVersioning.*nupkg) NBGV_NuGetPackageVersion=$([[ $PkgFileName =~ Nerdbank.GitVersioning\.(.*)\.nupkg ]] && echo "${BASH_REMATCH[1]}") - dotnet tool install nbgv --tool-path . --version $NBGV_NuGetPackageVersion --add-source deployables && + dotnet tool install nbgv --tool-path . --version $NBGV_NuGetPackageVersion --add-source deployables-Windows && ./nbgv get-version -f json -p lib && ./nbgv get-version -f json -p lib | grep 42.42.1 displayName: Use nbgv dotnet CLI tool diff --git a/build.ps1 b/build.ps1 index f49055e5..269bdaa2 100644 --- a/build.ps1 +++ b/build.ps1 @@ -15,7 +15,7 @@ Param( [string]$MsBuildVerbosity = 'minimal' ) -$msbuildCommandLine = "dotnet build `"$PSScriptRoot\src\Nerdbank.GitVersioning.sln`" /m /verbosity:$MsBuildVerbosity /nologo /p:Platform=`"Any CPU`" /t:build,pack" +$msbuildCommandLine = "dotnet build `"$PSScriptRoot\Nerdbank.GitVersioning.sln`" /m /verbosity:$MsBuildVerbosity /nologo /p:Platform=`"Any CPU`" /t:build,pack" if (Test-Path "C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll") { $msbuildCommandLine += " /logger:`"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll`"" @@ -27,7 +27,7 @@ if ($Configuration) { Push-Location . try { - if ($PSCmdlet.ShouldProcess("$PSScriptRoot\src\Nerdbank.GitVersioning.sln", "msbuild")) { + if ($PSCmdlet.ShouldProcess("$PSScriptRoot\Nerdbank.GitVersioning.sln", "msbuild")) { Invoke-Expression $msbuildCommandLine if ($LASTEXITCODE -ne 0) { throw "dotnet build failed" diff --git a/doc/cloudbuild.md b/doc/cloudbuild.md index 9230c4c0..3db9e4b9 100644 --- a/doc/cloudbuild.md +++ b/doc/cloudbuild.md @@ -22,12 +22,30 @@ But `actions/checkout@v2` checks out a shallow clone by default, so you'll have fetch-depth: 0 # avoid shallow clone so nbgv can do its work. ``` +### Azure Pipelines + +[Azure Pipelines behavior has changed][AzpDepthChange] for *new* pipelines +such that build agents now default to creating shallow clones. +You can defeat this, thereby forcing a full history clone by adding this to the top of your `steps` list: + +```yml +steps: +- checkout: self + fetchDepth: 0 +``` + +In particular, setting `fetchDepth: 0` will cause Azure Pipelines to *not* do shallow clones. + +See this [example change](https://github.com/AArnott/Library.Template/commit/5d14d2cecbb3fd3caa6a421da1525d8480baef8b). + +[Read more about this and how to configure shallow cloning when not using YAML files in Microsoft documentation](https://learn.microsoft.com/azure/devops/pipelines/repos/azure-repos-git?view=azure-devops&tabs=yaml#shallow-fetch). + ## Optional features By specifying certain `cloudBuild` options in your `version.json` file, you can activate features for some cloud build systems, as follows: -### Automatically match cloud build numbers to to your git version +### Automatically match cloud build numbers to your git version Cloud builds tend to associate some calendar date or monotonically increasing build number to each build. These build numbers are not very informative, if at all. @@ -177,3 +195,4 @@ When building inside a docker container, special considerations may apply: When using `docker run` yourself in your build script, you can add `--env BUILD_SOURCEBRANCH --env SYSTEM_TEAMPROJECTID` to your command line to pass-through those environment variables to your container. [Issue37]: https://github.com/dotnet/Nerdbank.GitVersioning/issues/37 +[AzpDepthChange]: https://github.com/MicrosoftDocs/azure-devops-yaml-schema/issues/32 diff --git a/doc/nbgv-cli.md b/doc/nbgv-cli.md index 3c9f46af..b7e58610 100644 --- a/doc/nbgv-cli.md +++ b/doc/nbgv-cli.md @@ -22,8 +22,9 @@ It will also add/modify your `Directory.Build.props` file in the root of your re If scripting for running in a CI build where global impact from installing a tool is undesirable, you can localize the tool installation: ```ps1 -dotnet tool install --tool-path . nbgv +dotnet tool install --tool-path my/path nbgv ``` +> Ensure your custom path is outside of your git repository, as the `nbgv` tool doesn't support uncommited changes At this point you can launch the tool using `./nbgv` in your build script. @@ -41,7 +42,7 @@ The `prepare-release` command automates the task of branching off the main devel The `prepare-release` command supports this working model by taking care of creating the release branch and updating `version.json` on both branches. -To prepare a release, run: +To prepare a release, first ensure there is no uncommited changes in your repository then run: ```ps1 nbgv prepare-release @@ -127,7 +128,7 @@ By default, the `prepare-release` command writes information about created and u Alternatively the information can be written to the output as `json`. The output format to use can be set using the `--format` command line parameter. -For example, running the follwoing command on `master` +For example, running the following command on `master` ``` nbgv prepare-release --format json @@ -164,6 +165,44 @@ For each branch, the following properties are provided: **Note:** When the current branch is already the release branch for the current version, no new branch will be created. In that case, the `NewBranch` property will be `null`. +## Creating a version tag + +The `tag` command automates the task of tagging a commit with a version. + +To create a version tag, run: + +```ps1 +nbgv tag +``` + +This will: + +1. Read version.json to ascertain the version under development, and the naming convention of tag names. +1. Create a new tag for that version. + +You can optionally include a version or commit id to create a new tag for an older version/commit, e.g.: + +```ps1 +nbgv tag 1.0.0 +``` + +### Customizing the behaviour of `tag` + +The behaviour of the `tag` command can be customized in `version.json`: + +```json +{ + "version": "1.0", + "release": { + "tagName" : "v{version}" + } +} +``` + +| Property | Default value | Description | +|----------|---------------|-------------------------------------------------------------------------------------------------| +| tagName | `v{version}` | Defines the format of tag names. Format must include a placeholder '{version}' for the version. | + ## Learn more There are several more sub-commands and switches to each to help you build and maintain your projects, find a commit that built a particular version later on, create tags, etc. diff --git a/doc/versionJson.md b/doc/versionJson.md index 910a03e3..a56adb55 100644 --- a/doc/versionJson.md +++ b/doc/versionJson.md @@ -10,7 +10,7 @@ Here is the content of a sample version.json file you may start with: ```json { - "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", "version": "1.0-beta" } ``` @@ -59,6 +59,7 @@ The content of the version.json file is a JSON serialized object with these prop } }, "release" : { + "tagName" : "v{version}", "branchName" : "v{version}", "versionIncrement" : "minor", "firstUnstableTag" : "alpha" diff --git a/global.json b/global.json index e2af429b..c7d7e468 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "6.0.100", + "version": "7.0.203", "rollForward": "patch", "allowPrerelease": false } diff --git a/init.cmd b/init.cmd index 970285c2..667efabb 100644 --- a/init.cmd +++ b/init.cmd @@ -1,4 +1,20 @@ @echo off SETLOCAL set PS1UnderCmd=1 + +:: Get the datetime in a format that can go in a filename. +set _my_datetime=%date%_%time% +set _my_datetime=%_my_datetime: =_% +set _my_datetime=%_my_datetime::=% +set _my_datetime=%_my_datetime:/=_% +set _my_datetime=%_my_datetime:.=_% +set CmdEnvScriptPath=%temp%\envvarscript_%_my_datetime%.cmd + powershell.exe -NoProfile -NoLogo -ExecutionPolicy bypass -Command "try { & '%~dpn0.ps1' %*; exit $LASTEXITCODE } catch { write-host $_; exit 1 }" + +:: Set environment variables in the parent cmd.exe process. +IF EXIST "%CmdEnvScriptPath%" ( + ENDLOCAL + CALL "%CmdEnvScriptPath%" + DEL "%CmdEnvScriptPath%" +) diff --git a/init.ps1 b/init.ps1 index 8b11d60b..062c8c85 100755 --- a/init.ps1 +++ b/init.ps1 @@ -2,68 +2,123 @@ <# .SYNOPSIS -Installs dependencies required to build and test the projects in this repository. + Installs dependencies required to build and test the projects in this repository. .DESCRIPTION -This MAY not require elevation, as the SDK and runtimes are installed to a per-user location, -unless the `-InstallLocality` switch is specified directing to a per-repo or per-machine location. -See detailed help on that switch for more information. + This MAY not require elevation, as the SDK and runtimes are installed to a per-user location, + unless the `-InstallLocality` switch is specified directing to a per-repo or per-machine location. + See detailed help on that switch for more information. + + The CmdEnvScriptPath environment variable may be optionally set to a path to a cmd shell script to be created (or appended to if it already exists) that will set the environment variables in cmd.exe that are set within the PowerShell environment. + This is used by init.cmd in order to reapply any new environment variables to the parent cmd.exe process that were set in the powershell child process. .PARAMETER InstallLocality -A value indicating whether dependencies should be installed locally to the repo or at a per-user location. -Per-user allows sharing the installed dependencies across repositories and allows use of a shared expanded package cache. -Visual Studio will only notice and use these SDKs/runtimes if VS is launched from the environment that runs this script. -Per-repo allows for high isolation, allowing for a more precise recreation of the environment within an Azure Pipelines build. -When using 'repo', environment variables are set to cause the locally installed dotnet SDK to be used. -Per-repo can lead to file locking issues when dotnet.exe is left running as a build server and can be mitigated by running `dotnet build-server shutdown`. -Per-machine requires elevation and will download and install all SDKs and runtimes to machine-wide locations so all applications can find it. + A value indicating whether dependencies should be installed locally to the repo or at a per-user location. + Per-user allows sharing the installed dependencies across repositories and allows use of a shared expanded package cache. + Visual Studio will only notice and use these SDKs/runtimes if VS is launched from the environment that runs this script. + Per-repo allows for high isolation, allowing for a more precise recreation of the environment within an Azure Pipelines build. + When using 'repo', environment variables are set to cause the locally installed dotnet SDK to be used. + Per-repo can lead to file locking issues when dotnet.exe is left running as a build server and can be mitigated by running `dotnet build-server shutdown`. + Per-machine requires elevation and will download and install all SDKs and runtimes to machine-wide locations so all applications can find it. .PARAMETER NoPrerequisites -Skips the installation of prerequisite software (e.g. SDKs, tools). + Skips the installation of prerequisite software (e.g. SDKs, tools). +.PARAMETER NoNuGetCredProvider + Skips the installation of the NuGet credential provider. Useful in pipelines with the `NuGetAuthenticate` task, as a workaround for https://github.com/microsoft/artifacts-credprovider/issues/244. + This switch is ignored and installation is skipped when -NoPrerequisites is specified. +.PARAMETER UpgradePrerequisites + Takes time to install prerequisites even if they are already present in case they need to be upgraded. + No effect if -NoPrerequisites is specified. .PARAMETER NoRestore -Skips the package restore step. + Skips the package restore step. +.PARAMETER NoToolRestore + Skips the dotnet tool restore step. +.PARAMETER AccessToken + An optional access token for authenticating to Azure Artifacts authenticated feeds. +.PARAMETER Interactive + Runs NuGet restore in interactive mode. This can turn authentication failures into authentication challenges. #> -[CmdletBinding(SupportsShouldProcess=$true)] -Param( - [ValidateSet('repo','user','machine')] - [string]$InstallLocality='user', +[CmdletBinding(SupportsShouldProcess = $true)] +Param ( + [ValidateSet('repo', 'user', 'machine')] + [string]$InstallLocality = 'user', [Parameter()] [switch]$NoPrerequisites, [Parameter()] - [switch]$NoRestore + [switch]$NoNuGetCredProvider, + [Parameter()] + [switch]$UpgradePrerequisites, + [Parameter()] + [switch]$NoRestore, + [Parameter()] + [switch]$NoToolRestore, + [Parameter()] + [string]$AccessToken, + [Parameter()] + [switch]$Interactive ) +$EnvVars = @{} +$PrependPath = @() + if (!$NoPrerequisites) { - & "$PSScriptRoot\tools\Install-DotNetSdk.ps1" -InstallLocality $InstallLocality + if (!$NoNuGetCredProvider) { + & "$PSScriptRoot\tools\Install-NuGetCredProvider.ps1" -AccessToken $AccessToken -Force:$UpgradePrerequisites + } + + & "$PSScriptRoot\tools\Install-DotNetSdk.ps1" -InstallLocality $InstallLocality -IncludeX86 + if ($LASTEXITCODE -eq 3010) { + Exit 3010 + } + + # The procdump tool and env var is required for dotnet test to collect hang/crash dumps of tests. + # But it only works on Windows. + if ($env:OS -eq 'Windows_NT') { + $EnvVars['PROCDUMP_PATH'] = & "$PSScriptRoot\azure-pipelines\Get-ProcDump.ps1" + } } -$oldPlatform=$env:Platform -$env:Platform='Any CPU' # Some people wander in here from a platform-specific build window. +# Workaround nuget credential provider bug that causes very unreliable package restores on Azure Pipelines +$env:NUGET_PLUGIN_HANDSHAKE_TIMEOUT_IN_SECONDS = 20 +$env:NUGET_PLUGIN_REQUEST_TIMEOUT_IN_SECONDS = 20 Push-Location $PSScriptRoot try { $HeaderColor = 'Green' if (!$NoRestore -and $PSCmdlet.ShouldProcess("NuGet packages", "Restore")) { + $RestoreArguments = @() + if ($Interactive) + { + $RestoreArguments += '--interactive' + } + Write-Host "Restoring NuGet packages" -ForegroundColor $HeaderColor - dotnet restore "$PSScriptRoot\src" + dotnet restore @RestoreArguments if ($lastexitcode -ne 0) { throw "Failure while restoring packages." } } - Write-Host "Restoring NPM packages..." -ForegroundColor Yellow - Push-Location "$PSScriptRoot\src\nerdbank-gitversioning.npm" - try { + if (!$NoToolRestore -and $PSCmdlet.ShouldProcess("dotnet tool", "restore")) { + dotnet tool restore @RestoreArguments + if ($lastexitcode -ne 0) { + throw "Failure while restoring dotnet CLI tools." + } + } + + if (!$NoRestore -and $PSCmdlet.ShouldProcess("NPM packages", "Restore")) { + Write-Host "Installing yarn" -ForegroundColor Yellow + npm i -g yarn@">=1.22 <2.0" + Write-Host "Restoring NPM packages..." -ForegroundColor Yellow if ($PSCmdlet.ShouldProcess("$PSScriptRoot\src\nerdbank-gitversioning.npm", "yarn install")) { - yarn install --loglevel error + yarn install --cwd src/nerdbank-gitversioning.npm --loglevel error } - } finally { - Pop-Location } - Write-Host "Successfully restored all dependencies" -ForegroundColor Yellow -} catch { + & "$PSScriptRoot/tools/Set-EnvVars.ps1" -Variables $EnvVars -PrependPath $PrependPath | Out-Null +} +catch { Write-Error $error[0] exit $lastexitcode -} finally { - $env:Platform=$oldPlatform +} +finally { Pop-Location } diff --git a/nuget.config b/nuget.config new file mode 100644 index 00000000..b60f26be --- /dev/null +++ b/nuget.config @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/.editorconfig b/src/.editorconfig new file mode 100644 index 00000000..b12e588d --- /dev/null +++ b/src/.editorconfig @@ -0,0 +1,16 @@ +[*.cs] + +# CS1591: Missing XML comment for publicly visible type or member +dotnet_diagnostic.CS1591.severity = suggestion + +# SA1600: Elements should be documented +dotnet_diagnostic.SA1600.severity = suggestion + +# SA1405: Debug.Assert should provide message text +dotnet_diagnostic.SA1405.severity = suggestion + +# SA1601: Partial elements should be documented +dotnet_diagnostic.SA1601.severity = suggestion + +# SA1605: Partial element documentation should have summary +dotnet_diagnostic.SA1605.severity = suggestion diff --git a/src/AssemblyInfo.cs b/src/AssemblyInfo.cs new file mode 100644 index 00000000..f98fa527 --- /dev/null +++ b/src/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; + +[assembly: DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] diff --git a/src/Cake.GitVersioning.Tests/Cake.GitVersioning.Tests.csproj b/src/Cake.GitVersioning.Tests/Cake.GitVersioning.Tests.csproj deleted file mode 100644 index cbf2dcd2..00000000 --- a/src/Cake.GitVersioning.Tests/Cake.GitVersioning.Tests.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - net5.0;net461 - false - true - false - true - true - full - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - diff --git a/src/Cake.GitVersioning/Cake.GitVersioning.csproj b/src/Cake.GitVersioning/Cake.GitVersioning.csproj index ed37946b..7c978f07 100644 --- a/src/Cake.GitVersioning/Cake.GitVersioning.csproj +++ b/src/Cake.GitVersioning/Cake.GitVersioning.csproj @@ -1,12 +1,11 @@  - netstandard2.0 + net6.0 true Chris Crutchfield, Andrew Arnott andarno Cake wrapper for Nerdbank.GitVersioning. Stamps your assemblies with semver 2.0 compliant git commit specific version information and provides NuGet versioning information as well. - Copyright (c) .NET Foundation and Contributors git commit versioning version assemblyinfo cake-addin https://cdn.jsdelivr.net/gh/cake-contrib/graphics/png/addin/cake-contrib-addin-medium.png cake-contrib-addin-medium.png @@ -29,45 +28,32 @@ - - - + + + - + - - - - - + + + + + - + - + lib\$(TargetFramework) diff --git a/src/Cake.GitVersioning/GitVersioningAliases.cs b/src/Cake.GitVersioning/GitVersioningAliases.cs index aff32240..02303b15 100644 --- a/src/Cake.GitVersioning/GitVersioningAliases.cs +++ b/src/Cake.GitVersioning/GitVersioningAliases.cs @@ -1,13 +1,16 @@ -namespace Cake.GitVersioning -{ - using System; - using System.IO; - using System.Reflection; - using Cake.Core; - using Cake.Core.Annotations; - using Nerdbank.GitVersioning; - using Nerdbank.GitVersioning.Commands; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using Cake.Core; +using Cake.Core.Annotations; +using Nerdbank.GitVersioning; +using Nerdbank.GitVersioning.Commands; +namespace Cake.GitVersioning +{ /// /// Contains functionality for using Nerdbank.GitVersioning. /// @@ -17,20 +20,23 @@ public static class GitVersioningAliases /// /// Gets the Git Versioning version from the current repo. /// - /// + /// The context. + /// Directory to start the search for version.json. + /// The version information from Git Versioning. + /// + /// Example: + /// /// { /// Information(GetVersioningGetVersion().SemVer2) /// }); - /// - /// The context. - /// Directory to start the search for version.json. - /// The version information from Git Versioning. + /// ]]> + /// [CakeMethodAlias] public static VersionOracle GitVersioningGetVersion(this ICakeContext cakeContext, string projectDirectory = ".") { - var fullProjectDirectory = (new DirectoryInfo(projectDirectory)).FullName; + string fullProjectDirectory = new DirectoryInfo(projectDirectory).FullName; string directoryName = Path.GetDirectoryName(Assembly.GetAssembly(typeof(GitVersioningAliases)).Location); @@ -46,20 +52,23 @@ public static VersionOracle GitVersioningGetVersion(this ICakeContext cakeContex /// /// Adds versioning information to the current build environment's variables. /// - /// + /// The context. + /// Directory to start the search for version.json. + /// The settings to use for updating variables. + /// + /// Example: + /// /// { /// GitVersioningCloud() /// }); - /// - /// The context. - /// Directory to start the search for version.json. - /// The settings to use for updating variables. + /// ]]> + /// [CakeMethodAlias] public static void GitVersioningCloud(this ICakeContext cakeContext, string projectDirectory = ".", GitVersioningCloudSettings settings = null) { - var fullProjectDirectory = (new DirectoryInfo(projectDirectory)).FullName; + string fullProjectDirectory = new DirectoryInfo(projectDirectory).FullName; string directoryName = Path.GetDirectoryName(Assembly.GetAssembly(typeof(GitVersioningAliases)).Location); @@ -79,9 +88,7 @@ public static void GitVersioningCloud(this ICakeContext cakeContext, string proj settings.AllVariables, settings.CommonVariables, settings.AdditionalVariables, - false - ); + false); } - } } diff --git a/src/Cake.GitVersioning/GitVersioningCloudProvider.cs b/src/Cake.GitVersioning/GitVersioningCloudProvider.cs index 5a658028..0e9ad07d 100644 --- a/src/Cake.GitVersioning/GitVersioningCloudProvider.cs +++ b/src/Cake.GitVersioning/GitVersioningCloudProvider.cs @@ -1,4 +1,7 @@ -namespace Cake.GitVersioning +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cake.GitVersioning { /// /// Defines the supported cloud build providers for the alias. @@ -49,5 +52,10 @@ public enum GitVersioningCloudProvider /// Use the Jetbrains Space cloud build provider. /// SpaceAutomation, + + /// + /// Use the Bitbucket cloud build provider. + /// + BitbucketCloud, } } diff --git a/src/Cake.GitVersioning/GitVersioningCloudSettings.cs b/src/Cake.GitVersioning/GitVersioningCloudSettings.cs index 1082373b..9ccf5d32 100644 --- a/src/Cake.GitVersioning/GitVersioningCloudSettings.cs +++ b/src/Cake.GitVersioning/GitVersioningCloudSettings.cs @@ -1,44 +1,44 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable namespace Cake.GitVersioning { - using System; - using System.Collections.Generic; - /// /// Defines settings for the alias. /// public class GitVersioningCloudSettings { /// - /// The string to use for the cloud build number. + /// Gets or sets the string to use for the cloud build number. /// If no value is specified, the computed version will be used. /// public string? Version { get; set; } /// - /// Adds an identifier to the build metadata part of a semantic version. + /// Gets a list of identifiers to include with the build metadata part of a semantic version. /// public List Metadata { get; } = new(); /// - /// Force activation for a particular CI system. If not specified, - /// auto-detection will be used. + /// Gets or sets a a particular CI system to force usage of. + /// If not specified, auto-detection will be used. /// public GitVersioningCloudProvider? CISystem { get; set; } /// - /// Defines ALL version variables as cloud build variables, with a "NBGV_" prefix. + /// Gets or sets a value indicating whether to define ALL version variables as cloud build variables, with a "NBGV_" prefix. /// public bool AllVariables { get; set; } /// - /// Defines a few common version variables as cloud build variables, with a "Git" prefix. + /// Gets or sets a value indicating whether to define a few common version variables as cloud build variables, with a "Git" prefix. /// public bool CommonVariables { get; set; } /// - /// Additional cloud build variables to define. + /// Gets additional cloud build variables to define. /// public Dictionary AdditionalVariables { get; } = new(StringComparer.OrdinalIgnoreCase); } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 105676df..052fe3ef 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,43 +1,3 @@ - - Debug - $(MSBuildThisFileDirectory)..\obj\$(MSBuildProjectName)\ - $(MSBuildThisFileDirectory)..\bin\$(MSBuildProjectName)\$(Configuration)\ - $(MSBuildThisFileDirectory)..\wiki\api - 9.0 - - true - $(MSBuildThisFileDirectory)strongname.snk - - Andrew Arnott - aarnott - git commit versioning version assemblyinfo - https://github.com/dotnet/Nerdbank.GitVersioning - MIT - - true - true - $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb - - - - 2.0.315-alpha.0.9 - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - - - - https://github.com/dotnet/Nerdbank.GitVersioning/releases/tag/v$(Version) - - + diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets new file mode 100644 index 00000000..c1d929a5 --- /dev/null +++ b/src/Directory.Build.targets @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/NerdBank.GitVersioning.Benchmarks/Program.cs b/src/NerdBank.GitVersioning.Benchmarks/Program.cs deleted file mode 100644 index 90149bd6..00000000 --- a/src/NerdBank.GitVersioning.Benchmarks/Program.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using BenchmarkDotNet.Running; - -namespace Nerdbank.GitVersioning.Benchmarks -{ - class Program - { - static void Main(string[] args) => - BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); - } -} diff --git a/src/NerdBank.GitVersioning.Tests/AssemblyInfoTest.cs b/src/NerdBank.GitVersioning.Tests/AssemblyInfoTest.cs deleted file mode 100644 index 253fdeb6..00000000 --- a/src/NerdBank.GitVersioning.Tests/AssemblyInfoTest.cs +++ /dev/null @@ -1,221 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Build.Utilities; -using Nerdbank.GitVersioning.Tasks; -using Xunit; - -public class AssemblyInfoTest : IClassFixture -{ - [Theory] - [InlineData(false)] - [InlineData(true)] - [InlineData(null)] - public void FSharpGenerator(bool? thisAssemblyClass) - { - var info = new AssemblyVersionInfo(); - info.AssemblyCompany = "company"; - info.AssemblyFileVersion = "1.3.1.0"; - info.AssemblyVersion = "1.3.0.0"; - info.AdditionalThisAssemblyFields = - new TaskItem[] - { - new TaskItem( - "CustomString1", - new Dictionary() { { "String", "abc" } } ), - new TaskItem( - "CustomString2", - new Dictionary() { { "String", "" } } ), - new TaskItem( - "CustomString3", - new Dictionary() { { "String", "" }, { "EmitIfEmpty", "true" } } ), - new TaskItem( - "CustomBool", - new Dictionary() { { "Boolean", "true" } } ), - new TaskItem( - "CustomTicks", - new Dictionary() { { "Ticks", "637509805729817056" } } ), - }; - info.CodeLanguage = "f#"; - - if (thisAssemblyClass.HasValue) - { - info.EmitThisAssemblyClass = thisAssemblyClass.GetValueOrDefault(); - } - - var built = info.BuildCode(); - - var expected = $@"//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -#nowarn ""CA2243"" - -namespace AssemblyInfo -[] -[] -[] -{(thisAssemblyClass.GetValueOrDefault(true) ? $@"do() -#if NETSTANDARD || NETFRAMEWORK || NETCOREAPP -[] -#endif -#if NETFRAMEWORK || NETCOREAPP || NETSTANDARD2_0 || NETSTANDARD2_1 -[] -#endif -type internal ThisAssembly() = - static member internal AssemblyCompany = ""company"" - static member internal AssemblyFileVersion = ""1.3.1.0"" - static member internal AssemblyVersion = ""1.3.0.0"" - static member internal CustomBool = true - static member internal CustomString1 = ""abc"" - static member internal CustomString3 = """" - static member internal CustomTicks = new System.DateTime(637509805729817056L, System.DateTimeKind.Utc) - static member internal IsPrerelease = false - static member internal IsPublicRelease = false - static member internal RootNamespace = """" -do() -" : "")}"; - - Assert.Equal(expected, built); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - [InlineData(null)] - public void CSharpGenerator(bool? thisAssemblyClass) - { - var info = new AssemblyVersionInfo(); - info.AssemblyCompany = "company"; - info.AssemblyFileVersion = "1.3.1.0"; - info.AssemblyVersion = "1.3.0.0"; - info.CodeLanguage = "c#"; - info.AdditionalThisAssemblyFields = - new TaskItem[] - { - new TaskItem( - "CustomString1", - new Dictionary() { { "String", "abc" } } ), - new TaskItem( - "CustomString2", - new Dictionary() { { "String", "" } } ), - new TaskItem( - "CustomString3", - new Dictionary() { { "String", "" }, { "EmitIfEmpty", "true" } } ), - new TaskItem( - "CustomBool", - new Dictionary() { { "Boolean", "true" } } ), - new TaskItem( - "CustomTicks", - new Dictionary() { { "Ticks", "637509805729817056" } } ), - }; - - if (thisAssemblyClass.HasValue) - { - info.EmitThisAssemblyClass = thisAssemblyClass.GetValueOrDefault(); - } - - var built = info.BuildCode(); - - var expected = $@"//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -#pragma warning disable CA2243 - -[assembly: System.Reflection.AssemblyVersionAttribute(""1.3.0.0"")] -[assembly: System.Reflection.AssemblyFileVersionAttribute(""1.3.1.0"")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("""")] -{(thisAssemblyClass.GetValueOrDefault(true) ? $@"#if NETSTANDARD || NETFRAMEWORK || NETCOREAPP -[System.CodeDom.Compiler.GeneratedCode(""{AssemblyVersionInfo.GeneratorName}"",""{AssemblyVersionInfo.GeneratorVersion}"")] -#endif -#if NETFRAMEWORK || NETCOREAPP || NETSTANDARD2_0 || NETSTANDARD2_1 -[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -#endif -internal static partial class ThisAssembly {{ - internal const string AssemblyCompany = ""company""; - internal const string AssemblyFileVersion = ""1.3.1.0""; - internal const string AssemblyVersion = ""1.3.0.0""; - internal const bool CustomBool = true; - internal const string CustomString1 = ""abc""; - internal const string CustomString3 = """"; - internal static readonly System.DateTime CustomTicks = new System.DateTime(637509805729817056L, System.DateTimeKind.Utc); - internal const bool IsPrerelease = false; - internal const bool IsPublicRelease = false; - internal const string RootNamespace = """"; -}} -" : "")}"; - - Assert.Equal(expected, built); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - [InlineData(null)] - public void VisualBasicGenerator(bool? thisAssemblyClass) - { - var info = new AssemblyVersionInfo(); - info.AssemblyCompany = "company"; - info.AssemblyFileVersion = "1.3.1.0"; - info.AssemblyVersion = "1.3.0.0"; - info.CodeLanguage = "vb"; - - if (thisAssemblyClass.HasValue) - { - info.EmitThisAssemblyClass = thisAssemblyClass.GetValueOrDefault(); - } - - var built = info.BuildCode(); - - var expected = $@"'------------------------------------------------------------------------------ -' -' This code was generated by a tool. -' Runtime Version:4.0.30319.42000 -' -' Changes to this file may cause incorrect behavior and will be lost if -' the code is regenerated. -' -'------------------------------------------------------------------------------ - -#Disable Warning CA2243 - - - - -{(thisAssemblyClass.GetValueOrDefault(true) ? $@"#If NETFRAMEWORK Or NETCOREAPP Or NETSTANDARD2_0 Or NETSTANDARD2_1 Then - - -Partial Friend NotInheritable Class ThisAssembly -#ElseIf NETSTANDARD Or NETFRAMEWORK Or NETCOREAPP Then - -Partial Friend NotInheritable Class ThisAssembly -#Else -Partial Friend NotInheritable Class ThisAssembly -#End If - Friend Const AssemblyCompany As String = ""company"" - Friend Const AssemblyFileVersion As String = ""1.3.1.0"" - Friend Const AssemblyVersion As String = ""1.3.0.0"" - Friend Const IsPrerelease As Boolean = False - Friend Const IsPublicRelease As Boolean = False - Friend Const RootNamespace As String = """" -End Class -" : "")}"; - - Assert.Equal(expected, built); - } -} diff --git a/src/NerdBank.GitVersioning.Tests/BuildIntegrationTests.cs b/src/NerdBank.GitVersioning.Tests/BuildIntegrationTests.cs deleted file mode 100644 index 159ee377..00000000 --- a/src/NerdBank.GitVersioning.Tests/BuildIntegrationTests.cs +++ /dev/null @@ -1,1367 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using System.Xml; -using LibGit2Sharp; -using Microsoft.Build.Construction; -using Microsoft.Build.Evaluation; -using Microsoft.Build.Execution; -using Microsoft.Build.Framework; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Nerdbank.GitVersioning; -using Validation; -using Xunit; -using Xunit.Abstractions; -using Version = System.Version; - -[Trait("Engine", "Managed")] -[Collection("Build")] // msbuild sets current directory in the process, so we can't have it be concurrent with other build tests. -public class BuildIntegrationManagedTests : BuildIntegrationTests -{ - public BuildIntegrationManagedTests(ITestOutputHelper logger) - : base(logger) - { - } - - protected override GitContext CreateGitContext(string path, string committish = null) - => GitContext.Create(path, committish, writable: false); - - protected override void ApplyGlobalProperties(IDictionary globalProperties) - => globalProperties["NBGV_GitEngine"] = "Managed"; -} - -[Trait("Engine", "Managed")] -[Collection("Build")] // msbuild sets current directory in the process, so we can't have it be concurrent with other build tests. -public class BuildIntegrationInProjectManagedTests : BuildIntegrationTests -{ - public BuildIntegrationInProjectManagedTests(ITestOutputHelper logger) - : base(logger) - { - } - - protected override GitContext CreateGitContext(string path, string committish = null) - => GitContext.Create(path, committish, writable: false); - - protected override void ApplyGlobalProperties(IDictionary globalProperties) - { - globalProperties["NBGV_GitEngine"] = "Managed"; - globalProperties["NBGV_CacheMode"] = "None"; - } -} - -[Trait("Engine", "LibGit2")] -[Collection("Build")] // msbuild sets current directory in the process, so we can't have it be concurrent with other build tests. -public class BuildIntegrationLibGit2Tests : BuildIntegrationTests -{ - public BuildIntegrationLibGit2Tests(ITestOutputHelper logger) - : base(logger) - { - } - - protected override GitContext CreateGitContext(string path, string committish = null) - => GitContext.Create(path, committish, writable: true); - - protected override void ApplyGlobalProperties(IDictionary globalProperties) - => globalProperties["NBGV_GitEngine"] = "LibGit2"; -} - -public abstract class BuildIntegrationTests : RepoTestBase, IClassFixture -{ - private const string GitVersioningPropsFileName = "NerdBank.GitVersioning.props"; - private const string GitVersioningTargetsFileName = "NerdBank.GitVersioning.targets"; - private const string UnitTestCloudBuildPrefix = "UnitTest: "; - private static readonly string[] ToxicEnvironmentVariablePrefixes = new string[] - { - "APPVEYOR", - "SYSTEM_", - "BUILD_", - "NBGV_GitEngine", - }; - private BuildManager buildManager; - private ProjectCollection projectCollection; - private string projectDirectory; - private ProjectRootElement testProject; - private Dictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - // Set global properties to neutralize environment variables - // that might actually be defined by a CI that is building and running these tests. - { "PublicRelease", string.Empty }, - }; - private Random random; - - public BuildIntegrationTests(ITestOutputHelper logger) - : base(logger) - { - // MSBuildExtensions.LoadMSBuild will be called as part of the base constructor, because this class - // implements the IClassFixture interface. LoadMSBuild will load the MSBuild assemblies. - // This must happen _before_ any method that directly references types in the Microsoft.Build namespace has been called. - // Net, don't init MSBuild-related fields in the constructor, but in a method that is called by the constructor. - this.Init(); - } - - private void Init() - { - int seed = (int)DateTime.Now.Ticks; - this.random = new Random(seed); - this.Logger.WriteLine("Random seed: {0}", seed); - this.buildManager = new BuildManager(); - this.projectCollection = new ProjectCollection(); - this.projectDirectory = Path.Combine(this.RepoPath, "projdir"); - Directory.CreateDirectory(this.projectDirectory); - this.LoadTargetsIntoProjectCollection(); - this.testProject = this.CreateProjectRootElement(this.projectDirectory, "test.prj"); - this.globalProperties.Add("NerdbankGitVersioningTasksPath", Environment.CurrentDirectory + "\\"); - Environment.SetEnvironmentVariable("_NBGV_UnitTest", "true"); - - // Sterilize the test of any environment variables. - foreach (System.Collections.DictionaryEntry variable in Environment.GetEnvironmentVariables()) - { - string name = (string)variable.Key; - if (ToxicEnvironmentVariablePrefixes.Any(toxic => name.StartsWith(toxic, StringComparison.OrdinalIgnoreCase))) - { - this.globalProperties[name] = string.Empty; - } - } - } - - private string CommitIdShort => this.Context.GitCommitId?.Substring(0, VersionOptions.DefaultGitCommitIdShortFixedLength); - - protected abstract void ApplyGlobalProperties(IDictionary globalProperties); - - protected override void Dispose(bool disposing) - { - Environment.SetEnvironmentVariable("_NBGV_UnitTest", string.Empty); - base.Dispose(disposing); - } - - [Fact] - public async Task GetBuildVersion_Returns_BuildVersion_Property() - { - this.WriteVersionFile(); - this.InitializeSourceControl(); - var buildResult = await this.BuildAsync(); - Assert.Equal( - buildResult.BuildVersion, - buildResult.BuildResult.ResultsByTarget[Targets.GetBuildVersion].Items.Single().ItemSpec); - } - - [Fact] - public async Task GetBuildVersion_Without_Git() - { - this.WriteVersionFile("3.4"); - var buildResult = await this.BuildAsync(); - Assert.Equal("3.4", buildResult.BuildVersion); - Assert.Equal("3.4.0", buildResult.AssemblyInformationalVersion); - } - - [Fact] - public async Task GetBuildVersion_WithThreeVersionIntegers() - { - VersionOptions workingCopyVersion = new VersionOptions - { - Version = SemanticVersion.Parse("7.8.9-beta.3"), - SemVer1NumericIdentifierPadding = 1, - }; - this.WriteVersionFile(workingCopyVersion); - this.InitializeSourceControl(); - var buildResult = await this.BuildAsync(); - this.AssertStandardProperties(workingCopyVersion, buildResult); - } - - [Fact] - public async Task GetBuildVersion_Without_Git_HighPrecisionAssemblyVersion() - { - this.WriteVersionFile(new VersionOptions - { - Version = SemanticVersion.Parse("3.4"), - AssemblyVersion = new VersionOptions.AssemblyVersionOptions - { - Precision = VersionOptions.VersionPrecision.Revision, - }, - }); - var buildResult = await this.BuildAsync(); - Assert.Equal("3.4", buildResult.BuildVersion); - Assert.Equal("3.4.0", buildResult.AssemblyInformationalVersion); - } - - [Fact] - public async Task GetBuildVersion_OutsideGit_PointingToGit() - { - // Write a version file to the 'virtualized' repo. - string version = "3.4"; - this.WriteVersionFile(version); - - string repoRelativeProjectPath = this.testProject.FullPath.Substring(this.RepoPath.Length + 1); - - // Update the repo path so we create the 'normal' one elsewhere - this.RepoPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - this.InitializeSourceControl(); - - // Write the same version file to the 'real' repo - this.WriteVersionFile(version); - - // Point the project to the 'real' repo - this.testProject.AddProperty("GitRepoRoot", this.RepoPath); - this.testProject.AddProperty("ProjectPathRelativeToGitRepoRoot", repoRelativeProjectPath); - - var buildResult = await this.BuildAsync(); - - var workingCopyVersion = VersionOptions.FromVersion(new Version(version)); - - this.AssertStandardProperties(workingCopyVersion, buildResult); - } - - [Fact] - public async Task GetBuildVersion_In_Git_But_Without_Commits() - { - Repository.Init(this.RepoPath); - var repo = new Repository(this.RepoPath); // do not assign Repo property to avoid commits being generated later - this.WriteVersionFile("3.4"); - Assumes.False(repo.Head.Commits.Any()); // verification that the test is doing what it claims - var buildResult = await this.BuildAsync(); - Assert.Equal("3.4.0.0", buildResult.BuildVersion); - Assert.Equal("3.4.0", buildResult.AssemblyInformationalVersion); - } - - [Fact] - public async Task GetBuildVersion_In_Git_But_Head_Lacks_VersionFile() - { - Repository.Init(this.RepoPath); - var repo = new Repository(this.RepoPath); // do not assign Repo property to avoid commits being generated later - repo.Commit("empty", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); - this.WriteVersionFile("3.4"); - Assumes.True(repo.Index[VersionFile.JsonFileName] is null); - var buildResult = await this.BuildAsync(); - Assert.Equal("3.4.0." + this.GetVersion().Revision, buildResult.BuildVersion); - Assert.Equal("3.4.0+" + repo.Head.Tip.Id.Sha.Substring(0, VersionOptions.DefaultGitCommitIdShortFixedLength), buildResult.AssemblyInformationalVersion); - } - - [Fact] - public async Task GetBuildVersion_In_Git_But_WorkingCopy_Has_Changes() - { - const string majorMinorVersion = "5.8"; - const string prerelease = ""; - - this.WriteVersionFile(majorMinorVersion, prerelease); - this.InitializeSourceControl(); - var workingCopyVersion = VersionOptions.FromVersion(new Version("6.0")); - this.Context.VersionFile.SetVersion(this.RepoPath, workingCopyVersion); - var buildResult = await this.BuildAsync(); - this.AssertStandardProperties(workingCopyVersion, buildResult); - } - - [Fact] - public async Task GetBuildVersion_In_Git_No_VersionFile_At_All() - { - Repository.Init(this.RepoPath); - var repo = new Repository(this.RepoPath); // do not assign Repo property to avoid commits being generated later - repo.Commit("empty", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); - var buildResult = await this.BuildAsync(); - Assert.Equal("0.0.0." + this.GetVersion().Revision, buildResult.BuildVersion); - Assert.Equal("0.0.0+" + repo.Head.Tip.Id.Sha.Substring(0, VersionOptions.DefaultGitCommitIdShortFixedLength), buildResult.AssemblyInformationalVersion); - } - - [Fact] - public async Task GetBuildVersion_In_Git_With_Version_File_In_Subdirectory_Works() - { - const string majorMinorVersion = "5.8"; - const string prerelease = ""; - const string subdirectory = "projdir"; - - this.WriteVersionFile(majorMinorVersion, prerelease, subdirectory); - this.InitializeSourceControl(); - this.AddCommits(this.random.Next(15)); - var buildResult = await this.BuildAsync(); - this.AssertStandardProperties(VersionOptions.FromVersion(new Version(majorMinorVersion)), buildResult, subdirectory); - } - - [Fact] - public async Task GetBuildVersion_In_Git_With_Version_File_In_Root_And_Subdirectory_Works() - { - var rootVersionSpec = new VersionOptions - { - Version = SemanticVersion.Parse("14.1"), - AssemblyVersion = new VersionOptions.AssemblyVersionOptions(new Version(14, 0)), - }; - var subdirVersionSpec = new VersionOptions { Version = SemanticVersion.Parse("11.0") }; - const string subdirectory = "projdir"; - - this.WriteVersionFile(rootVersionSpec); - this.WriteVersionFile(subdirVersionSpec, subdirectory); - this.InitializeSourceControl(); - this.AddCommits(this.random.Next(15)); - var buildResult = await this.BuildAsync(); - this.AssertStandardProperties(subdirVersionSpec, buildResult, subdirectory); - } - - [Fact] - public async Task GetBuildVersion_In_Git_With_Version_File_In_Root_And_Project_In_Root_Works() - { - var rootVersionSpec = new VersionOptions - { - Version = SemanticVersion.Parse("14.1"), - AssemblyVersion = new VersionOptions.AssemblyVersionOptions(new Version(14, 0)), - }; - - this.WriteVersionFile(rootVersionSpec); - this.InitializeSourceControl(); - this.AddCommits(this.random.Next(15)); - this.testProject = this.CreateProjectRootElement(this.RepoPath, "root.proj"); - var buildResult = await this.BuildAsync(); - this.AssertStandardProperties(rootVersionSpec, buildResult); - } - - [Fact] - public async Task GetBuildVersion_StablePreRelease() - { - const string majorMinorVersion = "5.8"; - const string prerelease = ""; - - this.WriteVersionFile(majorMinorVersion, prerelease); - this.InitializeSourceControl(); - this.AddCommits(this.random.Next(15)); - var buildResult = await this.BuildAsync(); - this.AssertStandardProperties(VersionOptions.FromVersion(new Version(majorMinorVersion)), buildResult); - } - - [Fact] - public async Task GetBuildVersion_StableRelease() - { - const string majorMinorVersion = "5.8"; - const string prerelease = ""; - - this.WriteVersionFile(majorMinorVersion, prerelease); - this.InitializeSourceControl(); - this.AddCommits(this.random.Next(15)); - this.globalProperties["PublicRelease"] = "true"; - var buildResult = await this.BuildAsync(); - this.AssertStandardProperties(VersionOptions.FromVersion(new Version(majorMinorVersion)), buildResult); - - Version version = this.GetVersion(); - Assert.Equal($"{version.Major}.{version.Minor}.{buildResult.GitVersionHeight}", buildResult.NuGetPackageVersion); - } - - [Fact] - public async Task GetBuildVersion_UnstablePreRelease() - { - const string majorMinorVersion = "5.8"; - const string prerelease = "-beta"; - - this.WriteVersionFile(majorMinorVersion, prerelease); - this.InitializeSourceControl(); - this.AddCommits(this.random.Next(15)); - var buildResult = await this.BuildAsync(); - this.AssertStandardProperties(VersionOptions.FromVersion(new Version(majorMinorVersion), prerelease), buildResult); - } - - [Fact] - public async Task GetBuildVersion_UnstableRelease() - { - const string majorMinorVersion = "5.8"; - const string prerelease = "-beta"; - - this.WriteVersionFile(majorMinorVersion, prerelease); - this.InitializeSourceControl(); - this.AddCommits(this.random.Next(15)); - this.globalProperties["PublicRelease"] = "true"; - var buildResult = await this.BuildAsync(); - this.AssertStandardProperties(VersionOptions.FromVersion(new Version(majorMinorVersion), prerelease), buildResult); - } - - [Fact] - public async Task GetBuildVersion_CustomAssemblyVersion() - { - this.WriteVersionFile("14.0"); - this.InitializeSourceControl(); - var versionOptions = new VersionOptions - { - Version = new SemanticVersion(new Version(14, 1)), - AssemblyVersion = new VersionOptions.AssemblyVersionOptions(new Version(14, 0)), - }; - this.WriteVersionFile(versionOptions); - var buildResult = await this.BuildAsync(); - this.AssertStandardProperties(versionOptions, buildResult); - } - - [Theory] - [InlineData(VersionOptions.VersionPrecision.Major)] - [InlineData(VersionOptions.VersionPrecision.Build)] - [InlineData(VersionOptions.VersionPrecision.Revision)] - public async Task GetBuildVersion_CustomAssemblyVersionWithPrecision(VersionOptions.VersionPrecision precision) - { - var versionOptions = new VersionOptions - { - Version = new SemanticVersion("14.1"), - AssemblyVersion = new VersionOptions.AssemblyVersionOptions - { - Version = new Version("15.2"), - Precision = precision, - }, - }; - this.WriteVersionFile(versionOptions); - this.InitializeSourceControl(); - var buildResult = await this.BuildAsync(); - this.AssertStandardProperties(versionOptions, buildResult); - } - - [Theory] - [InlineData(VersionOptions.VersionPrecision.Major)] - [InlineData(VersionOptions.VersionPrecision.Build)] - [InlineData(VersionOptions.VersionPrecision.Revision)] - public async Task GetBuildVersion_CustomAssemblyVersionPrecision(VersionOptions.VersionPrecision precision) - { - var versionOptions = new VersionOptions - { - Version = new SemanticVersion("14.1"), - AssemblyVersion = new VersionOptions.AssemblyVersionOptions - { - Precision = precision, - }, - }; - this.WriteVersionFile(versionOptions); - this.InitializeSourceControl(); - var buildResult = await this.BuildAsync(); - this.AssertStandardProperties(versionOptions, buildResult); - } - - [Fact] - public async Task GetBuildVersion_CustomBuildNumberOffset() - { - this.WriteVersionFile("14.0"); - this.InitializeSourceControl(); - var versionOptions = new VersionOptions - { - Version = new SemanticVersion(new Version(14, 1)), - VersionHeightOffset = 5, - }; - this.WriteVersionFile(versionOptions); - var buildResult = await this.BuildAsync(); - this.AssertStandardProperties(versionOptions, buildResult); - } - - [Fact] - public async Task GetBuildVersion_OverrideBuildNumberOffset() - { - this.WriteVersionFile("14.0"); - this.InitializeSourceControl(); - var versionOptions = new VersionOptions - { - Version = new SemanticVersion(new Version(14, 1)) - }; - this.WriteVersionFile(versionOptions); - this.testProject.AddProperty("OverrideBuildNumberOffset", "10"); - var buildResult = await this.BuildAsync(); - Assert.StartsWith("14.1.11.", buildResult.AssemblyFileVersion); - } - - [Fact] - public async Task GetBuildVersion_Minus1BuildOffset_NotYetCommitted() - { - this.WriteVersionFile("14.0"); - this.InitializeSourceControl(); - var versionOptions = new VersionOptions - { - Version = new SemanticVersion(new Version(14, 1)), - VersionHeightOffset = -1, - }; - this.Context.VersionFile.SetVersion(this.RepoPath, versionOptions); - var buildResult = await this.BuildAsync(); - this.AssertStandardProperties(versionOptions, buildResult); - } - - [Theory] - [InlineData(0)] - [InlineData(21)] - public async Task GetBuildVersion_BuildNumberSpecifiedInVersionJson(int buildNumber) - { - var versionOptions = new VersionOptions - { - Version = SemanticVersion.Parse("14.0." + buildNumber), - }; - this.WriteVersionFile(versionOptions); - this.InitializeSourceControl(); - var buildResult = await this.BuildAsync(); - this.AssertStandardProperties(versionOptions, buildResult); - } - - [Fact] - public async Task PublicRelease_RegEx_Unsatisfied() - { - var versionOptions = new VersionOptions - { - Version = SemanticVersion.Parse("1.0"), - PublicReleaseRefSpec = new string[] { "^refs/heads/release$" }, - }; - this.WriteVersionFile(versionOptions); - this.InitializeSourceControl(); - - // Just build "master", which doesn't conform to the regex. - var buildResult = await this.BuildAsync(); - Assert.False(buildResult.PublicRelease); - this.AssertStandardProperties(versionOptions, buildResult); - } - - public static IEnumerable CloudBuildOfBranch(string branchName) - { - return new object[][] - { - new object[] { CloudBuild.AppVeyor.SetItem("APPVEYOR_REPO_BRANCH", branchName) }, - new object[] { CloudBuild.VSTS.SetItem("BUILD_SOURCEBRANCH", $"refs/heads/{branchName}") }, - new object[] { CloudBuild.VSTS.SetItem("BUILD_SOURCEBRANCH", $"refs/tags/{branchName}") }, - new object[] { CloudBuild.Teamcity.SetItem("BUILD_GIT_BRANCH", $"refs/heads/{branchName}") }, - new object[] { CloudBuild.Teamcity.SetItem("BUILD_GIT_BRANCH", $"refs/tags/{branchName}") }, - }; - } - - [Theory] - [MemberData(nameof(CloudBuildOfBranch), "release")] - public async Task PublicRelease_RegEx_SatisfiedByCI(IReadOnlyDictionary serverProperties) - { - var versionOptions = new VersionOptions - { - Version = SemanticVersion.Parse("1.0"), - PublicReleaseRefSpec = new string[] - { - "^refs/heads/release$", - "^refs/tags/release$" - }, - }; - this.WriteVersionFile(versionOptions); - this.InitializeSourceControl(); - - // Don't actually switch the checked out branch in git. CI environment variables - // should take precedence over actual git configuration. (Why? because these variables may - // retain information about which tag was checked out on a detached head). - using (ApplyEnvironmentVariables(serverProperties)) - { - var buildResult = await this.BuildAsync(); - Assert.True(buildResult.PublicRelease); - this.AssertStandardProperties(versionOptions, buildResult); - } - } - - public static object[][] CloudBuildVariablesData - { - get - { - return new object[][] - { - new object[] { CloudBuild.VSTS, "##vso[task.setvariable variable={NAME};]{VALUE}", false }, - new object[] { CloudBuild.VSTS, "##vso[task.setvariable variable={NAME};]{VALUE}", true }, - }; - } - } - - [Theory] - [Trait("TestCategory", "FailsOnAzurePipelines")] - [MemberData(nameof(CloudBuildVariablesData))] - public async Task CloudBuildVariables_SetInCI(IReadOnlyDictionary properties, string expectedMessage, bool setAllVariables) - { - using (ApplyEnvironmentVariables(properties)) - { - string keyName = "n1"; - string value = "v1"; - this.testProject.AddItem("CloudBuildVersionVars", keyName, new Dictionary { { "Value", value } }); - - string alwaysExpectedMessage = UnitTestCloudBuildPrefix + expectedMessage - .Replace("{NAME}", keyName) - .Replace("{VALUE}", value); - - var versionOptions = new VersionOptions - { - Version = SemanticVersion.Parse("1.0"), - CloudBuild = new VersionOptions.CloudBuildOptions { SetAllVariables = setAllVariables, SetVersionVariables = true }, - }; - this.WriteVersionFile(versionOptions); - this.InitializeSourceControl(); - - var buildResult = await this.BuildAsync(); - this.AssertStandardProperties(versionOptions, buildResult); - - // Assert GitBuildVersion was set - string conditionallyExpectedMessage = UnitTestCloudBuildPrefix + expectedMessage - .Replace("{NAME}", "GitBuildVersion") - .Replace("{VALUE}", buildResult.BuildVersion); - Assert.Contains(alwaysExpectedMessage, buildResult.LoggedEvents.Select(e => e.Message.TrimEnd())); - Assert.Contains(conditionallyExpectedMessage, buildResult.LoggedEvents.Select(e => e.Message.TrimEnd())); - - // Assert GitBuildVersionSimple was set - conditionallyExpectedMessage = UnitTestCloudBuildPrefix + expectedMessage - .Replace("{NAME}", "GitBuildVersionSimple") - .Replace("{VALUE}", buildResult.BuildVersionSimple); - Assert.Contains(alwaysExpectedMessage, buildResult.LoggedEvents.Select(e => e.Message.TrimEnd())); - Assert.Contains(conditionallyExpectedMessage, buildResult.LoggedEvents.Select(e => e.Message.TrimEnd())); - - // Assert that project properties are also set. - Assert.Equal(buildResult.BuildVersion, buildResult.GitBuildVersion); - Assert.Equal(buildResult.BuildVersionSimple, buildResult.GitBuildVersionSimple); - Assert.Equal(buildResult.AssemblyInformationalVersion, buildResult.GitAssemblyInformationalVersion); - - if (setAllVariables) - { - // Assert that some project properties were set as build properties prefaced with "NBGV_". - Assert.Equal(buildResult.GitCommitIdShort, buildResult.NBGV_GitCommitIdShort); - Assert.Equal(buildResult.NuGetPackageVersion, buildResult.NBGV_NuGetPackageVersion); - } - else - { - // Assert that the NBGV_ prefixed properties are *not* set. - Assert.Equal(string.Empty, buildResult.NBGV_GitCommitIdShort); - Assert.Equal(string.Empty, buildResult.NBGV_NuGetPackageVersion); - } - - // Assert that env variables were also set in context of the build. - Assert.Contains(buildResult.LoggedEvents, e => string.Equals(e.Message, $"n1=v1", StringComparison.OrdinalIgnoreCase)); - - versionOptions.CloudBuild.SetVersionVariables = false; - this.WriteVersionFile(versionOptions); - this.SetContextToHead(); - buildResult = await this.BuildAsync(); - this.AssertStandardProperties(versionOptions, buildResult); - - // Assert GitBuildVersion was not set - conditionallyExpectedMessage = UnitTestCloudBuildPrefix + expectedMessage - .Replace("{NAME}", "GitBuildVersion") - .Replace("{VALUE}", buildResult.BuildVersion); - Assert.Contains(alwaysExpectedMessage, buildResult.LoggedEvents.Select(e => e.Message.TrimEnd())); - Assert.DoesNotContain(conditionallyExpectedMessage, buildResult.LoggedEvents.Select(e => e.Message.TrimEnd())); - Assert.NotEqual(buildResult.BuildVersion, buildResult.BuildResult.ProjectStateAfterBuild.GetPropertyValue("GitBuildVersion")); - - // Assert GitBuildVersionSimple was not set - conditionallyExpectedMessage = UnitTestCloudBuildPrefix + expectedMessage - .Replace("{NAME}", "GitBuildVersionSimple") - .Replace("{VALUE}", buildResult.BuildVersionSimple); - Assert.Contains(alwaysExpectedMessage, buildResult.LoggedEvents.Select(e => e.Message.TrimEnd())); - Assert.DoesNotContain(conditionallyExpectedMessage, buildResult.LoggedEvents.Select(e => e.Message.TrimEnd())); - Assert.NotEqual(buildResult.BuildVersionSimple, buildResult.BuildResult.ProjectStateAfterBuild.GetPropertyValue("GitBuildVersionSimple")); - } - } - - private static VersionOptions BuildNumberVersionOptionsBasis - { - get - { - return new VersionOptions - { - Version = SemanticVersion.Parse("1.0"), - CloudBuild = new VersionOptions.CloudBuildOptions - { - BuildNumber = new VersionOptions.CloudBuildNumberOptions - { - Enabled = true, - IncludeCommitId = new VersionOptions.CloudBuildNumberCommitIdOptions(), - } - }, - }; - } - } - - public static object[][] BuildNumberData - { - get - { - return new object[][] - { - new object[] { BuildNumberVersionOptionsBasis, CloudBuild.VSTS, "##vso[build.updatebuildnumber]{CLOUDBUILDNUMBER}" }, - }; - } - } - - [Theory] - [MemberData(nameof(BuildNumberData))] - public async Task BuildNumber_SetInCI(VersionOptions versionOptions, IReadOnlyDictionary properties, string expectedBuildNumberMessage) - { - this.WriteVersionFile(versionOptions); - this.InitializeSourceControl(); - using (ApplyEnvironmentVariables(properties)) - { - var buildResult = await this.BuildAsync(); - this.AssertStandardProperties(versionOptions, buildResult); - expectedBuildNumberMessage = expectedBuildNumberMessage.Replace("{CLOUDBUILDNUMBER}", buildResult.CloudBuildNumber); - Assert.Contains(UnitTestCloudBuildPrefix + expectedBuildNumberMessage, buildResult.LoggedEvents.Select(e => e.Message.TrimEnd())); - } - - versionOptions.CloudBuild.BuildNumber.Enabled = false; - this.WriteVersionFile(versionOptions); - using (ApplyEnvironmentVariables(properties)) - { - var buildResult = await this.BuildAsync(); - this.AssertStandardProperties(versionOptions, buildResult); - expectedBuildNumberMessage = expectedBuildNumberMessage.Replace("{CLOUDBUILDNUMBER}", buildResult.CloudBuildNumber); - Assert.DoesNotContain(UnitTestCloudBuildPrefix + expectedBuildNumberMessage, buildResult.LoggedEvents.Select(e => e.Message.TrimEnd())); - } - } - - [Theory] - [PairwiseData] - public async Task BuildNumber_VariousOptions(bool isPublic, VersionOptions.CloudBuildNumberCommitWhere where, VersionOptions.CloudBuildNumberCommitWhen when, [CombinatorialValues(0, 1, 2)] int extraBuildMetadataCount, [CombinatorialValues(1, 2)] int semVer) - { - var versionOptions = BuildNumberVersionOptionsBasis; - versionOptions.CloudBuild.BuildNumber.IncludeCommitId.Where = where; - versionOptions.CloudBuild.BuildNumber.IncludeCommitId.When = when; - versionOptions.NuGetPackageVersion = new VersionOptions.NuGetPackageVersionOptions - { - SemVer = semVer, - }; - this.WriteVersionFile(versionOptions); - this.InitializeSourceControl(); - - this.globalProperties["PublicRelease"] = isPublic.ToString(); - for (int i = 0; i < extraBuildMetadataCount; i++) - { - this.testProject.AddItem("BuildMetadata", $"A{i}"); - } - - var buildResult = await this.BuildAsync(); - this.AssertStandardProperties(versionOptions, buildResult); - } - - [Fact] - public void GitLab_BuildTag() - { - // Based on the values defined in https://docs.gitlab.com/ee/ci/variables/#syntax-of-environment-variables-in-job-scripts - using (ApplyEnvironmentVariables( - CloudBuild.SuppressEnvironment.SetItems( - new Dictionary() - { - { "CI_COMMIT_TAG", "1.0.0" }, - { "CI_COMMIT_SHA", "1ecfd275763eff1d6b4844ea3168962458c9f27a" }, - { "GITLAB_CI", "true" }, - { "SYSTEM_TEAMPROJECTID", string.Empty } - }))) - { - var activeCloudBuild = Nerdbank.GitVersioning.CloudBuild.Active; - Assert.NotNull(activeCloudBuild); - Assert.Null(activeCloudBuild.BuildingBranch); - Assert.Equal("refs/tags/1.0.0", activeCloudBuild.BuildingTag); - Assert.Equal("1ecfd275763eff1d6b4844ea3168962458c9f27a", activeCloudBuild.GitCommitId); - Assert.True(activeCloudBuild.IsApplicable); - Assert.False(activeCloudBuild.IsPullRequest); - } - } - - [Fact] - public void GitLab_BuildBranch() - { - // Based on the values defined in https://docs.gitlab.com/ee/ci/variables/#syntax-of-environment-variables-in-job-scripts - using (ApplyEnvironmentVariables( - CloudBuild.SuppressEnvironment.SetItems( - new Dictionary() - { - { "CI_COMMIT_REF_NAME", "master" }, - { "CI_COMMIT_SHA", "1ecfd275763eff1d6b4844ea3168962458c9f27a" }, - { "GITLAB_CI", "true" }, - }))) - { - var activeCloudBuild = Nerdbank.GitVersioning.CloudBuild.Active; - Assert.NotNull(activeCloudBuild); - Assert.Equal("refs/heads/master", activeCloudBuild.BuildingBranch); - Assert.Null(activeCloudBuild.BuildingTag); - Assert.Equal("1ecfd275763eff1d6b4844ea3168962458c9f27a", activeCloudBuild.GitCommitId); - Assert.True(activeCloudBuild.IsApplicable); - Assert.False(activeCloudBuild.IsPullRequest); - } - } - - [Fact] - public async Task PublicRelease_RegEx_SatisfiedByCheckedOutBranch() - { - var versionOptions = new VersionOptions - { - Version = SemanticVersion.Parse("1.0"), - PublicReleaseRefSpec = new string[] { "^refs/heads/release$" }, - }; - this.WriteVersionFile(versionOptions); - this.InitializeSourceControl(); - - using (ApplyEnvironmentVariables(CloudBuild.SuppressEnvironment)) - { - // Check out a branch that conforms. - var releaseBranch = this.LibGit2Repository.CreateBranch("release"); - Commands.Checkout(this.LibGit2Repository, releaseBranch); - var buildResult = await this.BuildAsync(); - Assert.True(buildResult.PublicRelease); - this.AssertStandardProperties(versionOptions, buildResult); - } - } - - // This test builds projects using 'classic' MSBuild projects, which target net45. - // This is not supported on Linux. - [WindowsTheory] - [PairwiseData] - public async Task AssemblyInfo(bool isVB, bool includeNonVersionAttributes, bool gitRepo, bool isPrerelease, bool isPublicRelease) - { - this.WriteVersionFile(prerelease: isPrerelease ? "-beta" : string.Empty); - if (gitRepo) - { - this.InitializeSourceControl(); - } - - if (isVB) - { - this.MakeItAVBProject(); - } - - if (includeNonVersionAttributes) - { - this.testProject.AddProperty("NBGV_EmitNonVersionCustomAttributes", "true"); - } - - this.globalProperties["PublicRelease"] = isPublicRelease ? "true" : "false"; - - var result = await this.BuildAsync("Build", logVerbosity: LoggerVerbosity.Minimal); - string assemblyPath = result.BuildResult.ProjectStateAfterBuild.GetPropertyValue("TargetPath"); - string versionFileContent = File.ReadAllText(Path.Combine(this.projectDirectory, result.BuildResult.ProjectStateAfterBuild.GetPropertyValue("VersionSourceFile"))); - this.Logger.WriteLine(versionFileContent); - - var assembly = Assembly.LoadFile(assemblyPath); - - var assemblyFileVersion = assembly.GetCustomAttribute(); - var assemblyInformationalVersion = assembly.GetCustomAttribute(); - var assemblyTitle = assembly.GetCustomAttribute(); - var assemblyProduct = assembly.GetCustomAttribute(); - var assemblyCompany = assembly.GetCustomAttribute(); - var assemblyCopyright = assembly.GetCustomAttribute(); - var thisAssemblyClass = assembly.GetType("ThisAssembly") ?? assembly.GetType("TestNamespace.ThisAssembly"); - Assert.NotNull(thisAssemblyClass); - - Assert.Equal(new Version(result.AssemblyVersion), assembly.GetName().Version); - Assert.Equal(result.AssemblyFileVersion, assemblyFileVersion.Version); - Assert.Equal(result.AssemblyInformationalVersion, assemblyInformationalVersion.InformationalVersion); - if (includeNonVersionAttributes) - { - Assert.Equal(result.AssemblyTitle, assemblyTitle.Title); - Assert.Equal(result.AssemblyProduct, assemblyProduct.Product); - Assert.Equal(result.AssemblyCompany, assemblyCompany.Company); - Assert.Equal(result.AssemblyCopyright, assemblyCopyright.Copyright); - } - else - { - Assert.Null(assemblyTitle); - Assert.Null(assemblyProduct); - Assert.Null(assemblyCompany); - Assert.Null(assemblyCopyright); - } - - const BindingFlags fieldFlags = BindingFlags.Static | BindingFlags.NonPublic; - Assert.Equal(result.AssemblyVersion, thisAssemblyClass.GetField("AssemblyVersion", fieldFlags).GetValue(null)); - Assert.Equal(result.AssemblyFileVersion, thisAssemblyClass.GetField("AssemblyFileVersion", fieldFlags).GetValue(null)); - Assert.Equal(result.AssemblyInformationalVersion, thisAssemblyClass.GetField("AssemblyInformationalVersion", fieldFlags).GetValue(null)); - Assert.Equal(result.AssemblyName, thisAssemblyClass.GetField("AssemblyName", fieldFlags).GetValue(null)); - Assert.Equal(result.RootNamespace, thisAssemblyClass.GetField("RootNamespace", fieldFlags).GetValue(null)); - Assert.Equal(result.AssemblyConfiguration, thisAssemblyClass.GetField("AssemblyConfiguration", fieldFlags).GetValue(null)); - Assert.Equal(result.AssemblyTitle, thisAssemblyClass.GetField("AssemblyTitle", fieldFlags)?.GetValue(null)); - Assert.Equal(result.AssemblyProduct, thisAssemblyClass.GetField("AssemblyProduct", fieldFlags)?.GetValue(null)); - Assert.Equal(result.AssemblyCompany, thisAssemblyClass.GetField("AssemblyCompany", fieldFlags)?.GetValue(null)); - Assert.Equal(result.AssemblyCopyright, thisAssemblyClass.GetField("AssemblyCopyright", fieldFlags)?.GetValue(null)); - Assert.Equal(result.GitCommitId, thisAssemblyClass.GetField("GitCommitId", fieldFlags)?.GetValue(null) ?? string.Empty); - Assert.Equal(result.PublicRelease, thisAssemblyClass.GetField("IsPublicRelease", fieldFlags)?.GetValue(null)); - Assert.Equal(!string.IsNullOrEmpty(result.PrereleaseVersion), thisAssemblyClass.GetField("IsPrerelease", fieldFlags)?.GetValue(null)); - - if (gitRepo) - { - Assert.True(long.TryParse(result.GitCommitDateTicks, out _), $"Invalid value for GitCommitDateTicks: '{result.GitCommitDateTicks}'"); - var gitCommitDate = new DateTime(long.Parse(result.GitCommitDateTicks), DateTimeKind.Utc); - Assert.Equal(gitCommitDate, thisAssemblyClass.GetProperty("GitCommitDate", fieldFlags)?.GetValue(null) ?? thisAssemblyClass.GetField("GitCommitDate", fieldFlags)?.GetValue(null) ?? string.Empty); - } - else - { - Assert.Empty(result.GitCommitDateTicks); - Assert.Null(thisAssemblyClass.GetProperty("GitCommitDate", fieldFlags)); - } - - // Verify that it doesn't have key fields - Assert.Null(thisAssemblyClass.GetField("PublicKey", fieldFlags)); - Assert.Null(thisAssemblyClass.GetField("PublicKeyToken", fieldFlags)); - } - - // TODO: add key container test. - [Theory] - [InlineData("keypair.snk", false)] - [InlineData("public.snk", true)] - [InlineData("protectedPair.pfx", true)] - public async Task AssemblyInfo_HasKeyData(string keyFile, bool delaySigned) - { - TestUtilities.ExtractEmbeddedResource($@"Keys\{keyFile}", Path.Combine(this.projectDirectory, keyFile)); - this.testProject.AddProperty("SignAssembly", "true"); - this.testProject.AddProperty("AssemblyOriginatorKeyFile", keyFile); - this.testProject.AddProperty("DelaySign", delaySigned.ToString()); - - this.WriteVersionFile(); - var result = await this.BuildAsync(Targets.GenerateAssemblyNBGVVersionInfo, logVerbosity: LoggerVerbosity.Minimal); - string versionCsContent = File.ReadAllText( - Path.GetFullPath( - Path.Combine( - this.projectDirectory, - result.BuildResult.ProjectStateAfterBuild.GetPropertyValue("VersionSourceFile")))); - this.Logger.WriteLine(versionCsContent); - - var sourceFile = CSharpSyntaxTree.ParseText(versionCsContent); - var syntaxTree = await sourceFile.GetRootAsync(); - var fields = syntaxTree.DescendantNodes().OfType(); - - var publicKeyField = (LiteralExpressionSyntax)fields.SingleOrDefault(f => f.Identifier.ValueText == "PublicKey")?.Initializer.Value; - var publicKeyTokenField = (LiteralExpressionSyntax)fields.SingleOrDefault(f => f.Identifier.ValueText == "PublicKeyToken")?.Initializer.Value; - if (Path.GetExtension(keyFile) == ".pfx") - { - // No support for PFX (yet anyway), since they're encrypted. - // Note for future: I think by this point, the user has typically already decrypted - // the PFX and stored the key pair in a key container. If we knew how to find which one, - // we could perhaps divert to that. - Assert.Null(publicKeyField); - Assert.Null(publicKeyTokenField); - } - else - { - Assert.Equal( - "002400000480000094000000060200000024000052534131000400000100010067cea773679e0ecc114b7e1d442466a90bf77c755811a0d3962a546ed716525b6508abf9f78df132ffd3fb75fe604b3961e39c52d5dfc0e6c1fb233cb4fb56b1a9e3141513b23bea2cd156cb2ef7744e59ba6b663d1f5b2f9449550352248068e85b61c68681a6103cad91b3bf7a4b50d2fabf97e1d97ac34db65b25b58cd0dc", - publicKeyField?.Token.ValueText); - Assert.Equal("ca2d1515679318f5", publicKeyTokenField?.Token.ValueText); - } - } - - [Fact] - [Trait("TestCategory", "FailsOnAzurePipelines")] - public async Task AssemblyInfo_IncrementalBuild() - { - this.WriteVersionFile(prerelease: "-beta"); - await this.BuildAsync("Build", logVerbosity: LoggerVerbosity.Minimal); - this.WriteVersionFile(prerelease: "-rc"); // two characters SHORTER, to test file truncation. - await this.BuildAsync("Build", logVerbosity: LoggerVerbosity.Minimal); - } - - /// - /// Emulate a project with an unsupported language, and verify that - /// one warning is emitted because the assembly info file couldn't be generated. - /// - [Fact] - public async Task AssemblyInfo_NotProducedWithoutCodeDomProvider() - { - var propertyGroup = this.testProject.CreatePropertyGroupElement(); - this.testProject.AppendChild(propertyGroup); - propertyGroup.AddProperty("Language", "NoCodeDOMProviderForThisLanguage"); - - this.WriteVersionFile(); - var result = await this.BuildAsync(Targets.GenerateAssemblyNBGVVersionInfo, logVerbosity: LoggerVerbosity.Minimal, assertSuccessfulBuild: false); - Assert.Equal(BuildResultCode.Failure, result.BuildResult.OverallResult); - string versionCsFilePath = Path.Combine(this.projectDirectory, result.BuildResult.ProjectStateAfterBuild.GetPropertyValue("VersionSourceFile")); - Assert.False(File.Exists(versionCsFilePath)); - Assert.Single(result.LoggedEvents.OfType()); - } - - /// - /// Emulate a project with an unsupported language, and verify that - /// no errors are emitted because the target is skipped. - /// - [Fact] - public async Task AssemblyInfo_Suppressed() - { - var propertyGroup = this.testProject.CreatePropertyGroupElement(); - this.testProject.AppendChild(propertyGroup); - propertyGroup.AddProperty("Language", "NoCodeDOMProviderForThisLanguage"); - propertyGroup.AddProperty(Properties.GenerateAssemblyVersionInfo, "false"); - - this.WriteVersionFile(); - var result = await this.BuildAsync(Targets.GenerateAssemblyNBGVVersionInfo, logVerbosity: LoggerVerbosity.Minimal); - string versionCsFilePath = Path.Combine(this.projectDirectory, result.BuildResult.ProjectStateAfterBuild.GetPropertyValue("VersionSourceFile")); - Assert.False(File.Exists(versionCsFilePath)); - Assert.Empty(result.LoggedEvents.OfType()); - Assert.Empty(result.LoggedEvents.OfType()); - } - - /// - /// Emulate a project with an unsupported language, and verify that - /// no errors are emitted because the target is skipped. - /// - [Fact] - public async Task AssemblyInfo_SuppressedImplicitlyByTargetExt() - { - var propertyGroup = this.testProject.CreatePropertyGroupElement(); - this.testProject.InsertAfterChild(propertyGroup, this.testProject.Imports.First()); // insert just after the Common.Targets import. - propertyGroup.AddProperty("Language", "NoCodeDOMProviderForThisLanguage"); - propertyGroup.AddProperty("TargetExt", ".notdll"); - - this.WriteVersionFile(); - var result = await this.BuildAsync(Targets.GenerateAssemblyNBGVVersionInfo, logVerbosity: LoggerVerbosity.Minimal); - string versionCsFilePath = Path.Combine(this.projectDirectory, result.BuildResult.ProjectStateAfterBuild.GetPropertyValue("VersionSourceFile")); - Assert.False(File.Exists(versionCsFilePath)); - Assert.Empty(result.LoggedEvents.OfType()); - Assert.Empty(result.LoggedEvents.OfType()); - } - - protected override GitContext CreateGitContext(string path, string committish = null) => throw new NotImplementedException(); - -#if !NETCOREAPP - /// - /// Create a native resource .dll and verify that its version - /// information is set correctly. - /// - [Fact] - public async Task NativeVersionInfo_CreateNativeResourceDll() - { - this.testProject = this.CreateNativeProjectRootElement(this.projectDirectory, "test.vcxproj"); - this.WriteVersionFile(); - var result = await this.BuildAsync(Targets.Build, logVerbosity: LoggerVerbosity.Minimal); - Assert.Empty(result.LoggedEvents.OfType()); - - string targetFile = Path.Combine(this.projectDirectory, result.BuildResult.ProjectStateAfterBuild.GetPropertyValue("TargetPath")); - Assert.True(File.Exists(targetFile)); - - var fileInfo = FileVersionInfo.GetVersionInfo(targetFile); - Assert.Equal("1.2", fileInfo.FileVersion); - Assert.Equal("1.2.0", fileInfo.ProductVersion); - Assert.Equal("test", fileInfo.InternalName); - Assert.Equal("NerdBank", fileInfo.CompanyName); - Assert.Equal($"Copyright (c) {DateTime.Now.Year}. All rights reserved.", fileInfo.LegalCopyright); - } -#endif - - private static Version GetExpectedAssemblyVersion(VersionOptions versionOptions, Version version) - { - // Function should be very similar to VersionOracle.GetAssemblyVersion() - var assemblyVersion = (versionOptions?.AssemblyVersion?.Version ?? versionOptions.Version.Version).EnsureNonNegativeComponents(); - - if (versionOptions?.AssemblyVersion?.Version is null) - { - VersionOptions.VersionPrecision precision = versionOptions?.AssemblyVersion?.Precision ?? VersionOptions.DefaultVersionPrecision; - assemblyVersion = version; - - assemblyVersion = new Version( - assemblyVersion.Major, - precision >= VersionOptions.VersionPrecision.Minor ? assemblyVersion.Minor : 0, - precision >= VersionOptions.VersionPrecision.Build ? assemblyVersion.Build : 0, - precision >= VersionOptions.VersionPrecision.Revision ? assemblyVersion.Revision : 0); - } - - return assemblyVersion; - } - - private static RestoreEnvironmentVariables ApplyEnvironmentVariables(IReadOnlyDictionary variables) - { - Requires.NotNull(variables, nameof(variables)); - - var oldValues = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var variable in variables) - { - oldValues[variable.Key] = Environment.GetEnvironmentVariable(variable.Key); - Environment.SetEnvironmentVariable(variable.Key, variable.Value); - } - - return new RestoreEnvironmentVariables(oldValues); - } - - private void AssertStandardProperties(VersionOptions versionOptions, BuildResults buildResult, string relativeProjectDirectory = null) - { - int versionHeight = this.GetVersionHeight(relativeProjectDirectory); - Version idAsVersion = this.GetVersion(relativeProjectDirectory); - string commitIdShort = this.CommitIdShort; - Version version = this.GetVersion(relativeProjectDirectory); - Version assemblyVersion = GetExpectedAssemblyVersion(versionOptions, version); - var additionalBuildMetadata = from item in buildResult.BuildResult.ProjectStateAfterBuild.GetItems("BuildMetadata") - select item.EvaluatedInclude; - var expectedBuildMetadata = $"+{commitIdShort}"; - if (additionalBuildMetadata.Any()) - { - expectedBuildMetadata += "." + string.Join(".", additionalBuildMetadata); - } - - string expectedBuildMetadataWithoutCommitId = additionalBuildMetadata.Any() ? $"+{string.Join(".", additionalBuildMetadata)}" : string.Empty; - string optionalFourthComponent = versionOptions.VersionHeightPosition == SemanticVersion.Position.Revision ? $".{idAsVersion.Revision}" : string.Empty; - - Assert.Equal($"{version}", buildResult.AssemblyFileVersion); - Assert.Equal($"{idAsVersion.Major}.{idAsVersion.Minor}.{idAsVersion.Build}{optionalFourthComponent}{versionOptions.Version.Prerelease}{expectedBuildMetadata}", buildResult.AssemblyInformationalVersion); - - // The assembly version property should always have four integer components to it, - // per bug https://github.com/dotnet/Nerdbank.GitVersioning/issues/26 - Assert.Equal($"{assemblyVersion.Major}.{assemblyVersion.Minor}.{assemblyVersion.Build}.{assemblyVersion.Revision}", buildResult.AssemblyVersion); - - Assert.Equal(idAsVersion.Build.ToString(), buildResult.BuildNumber); - Assert.Equal($"{version}", buildResult.BuildVersion); - Assert.Equal($"{idAsVersion.Major}.{idAsVersion.Minor}.{idAsVersion.Build}", buildResult.BuildVersion3Components); - Assert.Equal(idAsVersion.Build.ToString(), buildResult.BuildVersionNumberComponent); - Assert.Equal($"{idAsVersion.Major}.{idAsVersion.Minor}.{idAsVersion.Build}", buildResult.BuildVersionSimple); - Assert.Equal(this.LibGit2Repository.Head.Tip.Id.Sha, buildResult.GitCommitId); - Assert.Equal(this.LibGit2Repository.Head.Tip.Author.When.UtcTicks.ToString(CultureInfo.InvariantCulture), buildResult.GitCommitDateTicks); - Assert.Equal(commitIdShort, buildResult.GitCommitIdShort); - Assert.Equal(versionHeight.ToString(), buildResult.GitVersionHeight); - Assert.Equal($"{version.Major}.{version.Minor}", buildResult.MajorMinorVersion); - Assert.Equal(versionOptions.Version.Prerelease, buildResult.PrereleaseVersion); - Assert.Equal(expectedBuildMetadata, buildResult.SemVerBuildSuffix); - - string GetPkgVersionSuffix(bool useSemVer2) - { - string pkgVersionSuffix = buildResult.PublicRelease ? string.Empty : $"-g{commitIdShort}"; - if (useSemVer2) - { - pkgVersionSuffix += expectedBuildMetadataWithoutCommitId; - } - - return pkgVersionSuffix; - } - - // NuGet is now SemVer 2.0 and will pass additional build metadata if provided - string nugetPkgVersionSuffix = GetPkgVersionSuffix(useSemVer2: versionOptions?.NuGetPackageVersionOrDefault.SemVer == 2); - Assert.Equal($"{idAsVersion.Major}.{idAsVersion.Minor}.{idAsVersion.Build}{GetSemVerAppropriatePrereleaseTag(versionOptions)}{nugetPkgVersionSuffix}", buildResult.NuGetPackageVersion); - - // Chocolatey only supports SemVer 1.0 - string chocolateyPkgVersionSuffix = GetPkgVersionSuffix(useSemVer2: false); - Assert.Equal($"{idAsVersion.Major}.{idAsVersion.Minor}.{idAsVersion.Build}{GetSemVerAppropriatePrereleaseTag(versionOptions)}{chocolateyPkgVersionSuffix}", buildResult.ChocolateyPackageVersion); - - var buildNumberOptions = versionOptions.CloudBuildOrDefault.BuildNumberOrDefault; - if (buildNumberOptions.EnabledOrDefault) - { - var commitIdOptions = buildNumberOptions.IncludeCommitIdOrDefault; - var buildNumberSemVer = SemanticVersion.Parse(buildResult.CloudBuildNumber); - bool hasCommitData = commitIdOptions.WhenOrDefault == VersionOptions.CloudBuildNumberCommitWhen.Always - || (commitIdOptions.WhenOrDefault == VersionOptions.CloudBuildNumberCommitWhen.NonPublicReleaseOnly && !buildResult.PublicRelease); - Version expectedVersion = hasCommitData && commitIdOptions.WhereOrDefault == VersionOptions.CloudBuildNumberCommitWhere.FourthVersionComponent - ? idAsVersion - : new Version(version.Major, version.Minor, version.Build); - Assert.Equal(expectedVersion, buildNumberSemVer.Version); - Assert.Equal(buildResult.PrereleaseVersion, buildNumberSemVer.Prerelease); - string expectedBuildNumberMetadata = hasCommitData && commitIdOptions.WhereOrDefault == VersionOptions.CloudBuildNumberCommitWhere.BuildMetadata - ? $"+{commitIdShort}" - : string.Empty; - if (additionalBuildMetadata.Any()) - { - expectedBuildNumberMetadata = expectedBuildNumberMetadata.Length == 0 - ? "+" + string.Join(".", additionalBuildMetadata) - : expectedBuildNumberMetadata + "." + string.Join(".", additionalBuildMetadata); - } - - Assert.Equal(expectedBuildNumberMetadata, buildNumberSemVer.BuildMetadata); - } - else - { - Assert.Equal(string.Empty, buildResult.CloudBuildNumber); - } - } - - private static string GetSemVerAppropriatePrereleaseTag(VersionOptions versionOptions) - { - return versionOptions.NuGetPackageVersionOrDefault.SemVer == 1 - ? versionOptions.Version.Prerelease?.Replace('.', '-') - : versionOptions.Version.Prerelease; - } - - private async Task BuildAsync(string target = Targets.GetBuildVersion, LoggerVerbosity logVerbosity = LoggerVerbosity.Detailed, bool assertSuccessfulBuild = true) - { - var eventLogger = new MSBuildLogger { Verbosity = LoggerVerbosity.Minimal }; - var loggers = new ILogger[] { eventLogger }; - this.testProject.Save(); // persist generated project on disk for analysis - this.ApplyGlobalProperties(this.globalProperties); - var buildResult = await this.buildManager.BuildAsync( - this.Logger, - this.projectCollection, - this.testProject, - target, - this.globalProperties, - logVerbosity, - loggers); - var result = new BuildResults(buildResult, eventLogger.LoggedEvents); - this.Logger.WriteLine(result.ToString()); - if (assertSuccessfulBuild) - { - Assert.Equal(BuildResultCode.Success, buildResult.OverallResult); - } - - return result; - } - - private void LoadTargetsIntoProjectCollection() - { - string prefix = $"{ThisAssembly.RootNamespace}.Targets."; - - var streamNames = from name in Assembly.GetExecutingAssembly().GetManifestResourceNames() - where name.StartsWith(prefix, StringComparison.Ordinal) - select name; - foreach (string name in streamNames) - { - using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(name)) - { - var targetsFile = ProjectRootElement.Create(XmlReader.Create(stream), this.projectCollection); - targetsFile.FullPath = Path.Combine(this.RepoPath, name.Substring(prefix.Length)); - targetsFile.Save(); // persist files on disk - } - } - } - - private ProjectRootElement CreateNativeProjectRootElement(string projectDirectory, string projectName) - { - using (var reader = XmlReader.Create(Assembly.GetExecutingAssembly().GetManifestResourceStream($"{ThisAssembly.RootNamespace}.test.vcprj"))) - { - var pre = ProjectRootElement.Create(reader, this.projectCollection); - pre.FullPath = Path.Combine(projectDirectory, projectName); - pre.InsertAfterChild(pre.CreateImportElement(Path.Combine(this.RepoPath, GitVersioningPropsFileName)), null); - pre.AddImport(Path.Combine(this.RepoPath, GitVersioningTargetsFileName)); - return pre; - } - } - - private ProjectRootElement CreateProjectRootElement(string projectDirectory, string projectName) - { - using (var reader = XmlReader.Create(Assembly.GetExecutingAssembly().GetManifestResourceStream($"{ThisAssembly.RootNamespace}.test.prj"))) - { - var pre = ProjectRootElement.Create(reader, this.projectCollection); - pre.FullPath = Path.Combine(projectDirectory, projectName); - pre.InsertAfterChild(pre.CreateImportElement(Path.Combine(this.RepoPath, GitVersioningPropsFileName)), null); - pre.AddImport(Path.Combine(this.RepoPath, GitVersioningTargetsFileName)); - return pre; - } - } - - private void MakeItAVBProject() - { - var csharpImport = this.testProject.Imports.Single(i => i.Project.Contains("CSharp")); - csharpImport.Project = "$(MSBuildToolsPath)/Microsoft.VisualBasic.targets"; - var isVbProperty = this.testProject.Properties.Single(p => p.Name == "IsVB"); - isVbProperty.Value = "true"; - } - - private struct RestoreEnvironmentVariables : IDisposable - { - private readonly IReadOnlyDictionary applyVariables; - - internal RestoreEnvironmentVariables(IReadOnlyDictionary applyVariables) - { - this.applyVariables = applyVariables; - } - - public void Dispose() - { - ApplyEnvironmentVariables(this.applyVariables); - } - } - - private static class CloudBuild - { - public static readonly ImmutableDictionary SuppressEnvironment = ImmutableDictionary.Empty - // AppVeyor - .Add("APPVEYOR", string.Empty) - .Add("APPVEYOR_REPO_TAG", string.Empty) - .Add("APPVEYOR_REPO_TAG_NAME", string.Empty) - .Add("APPVEYOR_PULL_REQUEST_NUMBER", string.Empty) - // VSTS - .Add("SYSTEM_TEAMPROJECTID", string.Empty) - .Add("BUILD_SOURCEBRANCH", string.Empty) - // Teamcity - .Add("BUILD_VCS_NUMBER", string.Empty) - .Add("BUILD_GIT_BRANCH", string.Empty); - public static readonly ImmutableDictionary VSTS = SuppressEnvironment - .SetItem("SYSTEM_TEAMPROJECTID", "1"); - public static readonly ImmutableDictionary AppVeyor = SuppressEnvironment - .SetItem("APPVEYOR", "True"); - public static readonly ImmutableDictionary Teamcity = SuppressEnvironment - .SetItem("BUILD_VCS_NUMBER", "1"); - } - - private static class Targets - { - internal const string Build = "Build"; - internal const string GetBuildVersion = "GetBuildVersion"; - internal const string GetNuGetPackageVersion = "GetNuGetPackageVersion"; - internal const string GenerateAssemblyNBGVVersionInfo = "GenerateAssemblyNBGVVersionInfo"; - internal const string GenerateNativeNBGVVersionInfo = "GenerateNativeNBGVVersionInfo"; - } - - private static class Properties - { - internal const string GenerateAssemblyVersionInfo = "GenerateAssemblyVersionInfo"; - } - - private class BuildResults - { - internal BuildResults(BuildResult buildResult, IReadOnlyList loggedEvents) - { - Requires.NotNull(buildResult, nameof(buildResult)); - this.BuildResult = buildResult; - this.LoggedEvents = loggedEvents; - } - - public BuildResult BuildResult { get; private set; } - - public IReadOnlyList LoggedEvents { get; private set; } - - public bool PublicRelease => string.Equals("true", this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("PublicRelease"), StringComparison.OrdinalIgnoreCase); - public string BuildNumber => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("BuildNumber"); - public string GitCommitId => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("GitCommitId"); - public string BuildVersion => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("BuildVersion"); - public string BuildVersionSimple => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("BuildVersionSimple"); - public string PrereleaseVersion => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("PrereleaseVersion"); - public string MajorMinorVersion => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("MajorMinorVersion"); - public string BuildVersionNumberComponent => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("BuildVersionNumberComponent"); - public string GitCommitIdShort => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("GitCommitIdShort"); - public string GitCommitDateTicks => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("GitCommitDateTicks"); - public string GitVersionHeight => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("GitVersionHeight"); - public string SemVerBuildSuffix => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("SemVerBuildSuffix"); - public string BuildVersion3Components => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("BuildVersion3Components"); - public string AssemblyInformationalVersion => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("AssemblyInformationalVersion"); - public string AssemblyFileVersion => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("AssemblyFileVersion"); - public string AssemblyVersion => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("AssemblyVersion"); - public string NuGetPackageVersion => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("NuGetPackageVersion"); - public string ChocolateyPackageVersion => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("ChocolateyPackageVersion"); - public string CloudBuildNumber => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("CloudBuildNumber"); - public string AssemblyName => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("AssemblyName"); - public string AssemblyTitle => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("AssemblyTitle"); - public string AssemblyProduct => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("AssemblyProduct"); - public string AssemblyCompany => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("AssemblyCompany"); - public string AssemblyCopyright => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("AssemblyCopyright"); - public string AssemblyConfiguration => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("Configuration"); - public string RootNamespace => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("RootNamespace"); - - public string GitBuildVersion => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("GitBuildVersion"); - public string GitBuildVersionSimple => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("GitBuildVersionSimple"); - public string GitAssemblyInformationalVersion => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("GitAssemblyInformationalVersion"); - - // Just a sampling of other properties optionally set in cloud build. - public string NBGV_GitCommitIdShort => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("NBGV_GitCommitIdShort"); - public string NBGV_NuGetPackageVersion => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("NBGV_NuGetPackageVersion"); - - public override string ToString() - { - var sb = new StringBuilder(); - - foreach (var property in this.GetType().GetRuntimeProperties().OrderBy(p => p.Name, StringComparer.OrdinalIgnoreCase)) - { - if (property.DeclaringType == this.GetType() && property.Name != nameof(this.BuildResult)) - { - sb.AppendLine($"{property.Name} = {property.GetValue(this)}"); - } - } - - return sb.ToString(); - } - } - - private class MSBuildLogger : ILogger - { - public string Parameters { get; set; } - - public LoggerVerbosity Verbosity { get; set; } - - public List LoggedEvents { get; } = new List(); - - public void Initialize(IEventSource eventSource) - { - eventSource.AnyEventRaised += this.EventSource_AnyEventRaised; - } - - public void Shutdown() - { - } - - private void EventSource_AnyEventRaised(object sender, BuildEventArgs e) - { - this.LoggedEvents.Add(e); - } - } -} diff --git a/src/NerdBank.GitVersioning.Tests/MSBuildFixture.cs b/src/NerdBank.GitVersioning.Tests/MSBuildFixture.cs deleted file mode 100644 index 059f36f6..00000000 --- a/src/NerdBank.GitVersioning.Tests/MSBuildFixture.cs +++ /dev/null @@ -1,7 +0,0 @@ -public class MSBuildFixture -{ - public MSBuildFixture() - { - MSBuildExtensions.LoadMSBuild(); - } -} diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/DeltaStreamReaderTests.cs b/src/NerdBank.GitVersioning.Tests/ManagedGit/DeltaStreamReaderTests.cs deleted file mode 100644 index 279e0a16..00000000 --- a/src/NerdBank.GitVersioning.Tests/ManagedGit/DeltaStreamReaderTests.cs +++ /dev/null @@ -1,176 +0,0 @@ -using System; -using System.Collections.ObjectModel; -using System.IO; -using Nerdbank.GitVersioning.ManagedGit; -using Xunit; - -namespace ManagedGit -{ - // Test case borrowed from https://stefan.saasen.me/articles/git-clone-in-haskell-from-the-bottom-up/#format-of-the-delta-representation - public class DeltaStreamReaderTests - { - [Fact] - public void ReadCopyInstruction() - { - using (Stream stream = new MemoryStream( - new byte[] - { - 0b_10110000, - 0b_11010001, - 0b_00000001 - })) - { - var instruction = DeltaStreamReader.Read(stream).Value; - - Assert.Equal(DeltaInstructionType.Copy, instruction.InstructionType); - Assert.Equal(0, instruction.Offset); - Assert.Equal(465, instruction.Size); - } - } - - [Fact] - public void ReadCopyInstruction_Memory() - { - var stream = new byte[] - { - 0b_10110000, - 0b_11010001, - 0b_00000001 - }; - var memory = new ReadOnlyMemory(stream); - - var instruction = DeltaStreamReader.Read(ref memory).Value; - - Assert.Equal(0, memory.Length); - Assert.Equal(DeltaInstructionType.Copy, instruction.InstructionType); - Assert.Equal(0, instruction.Offset); - Assert.Equal(465, instruction.Size); - } - - [Fact] - public void ReadInsertInstruction() - { - using (Stream stream = new MemoryStream(new byte[] { 0b_00010111 })) - { - var instruction = DeltaStreamReader.Read(stream).Value; - - Assert.Equal(DeltaInstructionType.Insert, instruction.InstructionType); - Assert.Equal(0, instruction.Offset); - Assert.Equal(23, instruction.Size); - } - } - - [Fact] - public void ReadInsertInstruction_Memory() - { - var stream = new byte[] { 0b_00010111 }; - var memory = new ReadOnlyMemory(stream); - - var instruction = DeltaStreamReader.Read(ref memory).Value; - - Assert.Equal(0, memory.Length); - Assert.Equal(DeltaInstructionType.Insert, instruction.InstructionType); - Assert.Equal(0, instruction.Offset); - Assert.Equal(23, instruction.Size); - } - - [Fact] - public void ReadStreamTest() - { - using (Stream stream = new MemoryStream( - new byte[] - { - 0b_10110011, 0b_11001110, 0b_00000001, 0b_00100111, 0b_00000001, - 0b_10110011, 0b_01011111, 0b_00000011, 0b_01101100, 0b_00010000, 0b_10010011, - 0b_11110101, 0b_00000010, 0b_01101011, 0b_10110011, 0b_11001011, 0b_00010011, - 0b_01000110, 0b_00000011})) - { - Collection instructions = new Collection(); - - DeltaInstruction? current; - - while ((current = DeltaStreamReader.Read(stream)) is not null) - { - instructions.Add(current.Value); - } - - Assert.Collection( - instructions, - instruction => - { - Assert.Equal(DeltaInstructionType.Copy, instruction.InstructionType); - Assert.Equal(462, instruction.Offset); - Assert.Equal(295, instruction.Size); - }, - instruction => - { - Assert.Equal(DeltaInstructionType.Copy, instruction.InstructionType); - Assert.Equal(863, instruction.Offset); - Assert.Equal(4204, instruction.Size); - }, - instruction => - { - Assert.Equal(DeltaInstructionType.Copy, instruction.InstructionType); - Assert.Equal(757, instruction.Offset); - Assert.Equal(107, instruction.Size); - }, - instruction => - { - Assert.Equal(DeltaInstructionType.Copy, instruction.InstructionType); - Assert.Equal(5067, instruction.Offset); - Assert.Equal(838, instruction.Size); - }); - } - } - - [Fact] - public void ReadStreamTest_Memory() - { - var stream = - new byte[] - { - 0b_10110011, 0b_11001110, 0b_00000001, 0b_00100111, 0b_00000001, - 0b_10110011, 0b_01011111, 0b_00000011, 0b_01101100, 0b_00010000, 0b_10010011, - 0b_11110101, 0b_00000010, 0b_01101011, 0b_10110011, 0b_11001011, 0b_00010011, - 0b_01000110, 0b_00000011}; - var memory = new ReadOnlyMemory(stream); - - Collection instructions = new Collection(); - - DeltaInstruction? current; - - while ((current = DeltaStreamReader.Read(ref memory)) is not null) - { - instructions.Add(current.Value); - } - - Assert.Equal(0, memory.Length); - Assert.Collection( - instructions, - instruction => - { - Assert.Equal(DeltaInstructionType.Copy, instruction.InstructionType); - Assert.Equal(462, instruction.Offset); - Assert.Equal(295, instruction.Size); - }, - instruction => - { - Assert.Equal(DeltaInstructionType.Copy, instruction.InstructionType); - Assert.Equal(863, instruction.Offset); - Assert.Equal(4204, instruction.Size); - }, - instruction => - { - Assert.Equal(DeltaInstructionType.Copy, instruction.InstructionType); - Assert.Equal(757, instruction.Offset); - Assert.Equal(107, instruction.Size); - }, - instruction => - { - Assert.Equal(DeltaInstructionType.Copy, instruction.InstructionType); - Assert.Equal(5067, instruction.Offset); - Assert.Equal(838, instruction.Size); - }); - } - } -} diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/GitCommitReaderTests.cs b/src/NerdBank.GitVersioning.Tests/ManagedGit/GitCommitReaderTests.cs deleted file mode 100644 index ea4feeab..00000000 --- a/src/NerdBank.GitVersioning.Tests/ManagedGit/GitCommitReaderTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using Nerdbank.GitVersioning.ManagedGit; -using Xunit; - -namespace ManagedGit -{ - public class GitCommitReaderTests - { - [Fact] - public void ReadTest() - { - using (Stream stream = TestUtilities.GetEmbeddedResource(@"ManagedGit\commit-d56dc3ed179053abef2097d1120b4507769bcf1a")) - { - var commit = GitCommitReader.Read(stream, GitObjectId.Parse("d56dc3ed179053abef2097d1120b4507769bcf1a"), readAuthor: true); - - Assert.Equal("d56dc3ed179053abef2097d1120b4507769bcf1a", commit.Sha.ToString()); - Assert.Equal("f914b48023c7c804a4f3be780d451f31aef74ac1", commit.Tree.ToString()); - - Assert.Collection( - commit.Parents, - c => Assert.Equal("4497b0eaaa89abf0e6d70961ad5f04fd3a49cbc6", c.ToString()), - c => Assert.Equal("0989e8fe0cd0e0900173b26decdfb24bc0cc8232", c.ToString())); - - var author = commit.Author.Value; - - Assert.Equal("Andrew Arnott", author.Name); - Assert.Equal(new DateTimeOffset(2020, 10, 6, 13, 40, 09, TimeSpan.FromHours(-6)), author.Date); - Assert.Equal("andrewarnott@gmail.com", author.Email); - - // Committer and commit message are not read - } - } - - [Fact] - public void ReadCommitWithThreeParents() - { - using (Stream stream = TestUtilities.GetEmbeddedResource(@"ManagedGit\commit-ab39e8acac105fa0db88514f259341c9f0201b22")) - { - var commit = GitCommitReader.Read(stream, GitObjectId.Parse("ab39e8acac105fa0db88514f259341c9f0201b22"), readAuthor: true); - - Assert.Equal(3, commit.Parents.Count()); - - Assert.Collection( - commit.Parents, - c => Assert.Equal("e0b4d66ef7915417e04e88d5fa173185bb940029", c.ToString()), - c => Assert.Equal("10e67ce38fbee44b3f5584d4f9df6de6c5f4cc5c", c.ToString()), - c => Assert.Equal("a7fef320334121af85dce4b9b731f6c9a9127cfd", c.ToString())); - } - } - } -} diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/GitCommitTests.cs b/src/NerdBank.GitVersioning.Tests/ManagedGit/GitCommitTests.cs deleted file mode 100644 index 36aebaeb..00000000 --- a/src/NerdBank.GitVersioning.Tests/ManagedGit/GitCommitTests.cs +++ /dev/null @@ -1,98 +0,0 @@ -using Nerdbank.GitVersioning.ManagedGit; -using Xunit; - -namespace ManagedGit -{ - public class GitCommitTests - { - private readonly byte[] shaAsByteArray = new byte[] { 0x4e, 0x91, 0x27, 0x36, 0xc2, 0x7e, 0x40, 0xb3, 0x89, 0x90, 0x4d, 0x04, 0x6d, 0xc6, 0x3d, 0xc9, 0xf5, 0x78, 0x11, 0x7f }; - - [Fact] - public void EqualsObjectTest() - { - var commit = new GitCommit() - { - Sha = GitObjectId.Parse(this.shaAsByteArray), - }; - - var commit2 = new GitCommit() - { - Sha = GitObjectId.Parse(this.shaAsByteArray), - }; - - var emptyCommit = new GitCommit() - { - Sha = GitObjectId.Empty, - }; - - // Must be equal to itself - Assert.True(commit.Equals((object)commit)); - Assert.True(commit.Equals((object)commit2)); - - // Not equal to null - Assert.False(commit.Equals(null)); - - // Not equal to other representations of the commit - Assert.False(commit.Equals(this.shaAsByteArray)); - Assert.False(commit.Equals(commit.Sha)); - - // Not equal to other object ids - Assert.False(commit.Equals((object)emptyCommit)); - } - - [Fact] - public void EqualsCommitTest() - { - var commit = new GitCommit() - { - Sha = GitObjectId.Parse(this.shaAsByteArray), - }; - - var commit2 = new GitCommit() - { - Sha = GitObjectId.Parse(this.shaAsByteArray), - }; - - var emptyCommit = new GitCommit() - { - Sha = GitObjectId.Empty, - }; - - // Must be equal to itself - Assert.True(commit.Equals(commit2)); - Assert.True(commit.Equals(commit2)); - - // Not equal to other object ids - Assert.False(commit.Equals(emptyCommit)); - } - - [Fact] - public void GetHashCodeTest() - { - var commit = new GitCommit() - { - Sha = GitObjectId.Parse(this.shaAsByteArray), - }; - - var emptyCommit = new GitCommit() - { - Sha = GitObjectId.Empty, - }; - - // The hash code is the int32 representation of the first 4 bytes of the SHA hash - Assert.Equal(0x3627914e, commit.GetHashCode()); - Assert.Equal(0, emptyCommit.GetHashCode()); - } - - [Fact] - public void ToStringTest() - { - var commit = new GitCommit() - { - Sha = GitObjectId.Parse(this.shaAsByteArray), - }; - - Assert.Equal("Git Commit: 4e912736c27e40b389904d046dc63dc9f578117f", commit.ToString()); - } - } -} diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/GitObjectIdTests.cs b/src/NerdBank.GitVersioning.Tests/ManagedGit/GitObjectIdTests.cs deleted file mode 100644 index baeb4ff7..00000000 --- a/src/NerdBank.GitVersioning.Tests/ManagedGit/GitObjectIdTests.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Text; -using Nerdbank.GitVersioning.ManagedGit; -using Xunit; -using Xunit.Abstractions; - -namespace ManagedGit -{ - public class GitObjectIdTests - { - private readonly byte[] shaAsByteArray = new byte[] { 0x4e, 0x91, 0x27, 0x36, 0xc2, 0x7e, 0x40, 0xb3, 0x89, 0x90, 0x4d, 0x04, 0x6d, 0xc6, 0x3d, 0xc9, 0xf5, 0x78, 0x11, 0x7f }; - private const string shaAsHexString = "4e912736c27e40b389904d046dc63dc9f578117f"; - private readonly byte[] shaAsHexAsciiByteArray = Encoding.ASCII.GetBytes(shaAsHexString); - - [Fact] - public void ParseByteArrayTest() - { - var objectId = GitObjectId.Parse(this.shaAsByteArray); - - Span value = stackalloc byte[20]; - objectId.CopyTo(value); - Assert.True(value.SequenceEqual(this.shaAsByteArray.AsSpan())); - } - - [Fact] - public void ParseStringTest() - { - var objectId = GitObjectId.Parse(shaAsHexString); - - Span value = stackalloc byte[20]; - objectId.CopyTo(value); - Assert.True(value.SequenceEqual(this.shaAsByteArray.AsSpan())); - } - - [Fact] - public void ParseHexArrayTest() - { - var objectId = GitObjectId.ParseHex(this.shaAsHexAsciiByteArray); - - Span value = stackalloc byte[20]; - objectId.CopyTo(value); - Assert.True(value.SequenceEqual(this.shaAsByteArray.AsSpan())); - } - - [Fact] - public void EqualsObjectTest() - { - var objectId = GitObjectId.ParseHex(this.shaAsHexAsciiByteArray); - var objectId2 = GitObjectId.ParseHex(this.shaAsHexAsciiByteArray); - - // Must be equal to itself - Assert.True(objectId.Equals((object)objectId)); - Assert.True(objectId.Equals((object)objectId2)); - - // Not equal to null - Assert.False(objectId.Equals(null)); - - // Not equal to other representations of the object id - Assert.False(objectId.Equals(this.shaAsHexAsciiByteArray)); - Assert.False(objectId.Equals(this.shaAsByteArray)); - Assert.False(objectId.Equals(shaAsHexString)); - - // Not equal to other object ids - Assert.False(objectId.Equals((object)GitObjectId.Empty)); - } - - [Fact] - public void EqualsObjectIdTest() - { - var objectId = GitObjectId.ParseHex(this.shaAsHexAsciiByteArray); - var objectId2 = GitObjectId.ParseHex(this.shaAsHexAsciiByteArray); - - // Must be equal to itself - Assert.True(objectId.Equals(objectId)); - Assert.True(objectId.Equals(objectId2)); - - // Not equal to other object ids - Assert.False(objectId.Equals(GitObjectId.Empty)); - } - - [Fact] - public void GetHashCodeTest() - { - // The hash code is the int32 representation of the first 4 bytes - var objectId = GitObjectId.ParseHex(this.shaAsHexAsciiByteArray); - Assert.Equal(0x3627914e, objectId.GetHashCode()); - Assert.Equal(0, GitObjectId.Empty.GetHashCode()); - } - - [Fact] - public void AsUInt16Test() - { - // The hash code is the int32 representation of the first 4 bytes - var objectId = GitObjectId.ParseHex(this.shaAsHexAsciiByteArray); - Assert.Equal(0x4e91, objectId.AsUInt16()); - Assert.Equal(0, GitObjectId.Empty.GetHashCode()); - } - - [Fact] - public void ToStringTest() - { - var objectId = GitObjectId.Parse(this.shaAsByteArray); - Assert.Equal(shaAsHexString, objectId.ToString()); - } - - [Fact] - public void CopyToUtf16StringTest() - { - // Common use case: create the path to the object in the Git object store, - // e.g. git/objects/[byte 0]/[bytes 1 - 19] - byte[] valueAsBytes = Encoding.Unicode.GetBytes("git/objects/00/01020304050607080910111213141516171819"); - Span valueAsChars = MemoryMarshal.Cast(valueAsBytes); - - var objectId = GitObjectId.ParseHex(this.shaAsHexAsciiByteArray); - objectId.CopyAsHex(0, 1, valueAsChars.Slice(12, 1 * 2)); - objectId.CopyAsHex(1, 19, valueAsChars.Slice(15, 19 * 2)); - - var path = Encoding.Unicode.GetString(valueAsBytes); - Assert.Equal("git/objects/4e/912736c27e40b389904d046dc63dc9f578117f", path); - } - - [Fact] - public void CopyToTest() - { - var objectId = GitObjectId.Parse(this.shaAsByteArray); - - byte[] actual = new byte[20]; - objectId.CopyTo(actual); - - Assert.Equal(this.shaAsByteArray, actual); - } - } -} diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/GitObjectStreamTests.cs b/src/NerdBank.GitVersioning.Tests/ManagedGit/GitObjectStreamTests.cs deleted file mode 100644 index cd525257..00000000 --- a/src/NerdBank.GitVersioning.Tests/ManagedGit/GitObjectStreamTests.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.IO; -using System.IO.Compression; -using System.Security.Cryptography; -using Nerdbank.GitVersioning.ManagedGit; -using Xunit; - -namespace ManagedGit -{ - public class GitObjectStreamTests - { - [Fact] - public void ReadTest() - { - using (Stream rawStream = TestUtilities.GetEmbeddedResource(@"ManagedGit\3596ffe59898103a2675547d4597e742e1f2389c.gz")) - using (GitObjectStream stream = new GitObjectStream(rawStream, "commit")) - using (var sha = SHA1.Create()) - { - Assert.Equal(137, stream.Length); - var deflateStream = Assert.IsType(stream.BaseStream); - Assert.Same(rawStream, deflateStream.BaseStream); - Assert.Equal("commit", stream.ObjectType); - Assert.Equal(0, stream.Position); - - var hash = sha.ComputeHash(stream); - Assert.Equal("U1WYLbBP+xD47Y32m+hpCCTpnLA=", Convert.ToBase64String(hash)); - - Assert.Equal(stream.Length, stream.Position); - } - } - } -} diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/GitPackDeltafiedStreamTests.cs b/src/NerdBank.GitVersioning.Tests/ManagedGit/GitPackDeltafiedStreamTests.cs deleted file mode 100644 index 70728cfd..00000000 --- a/src/NerdBank.GitVersioning.Tests/ManagedGit/GitPackDeltafiedStreamTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.IO; -using Nerdbank.GitVersioning.ManagedGit; -using Xunit; - -namespace ManagedGit -{ - public class GitPackDeltafiedStreamTests - { - // Reconstructs an object by reading the base stream and the delta stream. - // You can create delta representations of an object by running the - // test tool which is located in the t/helper/ folder of the Git source repository. - // Use with the delta -d [base file,in] [updated file,in] [delta file,out] arguments. - [Theory] - [InlineData(@"ManagedGit\commit-4497b0eaaa89abf0e6d70961ad5f04fd3a49cbc6", @"ManagedGit\commit.delta", @"ManagedGit\commit-d56dc3ed179053abef2097d1120b4507769bcf1a")] - [InlineData(@"ManagedGit\tree-bb36cf0ca445ccc8e5ce9cc88f7cf74128e96dc9", @"ManagedGit\tree.delta", @"ManagedGit\tree-f914b48023c7c804a4f3be780d451f31aef74ac1")] - public void TestDeltaStream(string basePath, string deltaPath, string expectedPath) - { - byte[] expected = null; - - using (Stream expectedStream = TestUtilities.GetEmbeddedResource(expectedPath)) - { - expected = new byte[expectedStream.Length]; - expectedStream.Read(expected); - } - - byte[] actual = new byte[expected.Length]; - - using (Stream baseStream = TestUtilities.GetEmbeddedResource(basePath)) - using (Stream deltaStream = TestUtilities.GetEmbeddedResource(deltaPath)) - using (GitPackDeltafiedStream deltafiedStream = new GitPackDeltafiedStream(baseStream, deltaStream)) - { - // Assert.Equal(expected.Length, deltafiedStream.Length); - - deltafiedStream.Read(actual); - - Assert.Equal(expected, actual); - } - } - } -} diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/GitPackIndexMappedReaderTests.cs b/src/NerdBank.GitVersioning.Tests/ManagedGit/GitPackIndexMappedReaderTests.cs deleted file mode 100644 index 153839b7..00000000 --- a/src/NerdBank.GitVersioning.Tests/ManagedGit/GitPackIndexMappedReaderTests.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.IO; -using Nerdbank.GitVersioning.ManagedGit; -using Xunit; - -namespace ManagedGit -{ - public class GitPackIndexMappedReaderTests - { - [Fact] - public void ConstructorNullTest() - { - Assert.Throws(() => new GitPackIndexMappedReader(null)); - } - - [Fact] - public void GetOffsetTest() - { - var indexFile = Path.GetTempFileName(); - - using (Stream resourceStream = TestUtilities.GetEmbeddedResource(@"ManagedGit\pack-7d6b2c56ffb97eedb92f4e28583c093f7ee4b3d9.idx")) - using (FileStream stream = File.Open(indexFile, FileMode.Open)) - { - resourceStream.CopyTo(stream); - } - - using (FileStream stream = File.OpenRead(indexFile)) - using (GitPackIndexReader reader = new GitPackIndexMappedReader(stream)) - { - // Offset of an object which is present - Assert.Equal(12, reader.GetOffset(GitObjectId.Parse("f5b401f40ad83f13030e946c9ea22cb54cb853cd"))); - Assert.Equal(317, reader.GetOffset(GitObjectId.Parse("d6781552a0a94adbf73ed77696712084754dc274"))); - - // null for an object which is not present - Assert.Null(reader.GetOffset(GitObjectId.Empty)); - } - - try - { - File.Delete(indexFile); - } - catch (UnauthorizedAccessException) - { - // TBD: Figure out what's keeping a lock on the file. Seems to be unique to Windows. - } - - } - - [Fact] - public void GetOffsetFromPartialTest() - { - var indexFile = Path.GetTempFileName(); - - using (Stream resourceStream = TestUtilities.GetEmbeddedResource(@"ManagedGit\pack-7d6b2c56ffb97eedb92f4e28583c093f7ee4b3d9.idx")) - using (FileStream stream = File.Open(indexFile, FileMode.Open)) - { - resourceStream.CopyTo(stream); - } - - using (FileStream stream = File.OpenRead(indexFile)) - using (var reader = new GitPackIndexMappedReader(stream)) - { - // Offset of an object which is present - (var offset, var objectId) = reader.GetOffset(new byte[] { 0xf5, 0xb4, 0x01, 0xf4 }); - Assert.Equal(12, offset); - Assert.Equal(GitObjectId.Parse("f5b401f40ad83f13030e946c9ea22cb54cb853cd"), objectId); - - (offset, objectId) = reader.GetOffset(new byte[] { 0xd6, 0x78, 0x15, 0x52 }); - Assert.Equal(317, offset); - Assert.Equal(GitObjectId.Parse("d6781552a0a94adbf73ed77696712084754dc274"), objectId); - - // null for an object which is not present - (offset, objectId) = reader.GetOffset(new byte[] { 0x00, 0x00, 0x00, 0x00 }); - Assert.Null(offset); - Assert.Null(objectId); - } - - try - { - File.Delete(indexFile); - } - catch (UnauthorizedAccessException) - { - // TBD: Figure out what's keeping a lock on the file. Seems to be unique to Windows. - } - - } - } -} diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/GitPackMemoryCacheTests.cs b/src/NerdBank.GitVersioning.Tests/ManagedGit/GitPackMemoryCacheTests.cs deleted file mode 100644 index c9c51820..00000000 --- a/src/NerdBank.GitVersioning.Tests/ManagedGit/GitPackMemoryCacheTests.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.IO; -using Nerdbank.GitVersioning.ManagedGit; -using Xunit; - -namespace NerdBank.GitVersioning.Tests.ManagedGit -{ - /// - /// Tests the class. - /// - public class GitPackMemoryCacheTests - { - [Fact] - public void StreamsAreIndependent() - { - using (MemoryStream stream = new MemoryStream( - new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 })) - { - var cache = new GitPackMemoryCache(); - - var stream1 = cache.Add(0, stream); - Assert.True(cache.TryOpen(0, out Stream stream2)); - - using (stream1) - using (stream2) - { - stream1.Seek(5, SeekOrigin.Begin); - Assert.Equal(5, stream1.Position); - Assert.Equal(0, stream2.Position); - Assert.Equal(5, stream1.ReadByte()); - - Assert.Equal(6, stream1.Position); - Assert.Equal(0, stream2.Position); - - Assert.Equal(0, stream2.ReadByte()); - Assert.Equal(6, stream1.Position); - Assert.Equal(1, stream2.Position); - } - } - } - } -} diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/GitPackTests.cs b/src/NerdBank.GitVersioning.Tests/ManagedGit/GitPackTests.cs deleted file mode 100644 index 0c14a314..00000000 --- a/src/NerdBank.GitVersioning.Tests/ManagedGit/GitPackTests.cs +++ /dev/null @@ -1,173 +0,0 @@ -using System; -using System.IO; -using System.IO.Compression; -using System.Security.Cryptography; -using Nerdbank.GitVersioning; -using Nerdbank.GitVersioning.ManagedGit; -using Xunit; - -namespace ManagedGit -{ - public class GitPackTests : IDisposable - { - private readonly string indexFile = Path.GetTempFileName(); - private readonly string packFile = Path.GetTempFileName(); - - public GitPackTests() - { - using (Stream resourceStream = TestUtilities.GetEmbeddedResource(@"ManagedGit\pack-7d6b2c56ffb97eedb92f4e28583c093f7ee4b3d9.idx")) - using (FileStream stream = File.Open(this.indexFile, FileMode.Open)) - { - resourceStream.CopyTo(stream); - } - - using (Stream resourceStream = TestUtilities.GetEmbeddedResource(@"ManagedGit\pack-7d6b2c56ffb97eedb92f4e28583c093f7ee4b3d9.pack")) - using (FileStream stream = File.Open(this.packFile, FileMode.Open)) - { - resourceStream.CopyTo(stream); - } - } - - public void Dispose() - { - try - { - File.Delete(this.indexFile); - } - catch (UnauthorizedAccessException) - { - // TBD: Figure out what's keeping a lock on the file. Seems to be unique to Windows. - } - - try - { - File.Delete(this.packFile); - } - catch (UnauthorizedAccessException) - { - // TBD: Figure out what's keeping a lock on the file. Seems to be unique to Windows. - } - } - - [Fact] - public void GetPackedObject() - { - using (var gitPack = new GitPack( - (sha, objectType) => null, - new Lazy(() => File.OpenRead(this.indexFile)), - () => File.OpenRead(this.packFile), - GitPackNullCache.Instance)) - using (Stream commitStream = gitPack.GetObject(12, "commit")) - using (SHA1 sha = SHA1.Create()) - { - // This commit is not deltafied. It is stored as a .gz-compressed stream in the pack file. - var zlibStream = Assert.IsType(commitStream); - var deflateStream = Assert.IsType(zlibStream.BaseStream); - - if (IntPtr.Size > 4) - { - var pooledStream = Assert.IsType(deflateStream.BaseStream); - } - else - { - var pooledStream = Assert.IsType(deflateStream.BaseStream); - } - - Assert.Equal(222, commitStream.Length); - Assert.Equal("/zgldANj+jvgOwlecnOKylZDVQg=", Convert.ToBase64String(sha.ComputeHash(commitStream))); - } - } - - [Fact] - public void GetDeltafiedObject() - { - using (var gitPack = new GitPack( - (sha, objectType) => null, - new Lazy(() => File.OpenRead(this.indexFile)), - () => File.OpenRead(this.packFile), - GitPackNullCache.Instance)) - using (Stream commitStream = gitPack.GetObject(317, "commit")) - using (SHA1 sha = SHA1.Create()) - { - // This commit is not deltafied. It is stored as a .gz-compressed stream in the pack file. - var deltaStream = Assert.IsType(commitStream); - var zlibStream = Assert.IsType(deltaStream.BaseStream); - var deflateStream = Assert.IsType(zlibStream.BaseStream); - - if (IntPtr.Size > 4) - { - var pooledStream = Assert.IsType(deflateStream.BaseStream); - } - else - { - var directAccessStream = Assert.IsType(deflateStream.BaseStream); - } - - Assert.Equal(137, commitStream.Length); - Assert.Equal("lZu/7nGb0n1UuO9SlPluFnSvj4o=", Convert.ToBase64String(sha.ComputeHash(commitStream))); - } - } - - [Fact] - public void GetInvalidObject() - { - using (var gitPack = new GitPack( - (sha, objectType) => null, - new Lazy(() => File.OpenRead(this.indexFile)), - () => File.OpenRead(this.packFile), - GitPackNullCache.Instance)) - { - Assert.Throws(() => gitPack.GetObject(12, "invalid")); - Assert.Throws(() => gitPack.GetObject(-1, "commit")); - Assert.Throws(() => gitPack.GetObject(1, "commit")); - Assert.Throws(() => gitPack.GetObject(2, "commit")); - Assert.Throws(() => gitPack.GetObject(int.MaxValue, "commit")); - } - } - - [Fact] - public void TryGetObjectTest() - { - using (var gitPack = new GitPack( - (sha, objectType) => null, - new Lazy(() => File.OpenRead(this.indexFile)), - () => File.OpenRead(this.packFile), - GitPackNullCache.Instance)) - using (SHA1 sha = SHA1.Create()) - { - Assert.True(gitPack.TryGetObject(GitObjectId.Parse("f5b401f40ad83f13030e946c9ea22cb54cb853cd"), "commit", out Stream commitStream)); - using (commitStream) - { - // This commit is not deltafied. It is stored as a .gz-compressed stream in the pack file. - var zlibStream = Assert.IsType(commitStream); - var deflateStream = Assert.IsType(zlibStream.BaseStream); - - if (IntPtr.Size > 4) - { - var pooledStream = Assert.IsType(deflateStream.BaseStream); - } - else - { - var directAccessStream = Assert.IsType(deflateStream.BaseStream); - } - - Assert.Equal(222, commitStream.Length); - Assert.Equal("/zgldANj+jvgOwlecnOKylZDVQg=", Convert.ToBase64String(sha.ComputeHash(commitStream))); - } - } - } - - [Fact] - public void TryGetMissingObjectTest() - { - using (var gitPack = new GitPack( - (sha, objectType) => null, - new Lazy(() => File.OpenRead(this.indexFile)), - () => File.OpenRead(this.packFile), - GitPackNullCache.Instance)) - { - Assert.False(gitPack.TryGetObject(GitObjectId.Empty, "commit", out Stream commitStream)); - } - } - } -} diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/GitRepositoryTests.cs b/src/NerdBank.GitVersioning.Tests/ManagedGit/GitRepositoryTests.cs deleted file mode 100644 index edbbbb80..00000000 --- a/src/NerdBank.GitVersioning.Tests/ManagedGit/GitRepositoryTests.cs +++ /dev/null @@ -1,337 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Text; -using LibGit2Sharp; -using Nerdbank.GitVersioning; -using Nerdbank.GitVersioning.ManagedGit; -using Xunit; -using Xunit.Abstractions; - -namespace ManagedGit -{ - public class GitRepositoryTests : RepoTestBase - { - public GitRepositoryTests(ITestOutputHelper logger) - : base(logger) - { - } - - protected override Nerdbank.GitVersioning.GitContext CreateGitContext(string path, string committish = null) - => Nerdbank.GitVersioning.GitContext.Create(path, committish, writable: false); - - [Fact] - public void CreateTest() - { - this.InitializeSourceControl(); - this.AddCommits(1); - - using (var repository = GitRepository.Create(this.RepoPath)) - { - AssertPath(Path.Combine(this.RepoPath, ".git"), repository.CommonDirectory); - AssertPath(Path.Combine(this.RepoPath, ".git"), repository.GitDirectory); - AssertPath(this.RepoPath, repository.WorkingDirectory); - AssertPath(Path.Combine(this.RepoPath, ".git", "objects"), repository.ObjectDirectory); - } - } - - [Fact] - public void CreateWorkTreeTest() - { - this.InitializeSourceControl(); - this.AddCommits(2); - - string workTreePath = this.CreateDirectoryForNewRepo(); - Directory.Delete(workTreePath); - this.LibGit2Repository.Worktrees.Add("HEAD~1", "myworktree", workTreePath, isLocked: false); - - using (var repository = GitRepository.Create(workTreePath)) - { - AssertPath(Path.Combine(this.RepoPath, ".git"), repository.CommonDirectory); - AssertPath(Path.Combine(this.RepoPath, ".git", "worktrees", "myworktree"), repository.GitDirectory); - AssertPath(workTreePath, repository.WorkingDirectory); - AssertPath(Path.Combine(this.RepoPath, ".git", "objects"), repository.ObjectDirectory); - } - } - - [Fact] - public void CreateNotARepoTest() - { - Assert.Null(GitRepository.Create(null)); - Assert.Null(GitRepository.Create("")); - Assert.Null(GitRepository.Create("/A/Path/To/A/Directory/Which/Does/Not/Exist")); - Assert.Null(GitRepository.Create(this.RepoPath)); - } - - // A "normal" repository, where a branch is currently checked out. - [Fact] - public void GetHeadAsReferenceTest() - { - this.InitializeSourceControl(); - this.AddCommits(2); - - var headObjectId = GitObjectId.Parse(this.LibGit2Repository.Head.Tip.Sha); - - using (var repository = GitRepository.Create(this.RepoPath)) - { - var head = repository.GetHeadAsReferenceOrSha(); - var reference = Assert.IsType(head); - Assert.Equal("refs/heads/master", reference); - - Assert.Equal(headObjectId, repository.GetHeadCommitSha()); - - var headCommit = repository.GetHeadCommit(); - Assert.NotNull(headCommit); - Assert.Equal(headObjectId, headCommit.Value.Sha); - } - } - - // A repository with a detached HEAD. - [Fact] - public void GetHeadAsShaTest() - { - this.InitializeSourceControl(); - this.AddCommits(2); - - var newHead = this.LibGit2Repository.Head.Tip.Parents.Single(); - var newHeadObjectId = GitObjectId.Parse(newHead.Sha); - Commands.Checkout(this.LibGit2Repository, this.LibGit2Repository.Head.Tip.Parents.Single()); - - using (var repository = GitRepository.Create(this.RepoPath)) - { - var detachedHead = repository.GetHeadAsReferenceOrSha(); - var reference = Assert.IsType(detachedHead); - Assert.Equal(newHeadObjectId, reference); - - Assert.Equal(newHeadObjectId, repository.GetHeadCommitSha()); - - var headCommit = repository.GetHeadCommit(); - Assert.NotNull(headCommit); - Assert.Equal(newHeadObjectId, headCommit.Value.Sha); - } - } - - // A fresh repository with no commits yet. - [Fact] - public void GetHeadMissingTest() - { - this.InitializeSourceControl(withInitialCommit: false); - - using (var repository = GitRepository.Create(this.RepoPath)) - { - var head = repository.GetHeadAsReferenceOrSha(); - var reference = Assert.IsType(head); - Assert.Equal("refs/heads/master", reference); - - Assert.Equal(GitObjectId.Empty, repository.GetHeadCommitSha()); - - Assert.Null(repository.GetHeadCommit()); - } - } - - // Fetch a commit from the object store - [Fact] - public void GetCommitTest() - { - this.InitializeSourceControl(); - this.AddCommits(2); - - var headObjectId = GitObjectId.Parse(this.LibGit2Repository.Head.Tip.Sha); - - using (var repository = GitRepository.Create(this.RepoPath)) - { - var commit = repository.GetCommit(headObjectId); - Assert.Equal(headObjectId, commit.Sha); - } - } - - [Fact] - public void GetInvalidCommitTest() - { - this.InitializeSourceControl(); - this.AddCommits(2); - - var headObjectId = GitObjectId.Parse(this.LibGit2Repository.Head.Tip.Sha); - - using (var repository = GitRepository.Create(this.RepoPath)) - { - Assert.Throws(() => repository.GetCommit(GitObjectId.Empty)); - } - } - - [Fact] - public void GetTreeEntryTest() - { - this.InitializeSourceControl(); - File.WriteAllText(Path.Combine(this.RepoPath, "hello.txt"), "Hello, World"); - Commands.Stage(this.LibGit2Repository, "hello.txt"); - this.AddCommits(); - - using (var repository = GitRepository.Create(this.RepoPath)) - { - var headCommit = repository.GetHeadCommit(); - Assert.NotNull(headCommit); - - var helloBlobId = repository.GetTreeEntry(headCommit.Value.Tree, Encoding.UTF8.GetBytes("hello.txt")); - Assert.Equal("1856e9be02756984c385482a07e42f42efd5d2f3", helloBlobId.ToString()); - } - } - - [Fact] - public void GetInvalidTreeEntryTest() - { - this.InitializeSourceControl(); - File.WriteAllText(Path.Combine(this.RepoPath, "hello.txt"), "Hello, World"); - Commands.Stage(this.LibGit2Repository, "hello.txt"); - this.AddCommits(); - - using (var repository = GitRepository.Create(this.RepoPath)) - { - var headCommit = repository.GetHeadCommit(); - Assert.NotNull(headCommit); - - Assert.Equal(GitObjectId.Empty, repository.GetTreeEntry(headCommit.Value.Tree, Encoding.UTF8.GetBytes("goodbye.txt"))); - } - } - - [Fact] - public void GetObjectByShaTest() - { - this.InitializeSourceControl(); - this.AddCommits(2); - - var headObjectId = GitObjectId.Parse(this.LibGit2Repository.Head.Tip.Sha); - - using (var repository = GitRepository.Create(this.RepoPath)) - { - var commitStream = repository.GetObjectBySha(headObjectId, "commit"); - Assert.NotNull(commitStream); - - var objectStream = Assert.IsType(commitStream); - Assert.Equal("commit", objectStream.ObjectType); - Assert.Equal(186, objectStream.Length); - } - } - - // This test runs on netcoreapp only; netstandard/netfx don't support Path.GetRelativePath -#if NETCOREAPP - [Fact] - public void GetObjectFromAlternateTest() - { - // Add 2 alternates for this repository, each with their own commit. - // Make sure that commits from the current repository and the alternates - // can be found. - // - // Alternate1 Alternate2 - // | | - // +-----+ +-----+ - // | - // Repo - this.InitializeSourceControl(); - - var localCommit = this.LibGit2Repository.Commit("Local", this.Signer, this.Signer, new CommitOptions() { AllowEmptyCommit = true }); - - var alternate1Path = this.CreateDirectoryForNewRepo(); - this.InitializeSourceControl(alternate1Path).Dispose(); - var alternate1 = new Repository(alternate1Path); - var alternate1Commit = alternate1.Commit("Alternate 1", this.Signer, this.Signer, new CommitOptions() { AllowEmptyCommit = true }); - - var alternate2Path = this.CreateDirectoryForNewRepo(); - this.InitializeSourceControl(alternate2Path).Dispose(); - var alternate2 = new Repository(alternate2Path); - var alternate2Commit = alternate2.Commit("Alternate 2", this.Signer, this.Signer, new CommitOptions() { AllowEmptyCommit = true }); - - var objectDatabasePath = Path.Combine(this.RepoPath, ".git", "objects"); - - Directory.CreateDirectory(Path.Combine(this.RepoPath, ".git", "objects", "info")); - File.WriteAllText( - Path.Combine(this.RepoPath, ".git", "objects", "info", "alternates"), - $"{Path.GetRelativePath(objectDatabasePath, Path.Combine(alternate1Path, ".git", "objects"))}:{Path.GetRelativePath(objectDatabasePath, Path.Combine(alternate2Path, ".git", "objects"))}:"); - - using (GitRepository repository = GitRepository.Create(this.RepoPath)) - { - Assert.Equal(localCommit.Sha, repository.GetCommit(GitObjectId.Parse(localCommit.Sha)).Sha.ToString()); - Assert.Equal(alternate1Commit.Sha, repository.GetCommit(GitObjectId.Parse(alternate1Commit.Sha)).Sha.ToString()); - Assert.Equal(alternate2Commit.Sha, repository.GetCommit(GitObjectId.Parse(alternate2Commit.Sha)).Sha.ToString()); - } - } -#endif - - [Fact] - public void GetObjectByShaAndWrongTypeTest() - { - this.InitializeSourceControl(); - this.AddCommits(2); - - var headObjectId = GitObjectId.Parse(this.LibGit2Repository.Head.Tip.Sha); - - using (var repository = GitRepository.Create(this.RepoPath)) - { - Assert.Throws(() => repository.GetObjectBySha(headObjectId, "tree")); - } - } - - [Fact] - public void GetMissingObjectByShaTest() - { - this.InitializeSourceControl(); - this.AddCommits(2); - - var headObjectId = GitObjectId.Parse(this.LibGit2Repository.Head.Tip.Sha); - - using (var repository = GitRepository.Create(this.RepoPath)) - { - Assert.Throws(() => repository.GetObjectBySha(GitObjectId.Parse("7d6b2c56ffb97eedb92f4e28583c093f7ee4b3d9"), "commit")); - Assert.Null(repository.GetObjectBySha(GitObjectId.Empty, "commit")); - } - } - - [Fact] - public void ParseAlternates_SingleValue_Test() - { - var alternates = GitRepository.ParseAlternates(Encoding.UTF8.GetBytes("/home/git/nbgv/.git/objects\n")); - Assert.Collection( - alternates, - a => Assert.Equal("/home/git/nbgv/.git/objects", a)); - } - - [Fact] - public void ParseAlternates_SingleValue_NoTrailingNewline_Test() - { - var alternates = GitRepository.ParseAlternates(Encoding.UTF8.GetBytes("../repo/.git/objects")); - Assert.Collection( - alternates, - a => Assert.Equal("../repo/.git/objects", a)); - } - - [Fact] - public void ParseAlternates_TwoValues_Test() - { - var alternates = GitRepository.ParseAlternates(Encoding.UTF8.GetBytes("/home/git/nbgv/.git/objects:../../clone/.git/objects\n")); - Assert.Collection( - alternates, - a => Assert.Equal("/home/git/nbgv/.git/objects", a), - a => Assert.Equal("../../clone/.git/objects", a)); - } - - [Fact] - public void ParseAlternates_PathWithColon_Test() - { - var alternates = GitRepository.ParseAlternates( - Encoding.UTF8.GetBytes("C:/Users/nbgv/objects:C:/Users/nbgv2/objects/:../../clone/.git/objects\n"), - 2); - Assert.Collection( - alternates, - a => Assert.Equal("C:/Users/nbgv/objects", a), - a => Assert.Equal("C:/Users/nbgv2/objects/", a), - a => Assert.Equal("../../clone/.git/objects", a)); - } - - private static void AssertPath(string expected, string actual) - { - Assert.Equal( - Path.GetFullPath(expected), - Path.GetFullPath(actual)); - } - } -} diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/GitTreeStreamingReaderTests.cs b/src/NerdBank.GitVersioning.Tests/ManagedGit/GitTreeStreamingReaderTests.cs deleted file mode 100644 index b476e3e1..00000000 --- a/src/NerdBank.GitVersioning.Tests/ManagedGit/GitTreeStreamingReaderTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.IO; -using System.Text; -using Nerdbank.GitVersioning.ManagedGit; -using Xunit; - -namespace ManagedGit -{ - public class GitTreeStreamingReaderTests - { - [Fact] - public void FindBlobTest() - { - using (Stream stream = TestUtilities.GetEmbeddedResource(@"ManagedGit\tree.bin")) - { - var blobObjectId = GitTreeStreamingReader.FindNode(stream, Encoding.UTF8.GetBytes("version.json")); - Assert.Equal("59552a5eed6779aa4e5bb4dc96e80f36bb6e7380", blobObjectId.ToString()); - } - } - - [Fact] - public void FindTreeTest() - { - using (Stream stream = TestUtilities.GetEmbeddedResource(@"ManagedGit\tree.bin")) - { - var blobObjectId = GitTreeStreamingReader.FindNode(stream, Encoding.UTF8.GetBytes("tools")); - Assert.Equal("ec8e91fc4ad13d6a214584330f26d7a05495c8cc", blobObjectId.ToString()); - } - } - } -} diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/StreamExtensionsTests.cs b/src/NerdBank.GitVersioning.Tests/ManagedGit/StreamExtensionsTests.cs deleted file mode 100644 index c0b1c6ec..00000000 --- a/src/NerdBank.GitVersioning.Tests/ManagedGit/StreamExtensionsTests.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.IO; -using Xunit; -using Nerdbank.GitVersioning.ManagedGit; - -namespace ManagedGit -{ - public class StreamExtensionsTests - { - [Fact] - public void ReadTest() - { - byte[] data = new byte[] { 0b10010001, 0b00101110 }; - - using (MemoryStream stream = new MemoryStream(data)) - { - Assert.Equal(5905, stream.ReadMbsInt()); - } - } - } -} diff --git a/src/NerdBank.GitVersioning.Tests/NerdBank.GitVersioning.Tests.csproj b/src/NerdBank.GitVersioning.Tests/NerdBank.GitVersioning.Tests.csproj deleted file mode 100644 index 14fe5031..00000000 --- a/src/NerdBank.GitVersioning.Tests/NerdBank.GitVersioning.Tests.csproj +++ /dev/null @@ -1,53 +0,0 @@ - - - net5.0;net461 - true - true - full - false - false - true - - - - - - - false - Targets\%(FileName)%(Extension) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - - diff --git a/src/NerdBank.GitVersioning/AssemblyVersionOptionsConverter.cs b/src/NerdBank.GitVersioning/AssemblyVersionOptionsConverter.cs index 0e88f638..01efeeb5 100644 --- a/src/NerdBank.GitVersioning/AssemblyVersionOptionsConverter.cs +++ b/src/NerdBank.GitVersioning/AssemblyVersionOptionsConverter.cs @@ -1,74 +1,73 @@ -namespace Nerdbank.GitVersioning +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Reflection; +using Newtonsoft.Json; + +namespace Nerdbank.GitVersioning; + +internal class AssemblyVersionOptionsConverter : JsonConverter { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - using System.Text; - using System.Threading.Tasks; - using Newtonsoft.Json; - using Newtonsoft.Json.Linq; + private readonly bool includeDefaults; - internal class AssemblyVersionOptionsConverter : JsonConverter + internal AssemblyVersionOptionsConverter(bool includeDefaults) { - private readonly bool includeDefaults; - - internal AssemblyVersionOptionsConverter(bool includeDefaults) - { - this.includeDefaults = includeDefaults; - } + this.includeDefaults = includeDefaults; + } - public override bool CanConvert(Type objectType) - { - return typeof(VersionOptions.AssemblyVersionOptions).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo()); - } + /// + public override bool CanConvert(Type objectType) + { + return typeof(VersionOptions.AssemblyVersionOptions).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo()); + } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + /// + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (objectType.Equals(typeof(VersionOptions.AssemblyVersionOptions))) { - if (objectType.Equals(typeof(VersionOptions.AssemblyVersionOptions))) + if (reader.Value is string) { - if (reader.Value is string) - { - Version value; - if (Version.TryParse((string)reader.Value, out value)) - { - return new VersionOptions.AssemblyVersionOptions(value); - } - } - else if (reader.TokenType == JsonToken.StartObject) + Version value; + if (Version.TryParse((string)reader.Value, out value)) { - // Temporarily remove ourselves from the serializer so we don't recurse infinitely. - serializer.Converters.Remove(this); - var result = serializer.Deserialize(reader); - serializer.Converters.Add(this); - return result; + return new VersionOptions.AssemblyVersionOptions(value); } } - - throw new NotSupportedException(); + else if (reader.TokenType == JsonToken.StartObject) + { + // Temporarily remove ourselves from the serializer so we don't recurse infinitely. + serializer.Converters.Remove(this); + VersionOptions.AssemblyVersionOptions result = serializer.Deserialize(reader); + serializer.Converters.Add(this); + return result; + } } - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + throw new NotSupportedException(); + } + + /// + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var data = value as VersionOptions.AssemblyVersionOptions; + if (data is not null) { - var data = value as VersionOptions.AssemblyVersionOptions; - if (data is not null) + if (data.PrecisionOrDefault == VersionOptions.DefaultVersionPrecision && !this.includeDefaults) { - if (data.PrecisionOrDefault == VersionOptions.DefaultVersionPrecision && !this.includeDefaults) - { - serializer.Serialize(writer, data.Version); - return; - } - else - { - // Temporarily remove ourselves from the serializer so we don't recurse infinitely. - serializer.Converters.Remove(this); - serializer.Serialize(writer, data); - serializer.Converters.Add(this); - return; - } + serializer.Serialize(writer, data.Version); + return; + } + else + { + // Temporarily remove ourselves from the serializer so we don't recurse infinitely. + serializer.Converters.Remove(this); + serializer.Serialize(writer, data); + serializer.Converters.Add(this); + return; } - - throw new NotSupportedException(); } + + throw new NotSupportedException(); } } diff --git a/src/NerdBank.GitVersioning/CloudBuild.cs b/src/NerdBank.GitVersioning/CloudBuild.cs index e5417145..0c7c8a25 100644 --- a/src/NerdBank.GitVersioning/CloudBuild.cs +++ b/src/NerdBank.GitVersioning/CloudBuild.cs @@ -1,55 +1,57 @@ -namespace Nerdbank.GitVersioning -{ - using System; - using System.Linq; - using CloudBuildServices; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Nerdbank.GitVersioning.CloudBuildServices; +namespace Nerdbank.GitVersioning; + +/// +/// Provides access to cloud build providers. +/// +public static class CloudBuild +{ /// - /// Provides access to cloud build providers. + /// An array of cloud build systems we support. /// - public static class CloudBuild + public static readonly ICloudBuild[] SupportedCloudBuilds = new ICloudBuild[] { - /// - /// An array of cloud build systems we support. - /// - public static readonly ICloudBuild[] SupportedCloudBuilds = new ICloudBuild[] { - new AppVeyor(), - new VisualStudioTeamServices(), - new GitHubActions(), - new TeamCity(), - new AtlassianBamboo(), - new Jenkins(), - new GitLab(), - new Travis(), - new SpaceAutomation(), - }; + new AppVeyor(), + new VisualStudioTeamServices(), + new GitHubActions(), + new TeamCity(), + new AtlassianBamboo(), + new Jenkins(), + new GitLab(), + new Travis(), + new SpaceAutomation(), + new BitbucketCloud(), + }; - /// - /// Gets the cloud build provider that applies to this build, if any. - /// - public static ICloudBuild Active => SupportedCloudBuilds.FirstOrDefault(cb => cb.IsApplicable); - - /// - /// Gets the specified string, prefixing it with some value if it is non-empty and lacks the prefix. - /// - /// The prefix that should be included in the returned value. - /// The value to prefix. - /// The provided, with prepended - /// if the value doesn't already start with that string and the value is non-empty. - internal static string ShouldStartWith(string value, string prefix) - { - return - string.IsNullOrEmpty(value) ? value : - value.StartsWith(prefix, StringComparison.Ordinal) ? value : - prefix + value; - } + /// + /// Gets the cloud build provider that applies to this build, if any. + /// + public static ICloudBuild Active => SupportedCloudBuilds.FirstOrDefault(cb => cb.IsApplicable); - /// - /// Gets the specified string if it starts with a given prefix; otherwise null. - /// - /// The value to return. - /// The prefix to check for. - /// if it starts with ; otherwise null. - internal static string IfStartsWith(string value, string prefix) => value is object && value.StartsWith(prefix, StringComparison.Ordinal) ? value : null; + /// + /// Gets the specified string, prefixing it with some value if it is non-empty and lacks the prefix. + /// + /// The value to prefix. + /// The prefix that should be included in the returned value. + /// The provided, with prepended + /// if the value doesn't already start with that string and the value is non-empty. + internal static string ShouldStartWith(string value, string prefix) + { + return + string.IsNullOrEmpty(value) ? value : + value.StartsWith(prefix, StringComparison.Ordinal) ? value : + prefix + value; } + + /// + /// Gets the specified string if it starts with a given prefix; otherwise null. + /// + /// The value to return. + /// The prefix to check for. + /// if it starts with ; otherwise . + internal static string IfStartsWith(string value, string prefix) => value is object && value.StartsWith(prefix, StringComparison.Ordinal) ? value : null; } diff --git a/src/NerdBank.GitVersioning/CloudBuildServices/AppVeyor.cs b/src/NerdBank.GitVersioning/CloudBuildServices/AppVeyor.cs index 08ac7147..24bee227 100644 --- a/src/NerdBank.GitVersioning/CloudBuildServices/AppVeyor.cs +++ b/src/NerdBank.GitVersioning/CloudBuildServices/AppVeyor.cs @@ -1,72 +1,74 @@ -namespace Nerdbank.GitVersioning.CloudBuildServices -{ - using System; - using System.Collections.Generic; - using System.ComponentModel; - using System.Diagnostics; - using System.IO; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.ComponentModel; +using System.Diagnostics; + +namespace Nerdbank.GitVersioning.CloudBuildServices; - /// - /// - /// +/// +/// Cloud build handling for AppVeyor. +/// +/// +/// The AppVeyor-specific properties referenced here are documented here. +/// +internal class AppVeyor : ICloudBuild +{ + /// /// - /// The AppVeyor-specific properties referenced here are documented here: - /// http://www.appveyor.com/docs/environment-variables + /// AppVeyor's branch variable is the target branch of a PR, which is *NOT* to be misinterpreted + /// as building the target branch itself. So only set the branch built property if it's not a PR. /// - internal class AppVeyor : ICloudBuild - { - /// - /// - /// - /// - /// AppVeyor's branch variable is the target branch of a PR, which is *NOT* to be misinterpreted - /// as building the target branch itself. So only set the branch built property if it's not a PR. - /// - public string BuildingBranch => !this.IsPullRequest && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("APPVEYOR_REPO_BRANCH")) - ? $"refs/heads/{Environment.GetEnvironmentVariable("APPVEYOR_REPO_BRANCH")}" - : null; + public string BuildingBranch => !this.IsPullRequest && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("APPVEYOR_REPO_BRANCH")) + ? $"refs/heads/{Environment.GetEnvironmentVariable("APPVEYOR_REPO_BRANCH")}" + : null; - public string BuildingRef => null; + public string BuildingRef => null; - public string BuildingTag => string.Equals("true", Environment.GetEnvironmentVariable("APPVEYOR_REPO_TAG"), StringComparison.OrdinalIgnoreCase) - ? $"refs/tags/{Environment.GetEnvironmentVariable("APPVEYOR_REPO_TAG_NAME")}" - : null; + /// + public string BuildingTag => string.Equals("true", Environment.GetEnvironmentVariable("APPVEYOR_REPO_TAG"), StringComparison.OrdinalIgnoreCase) + ? $"refs/tags/{Environment.GetEnvironmentVariable("APPVEYOR_REPO_TAG_NAME")}" + : null; - public string GitCommitId => null; + /// + public string GitCommitId => null; - public bool IsApplicable => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("APPVEYOR")); + /// + public bool IsApplicable => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("APPVEYOR")); - public bool IsPullRequest => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("APPVEYOR_PULL_REQUEST_NUMBER")); + /// + public bool IsPullRequest => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("APPVEYOR_PULL_REQUEST_NUMBER")); - public IReadOnlyDictionary SetCloudBuildNumber(string buildNumber, TextWriter stdout, TextWriter stderr) - { - // We ignore exit code so as to not fail the build when the cloud build number is not unique. - RunAppveyor($"UpdateBuild -Version \"{buildNumber}\"", stdout, stderr); - return new Dictionary(); - } + /// + public IReadOnlyDictionary SetCloudBuildNumber(string buildNumber, TextWriter stdout, TextWriter stderr) + { + // We ignore exit code so as to not fail the build when the cloud build number is not unique. + RunAppveyor($"UpdateBuild -Version \"{buildNumber}\"", stdout, stderr); + return new Dictionary(); + } - public IReadOnlyDictionary SetCloudBuildVariable(string name, string value, TextWriter stdout, TextWriter stderr) - { - RunAppveyor($"SetVariable -Name {name} -Value \"{value}\"", stdout, stderr); - return new Dictionary(); - } + /// + public IReadOnlyDictionary SetCloudBuildVariable(string name, string value, TextWriter stdout, TextWriter stderr) + { + RunAppveyor($"SetVariable -Name {name} -Value \"{value}\"", stdout, stderr); + return new Dictionary(); + } - private static void RunAppveyor(string args, TextWriter stdout, TextWriter stderr) + private static void RunAppveyor(string args, TextWriter stdout, TextWriter stderr) + { + try { - try + // Skip this if this build is running in our own unit tests, since that can + // mess with AppVeyor's actual build information. + if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("_NBGV_UnitTest"))) { - // Skip this if this build is running in our own unit tests, since that can - // mess with AppVeyor's actual build information. - if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("_NBGV_UnitTest"))) - { - Process.Start("appveyor", args) - .WaitForExit(); - } - } - catch (Win32Exception ex) when ((uint)ex.HResult == 0x80004005) - { - (stderr ?? Console.Error).WriteLine("Could not find appveyor tool to set cloud build variable."); + Process.Start("appveyor", args) + .WaitForExit(); } } + catch (Win32Exception ex) when ((uint)ex.HResult == 0x80004005) + { + (stderr ?? Console.Error).WriteLine("Could not find appveyor tool to set cloud build variable."); + } } } diff --git a/src/NerdBank.GitVersioning/CloudBuildServices/AtlassianBamboo.cs b/src/NerdBank.GitVersioning/CloudBuildServices/AtlassianBamboo.cs index fe418113..72a395ee 100644 --- a/src/NerdBank.GitVersioning/CloudBuildServices/AtlassianBamboo.cs +++ b/src/NerdBank.GitVersioning/CloudBuildServices/AtlassianBamboo.cs @@ -1,38 +1,42 @@ -namespace Nerdbank.GitVersioning.CloudBuildServices +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Nerdbank.GitVersioning.CloudBuildServices; + +/// +/// Cloud build handling for Atlassian Bamboo. +/// +/// +/// The Bamboo-specific properties referenced here are documented here. +/// +internal class AtlassianBamboo : ICloudBuild { - using System; - using System.Collections.Generic; - using System.IO; - - /// - /// - /// - /// - /// The Bamboo-specific properties referenced here are documented here: - /// https://confluence.atlassian.com/bamboo/bamboo-variables-289277087.html - /// - internal class AtlassianBamboo : ICloudBuild - { - public bool IsPullRequest => false; + /// + public bool IsPullRequest => false; - public string BuildingTag => null; + /// + public string BuildingTag => null; - public string BuildingBranch => CloudBuild.ShouldStartWith(Environment.GetEnvironmentVariable("bamboo.planRepository.branch"), "refs/heads/"); + /// + public string BuildingBranch => CloudBuild.ShouldStartWith(Environment.GetEnvironmentVariable("bamboo.planRepository.branch"), "refs/heads/"); - public string BuildingRef => this.BuildingBranch; + public string BuildingRef => this.BuildingBranch; - public string GitCommitId => Environment.GetEnvironmentVariable("bamboo.planRepository.revision"); + /// + public string GitCommitId => Environment.GetEnvironmentVariable("bamboo.planRepository.revision"); - public bool IsApplicable => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("bamboo.buildKey")); + /// + public bool IsApplicable => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("bamboo.buildKey")); - public IReadOnlyDictionary SetCloudBuildNumber(string buildNumber, TextWriter stdout, TextWriter stderr) - { - return new Dictionary(); - } + /// + public IReadOnlyDictionary SetCloudBuildNumber(string buildNumber, TextWriter stdout, TextWriter stderr) + { + return new Dictionary(); + } - public IReadOnlyDictionary SetCloudBuildVariable(string name, string value, TextWriter stdout, TextWriter stderr) - { - return new Dictionary(); - } + /// + public IReadOnlyDictionary SetCloudBuildVariable(string name, string value, TextWriter stdout, TextWriter stderr) + { + return new Dictionary(); } } diff --git a/src/NerdBank.GitVersioning/CloudBuildServices/BitbucketCloud.cs b/src/NerdBank.GitVersioning/CloudBuildServices/BitbucketCloud.cs new file mode 100644 index 00000000..ea4592fd --- /dev/null +++ b/src/NerdBank.GitVersioning/CloudBuildServices/BitbucketCloud.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Nerdbank.GitVersioning.CloudBuildServices; + +/// +/// Cloud build handling for Bitbucket Cloud. +/// +/// +/// The Bitbucket-specific properties referenced here are documented here. +/// +public class BitbucketCloud : ICloudBuild +{ + /// + public bool IsApplicable => !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("BITBUCKET_PIPELINE_UUID")) && + !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("BITBUCKET_STEP_UUID")) && + !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("BITBUCKET_STEP_TRIGGERER_UUID")); + + /// + public bool IsPullRequest => !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("BITBUCKET_PR_ID")); + + /// + public string BuildingBranch => Environment.GetEnvironmentVariable("BITBUCKET_BRANCH"); + + /// + public string BuildingTag => Environment.GetEnvironmentVariable("BITBUCKET_TAG"); + + /// + public string GitCommitId => Environment.GetEnvironmentVariable("BITBUCKET_COMMIT"); + + /// + public IReadOnlyDictionary SetCloudBuildNumber(string buildNumber, TextWriter stdout, TextWriter stderr) + { + return new Dictionary(); + } + + /// + public IReadOnlyDictionary SetCloudBuildVariable(string name, string value, TextWriter stdout, TextWriter stderr) + { + return new Dictionary(); + } +} diff --git a/src/NerdBank.GitVersioning/CloudBuildServices/GitHubActions.cs b/src/NerdBank.GitVersioning/CloudBuildServices/GitHubActions.cs index f3931042..d43a885b 100644 --- a/src/NerdBank.GitVersioning/CloudBuildServices/GitHubActions.cs +++ b/src/NerdBank.GitVersioning/CloudBuildServices/GitHubActions.cs @@ -1,45 +1,58 @@ -namespace Nerdbank.GitVersioning.CloudBuildServices +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Nerdbank.GitVersioning.CloudBuildServices; + +internal class GitHubActions : ICloudBuild { - using System; - using System.Collections.Generic; - using System.IO; - using Nerdbank.GitVersioning; + /// + public bool IsApplicable => Environment.GetEnvironmentVariable("GITHUB_ACTIONS") == "true"; - internal class GitHubActions : ICloudBuild - { - public bool IsApplicable => Environment.GetEnvironmentVariable("GITHUB_ACTIONS") == "true"; + /// + public bool IsPullRequest => Environment.GetEnvironmentVariable("GITHUB_EVENT_NAME") == "PullRequestEvent"; - public bool IsPullRequest => Environment.GetEnvironmentVariable("GITHUB_EVENT_NAME") == "PullRequestEvent"; + /// + public string BuildingBranch => (BuildingRef?.StartsWith("refs/heads/") ?? false) ? BuildingRef : null; - public string BuildingBranch => (BuildingRef?.StartsWith("refs/heads/") ?? false) ? BuildingRef : null; + /// + public string BuildingTag => (BuildingRef?.StartsWith("refs/tags/") ?? false) ? BuildingRef : null; - public string BuildingTag => (BuildingRef?.StartsWith("refs/tags/") ?? false) ? BuildingRef : null; + /// + public string GitCommitId => IgnoreGitHubRef ? null : Environment.GetEnvironmentVariable("GITHUB_SHA"); - public string GitCommitId => Environment.GetEnvironmentVariable("GITHUB_SHA"); + private static string BuildingRef => IgnoreGitHubRef ? null : Environment.GetEnvironmentVariable("GITHUB_REF"); - private static string BuildingRef => Environment.GetEnvironmentVariable("GITHUB_REF"); + /// + /// Gets a value indicating whether to ignore GitHub Actions environment variables that indicate where HEAD is. + /// + /// + /// This is useful in a GitHub workflow where HEAD was moved by some prior Action, such that the environment variables are stale. + /// GitHub Actions does not allow these env vars to be changed mid-workflow, so in such cases NB.GV should just use HEAD. + /// + private static bool IgnoreGitHubRef => string.Equals(Environment.GetEnvironmentVariable("IGNORE_GITHUB_REF"), "true", StringComparison.OrdinalIgnoreCase); - private static string EnvironmentFile => Environment.GetEnvironmentVariable("GITHUB_ENV"); + private static string EnvironmentFile => Environment.GetEnvironmentVariable("GITHUB_ENV"); - public IReadOnlyDictionary SetCloudBuildNumber(string buildNumber, TextWriter stdout, TextWriter stderr) - { - return new Dictionary(); - } + /// + public IReadOnlyDictionary SetCloudBuildNumber(string buildNumber, TextWriter stdout, TextWriter stderr) + { + return new Dictionary(); + } - public IReadOnlyDictionary SetCloudBuildVariable(string name, string value, TextWriter stdout, TextWriter stderr) - { - Utilities.FileOperationWithRetry(() => File.AppendAllLines(EnvironmentFile, new [] {$"{name}={value}"})); - return GetDictionaryFor(name, value); - } + /// + public IReadOnlyDictionary SetCloudBuildVariable(string name, string value, TextWriter stdout, TextWriter stderr) + { + Utilities.FileOperationWithRetry(() => File.AppendAllLines(EnvironmentFile, new[] { $"{name}={value}" })); + return GetDictionaryFor(name, value); + } - private static IReadOnlyDictionary GetDictionaryFor(string variableName, string value) + private static IReadOnlyDictionary GetDictionaryFor(string variableName, string value) + { + return new Dictionary(StringComparer.OrdinalIgnoreCase) { - return new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { GetEnvironmentVariableNameForVariable(variableName), value }, - }; - } - - private static string GetEnvironmentVariableNameForVariable(string name) => name.ToUpperInvariant().Replace('.', '_'); + { GetEnvironmentVariableNameForVariable(variableName), value }, + }; } + + private static string GetEnvironmentVariableNameForVariable(string name) => name.ToUpperInvariant().Replace('.', '_'); } diff --git a/src/NerdBank.GitVersioning/CloudBuildServices/GitLab.cs b/src/NerdBank.GitVersioning/CloudBuildServices/GitLab.cs index 21abebd2..f02e4e17 100644 --- a/src/NerdBank.GitVersioning/CloudBuildServices/GitLab.cs +++ b/src/NerdBank.GitVersioning/CloudBuildServices/GitLab.cs @@ -1,42 +1,46 @@ -namespace Nerdbank.GitVersioning.CloudBuildServices +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Nerdbank.GitVersioning.CloudBuildServices; + +/// +/// Cloud build handling for GitLab. +/// +/// +/// The GitLab-specific properties referenced here are documented here. +/// +internal class GitLab : ICloudBuild { - using System; - using System.Collections.Generic; - using System.IO; - - /// - /// - /// - /// - /// The GitLab-specific properties referenced here are documented here: - /// https://docs.gitlab.com/ce/ci/variables/README.html - /// - internal class GitLab : ICloudBuild - { - public string BuildingBranch => - Environment.GetEnvironmentVariable("CI_COMMIT_TAG") is null ? - $"refs/heads/{Environment.GetEnvironmentVariable("CI_COMMIT_REF_NAME")}" : null; + /// + public string BuildingBranch => + Environment.GetEnvironmentVariable("CI_COMMIT_TAG") is null ? + $"refs/heads/{Environment.GetEnvironmentVariable("CI_COMMIT_REF_NAME")}" : null; - public string BuildingRef => this.BuildingBranch ?? this.BuildingTag; + public string BuildingRef => this.BuildingBranch ?? this.BuildingTag; - public string BuildingTag => - Environment.GetEnvironmentVariable("CI_COMMIT_TAG") is not null ? - $"refs/tags/{Environment.GetEnvironmentVariable("CI_COMMIT_TAG")}" : null; + /// + public string BuildingTag => + Environment.GetEnvironmentVariable("CI_COMMIT_TAG") is not null ? + $"refs/tags/{Environment.GetEnvironmentVariable("CI_COMMIT_TAG")}" : null; - public string GitCommitId => Environment.GetEnvironmentVariable("CI_COMMIT_SHA"); + /// + public string GitCommitId => Environment.GetEnvironmentVariable("CI_COMMIT_SHA"); - public bool IsApplicable => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITLAB_CI")); + /// + public bool IsApplicable => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITLAB_CI")); - public bool IsPullRequest => false; + /// + public bool IsPullRequest => false; - public IReadOnlyDictionary SetCloudBuildNumber(string buildNumber, TextWriter stdout, TextWriter stderr) - { - return new Dictionary(); - } + /// + public IReadOnlyDictionary SetCloudBuildNumber(string buildNumber, TextWriter stdout, TextWriter stderr) + { + return new Dictionary(); + } - public IReadOnlyDictionary SetCloudBuildVariable(string name, string value, TextWriter stdout, TextWriter stderr) - { - return new Dictionary(); - } + /// + public IReadOnlyDictionary SetCloudBuildVariable(string name, string value, TextWriter stdout, TextWriter stderr) + { + return new Dictionary(); } } diff --git a/src/NerdBank.GitVersioning/CloudBuildServices/Jenkins.cs b/src/NerdBank.GitVersioning/CloudBuildServices/Jenkins.cs index 688e6b65..e5534c55 100644 --- a/src/NerdBank.GitVersioning/CloudBuildServices/Jenkins.cs +++ b/src/NerdBank.GitVersioning/CloudBuildServices/Jenkins.cs @@ -1,64 +1,72 @@ -namespace Nerdbank.GitVersioning.CloudBuildServices -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.Text; - - /// - /// The Jenkins-specific properties referenced here are documented here: - /// https://wiki.jenkins-ci.org/display/JENKINS/Git+Plugin#GitPlugin-Environmentvariables - /// - internal class Jenkins : ICloudBuild - { - private static readonly Encoding UTF8NoBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. - public string BuildingTag => null; +using System.Text; - public bool IsPullRequest => false; +namespace Nerdbank.GitVersioning.CloudBuildServices; - public string BuildingBranch => CloudBuild.ShouldStartWith(Branch, "refs/heads/"); +/// +/// Cloud build handling for Jenkins. +/// +/// +/// The Jenkins-specific properties referenced here are documented here. +/// +internal class Jenkins : ICloudBuild +{ + private static readonly Encoding UTF8NoBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); - public string GitCommitId => Environment.GetEnvironmentVariable("GIT_COMMIT"); + /// + public string BuildingTag => null; - public bool IsApplicable => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("JENKINS_URL")); + /// + public bool IsPullRequest => false; - private static string Branch => - Environment.GetEnvironmentVariable("GIT_LOCAL_BRANCH") - ?? Environment.GetEnvironmentVariable("GIT_BRANCH"); + /// + public string BuildingBranch => CloudBuild.ShouldStartWith(Branch, "refs/heads/"); - public IReadOnlyDictionary SetCloudBuildNumber(string buildNumber, TextWriter stdout, TextWriter stderr) - { - WriteVersionFile(buildNumber); + /// + public string GitCommitId => Environment.GetEnvironmentVariable("GIT_COMMIT"); - stdout.WriteLine($"## GIT_VERSION: {buildNumber}"); + /// + public bool IsApplicable => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("JENKINS_URL")); - return new Dictionary - { - { "GIT_VERSION", buildNumber } - }; - } + private static string Branch => + Environment.GetEnvironmentVariable("GIT_LOCAL_BRANCH") + ?? Environment.GetEnvironmentVariable("GIT_BRANCH"); - public IReadOnlyDictionary SetCloudBuildVariable(string name, string value, TextWriter stdout, TextWriter stderr) - { - return new Dictionary - { - { name, value } - }; - } + /// + public IReadOnlyDictionary SetCloudBuildNumber(string buildNumber, TextWriter stdout, TextWriter stderr) + { + WriteVersionFile(buildNumber); + + stdout.WriteLine($"## GIT_VERSION: {buildNumber}"); - private static void WriteVersionFile(string buildNumber) + return new Dictionary { - var workspacePath = Environment.GetEnvironmentVariable("WORKSPACE"); + { "GIT_VERSION", buildNumber }, + }; + } - if (string.IsNullOrEmpty(workspacePath)) - { - return; - } + /// + public IReadOnlyDictionary SetCloudBuildVariable(string name, string value, TextWriter stdout, TextWriter stderr) + { + return new Dictionary + { + { name, value }, + }; + } - var versionFilePath = Path.Combine(workspacePath, "jenkins_build_number.txt"); + private static void WriteVersionFile(string buildNumber) + { + string workspacePath = Environment.GetEnvironmentVariable("WORKSPACE"); - Utilities.FileOperationWithRetry(() => File.WriteAllText(versionFilePath, buildNumber, UTF8NoBOM)); + if (string.IsNullOrEmpty(workspacePath)) + { + return; } + + string versionFilePath = Path.Combine(workspacePath, "jenkins_build_number.txt"); + + Utilities.FileOperationWithRetry(() => File.WriteAllText(versionFilePath, buildNumber, UTF8NoBOM)); } } diff --git a/src/NerdBank.GitVersioning/CloudBuildServices/SpaceAutomation.cs b/src/NerdBank.GitVersioning/CloudBuildServices/SpaceAutomation.cs index b3d11a0d..7df2ab28 100644 --- a/src/NerdBank.GitVersioning/CloudBuildServices/SpaceAutomation.cs +++ b/src/NerdBank.GitVersioning/CloudBuildServices/SpaceAutomation.cs @@ -1,38 +1,42 @@ -using System; -using System.Collections.Generic; -using System.IO; - -namespace Nerdbank.GitVersioning.CloudBuildServices +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Nerdbank.GitVersioning.CloudBuildServices; + +/// +/// SpaceAutomation CI build support. +/// +/// +/// The SpaceAutomation-specific properties referenced here are documented here. +/// +internal class SpaceAutomation : ICloudBuild { - /// - /// SpaceAutomation CI build support. - /// - /// - /// The SpaceAutomation-specific properties referenced here are documented here: - /// https://www.jetbrains.com/help/space/automation-environment-variables.html - /// - internal class SpaceAutomation : ICloudBuild - { - public string BuildingBranch => CloudBuild.IfStartsWith(BuildingRef, "refs/heads/"); + /// + public string BuildingBranch => CloudBuild.IfStartsWith(BuildingRef, "refs/heads/"); - public string BuildingTag => CloudBuild.IfStartsWith(BuildingRef, "refs/tags/"); + /// + public string BuildingTag => CloudBuild.IfStartsWith(BuildingRef, "refs/tags/"); - public string GitCommitId => Environment.GetEnvironmentVariable("JB_SPACE_GIT_REVISION"); + /// + public string GitCommitId => Environment.GetEnvironmentVariable("JB_SPACE_GIT_REVISION"); - public bool IsApplicable => this.GitCommitId is not null; + /// + public bool IsApplicable => this.GitCommitId is not null; - public bool IsPullRequest => false; + /// + public bool IsPullRequest => false; - private static string BuildingRef => Environment.GetEnvironmentVariable("JB_SPACE_GIT_BRANCH"); + private static string BuildingRef => Environment.GetEnvironmentVariable("JB_SPACE_GIT_BRANCH"); - public IReadOnlyDictionary SetCloudBuildNumber(string buildNumber, TextWriter stdout, TextWriter stderr) - { - return new Dictionary(); - } + /// + public IReadOnlyDictionary SetCloudBuildNumber(string buildNumber, TextWriter stdout, TextWriter stderr) + { + return new Dictionary(); + } - public IReadOnlyDictionary SetCloudBuildVariable(string name, string value, TextWriter stdout, TextWriter stderr) - { - return new Dictionary(); - } + /// + public IReadOnlyDictionary SetCloudBuildVariable(string name, string value, TextWriter stdout, TextWriter stderr) + { + return new Dictionary(); } -} \ No newline at end of file +} diff --git a/src/NerdBank.GitVersioning/CloudBuildServices/TeamCity.cs b/src/NerdBank.GitVersioning/CloudBuildServices/TeamCity.cs index 828798ee..89f5b243 100644 --- a/src/NerdBank.GitVersioning/CloudBuildServices/TeamCity.cs +++ b/src/NerdBank.GitVersioning/CloudBuildServices/TeamCity.cs @@ -1,42 +1,46 @@ -namespace Nerdbank.GitVersioning.CloudBuildServices +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Nerdbank.GitVersioning.CloudBuildServices; + +/// +/// TeamCity CI build support. +/// +/// +/// The TeamCity-specific properties referenced here are documented here. +/// +internal class TeamCity : ICloudBuild { - using System; - using System.Collections.Generic; - using System.IO; - - /// - /// TeamCity CI build support. - /// - /// - /// The TeamCity-specific properties referenced here are documented here: - /// https://www.jetbrains.com/help/teamcity/predefined-build-parameters.html - /// - internal class TeamCity : ICloudBuild - { - public string BuildingBranch => CloudBuild.IfStartsWith(BuildingRef, "refs/heads/"); + /// + public string BuildingBranch => CloudBuild.IfStartsWith(BuildingRef, "refs/heads/"); - public string BuildingTag => CloudBuild.IfStartsWith(BuildingRef, "refs/tags/"); + /// + public string BuildingTag => CloudBuild.IfStartsWith(BuildingRef, "refs/tags/"); - public string GitCommitId => Environment.GetEnvironmentVariable("BUILD_VCS_NUMBER"); + /// + public string GitCommitId => Environment.GetEnvironmentVariable("BUILD_VCS_NUMBER"); - public bool IsApplicable => this.GitCommitId is not null; + /// + public bool IsApplicable => this.GitCommitId is not null; - public bool IsPullRequest => false; + /// + public bool IsPullRequest => false; - private static string BuildingRef => Environment.GetEnvironmentVariable("BUILD_GIT_BRANCH"); + private static string BuildingRef => Environment.GetEnvironmentVariable("BUILD_GIT_BRANCH"); - public IReadOnlyDictionary SetCloudBuildNumber(string buildNumber, TextWriter stdout, TextWriter stderr) - { - (stdout ?? Console.Out).WriteLine($"##teamcity[buildNumber '{buildNumber}']"); - return new Dictionary(); - } + /// + public IReadOnlyDictionary SetCloudBuildNumber(string buildNumber, TextWriter stdout, TextWriter stderr) + { + (stdout ?? Console.Out).WriteLine($"##teamcity[buildNumber '{buildNumber}']"); + return new Dictionary(); + } - public IReadOnlyDictionary SetCloudBuildVariable(string name, string value, TextWriter stdout, TextWriter stderr) - { - (stdout ?? Console.Out).WriteLine($"##teamcity[setParameter name='{name}' value='{value}']"); - (stdout ?? Console.Out).WriteLine($"##teamcity[setParameter name='system.{name}' value='{value}']"); + /// + public IReadOnlyDictionary SetCloudBuildVariable(string name, string value, TextWriter stdout, TextWriter stderr) + { + (stdout ?? Console.Out).WriteLine($"##teamcity[setParameter name='{name}' value='{value}']"); + (stdout ?? Console.Out).WriteLine($"##teamcity[setParameter name='system.{name}' value='{value}']"); - return new Dictionary(); - } + return new Dictionary(); } } diff --git a/src/NerdBank.GitVersioning/CloudBuildServices/Travis.cs b/src/NerdBank.GitVersioning/CloudBuildServices/Travis.cs index 1c57a4c8..21e6ed0b 100644 --- a/src/NerdBank.GitVersioning/CloudBuildServices/Travis.cs +++ b/src/NerdBank.GitVersioning/CloudBuildServices/Travis.cs @@ -1,37 +1,42 @@ -namespace Nerdbank.GitVersioning.CloudBuildServices +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Nerdbank.GitVersioning.CloudBuildServices; + +/// +/// Travis CI build support. +/// +/// +/// The Travis CI environment variables referenced here are documented here. +/// +internal class Travis : ICloudBuild { - using System; - using System.Collections.Generic; - using System.IO; - - /// - /// Travis CI build support. - /// - /// - /// The Travis CI environment variables referenced here are documented here: - /// https://docs.travis-ci.com/user/environment-variables/#default-environment-variables - /// - internal class Travis: ICloudBuild - { - // TRAVIS_BRANCH can reference a branch or a tag, so make sure it starts with refs/heads - public string BuildingBranch => CloudBuild.ShouldStartWith(Environment.GetEnvironmentVariable("TRAVIS_BRANCH"), "refs/heads/"); + // TRAVIS_BRANCH can reference a branch or a tag, so make sure it starts with refs/heads - public string BuildingTag => Environment.GetEnvironmentVariable("TRAVIS_TAG"); + /// + public string BuildingBranch => CloudBuild.ShouldStartWith(Environment.GetEnvironmentVariable("TRAVIS_BRANCH"), "refs/heads/"); - public string GitCommitId => Environment.GetEnvironmentVariable("TRAVIS_COMMIT"); + /// + public string BuildingTag => Environment.GetEnvironmentVariable("TRAVIS_TAG"); - public bool IsApplicable => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TRAVIS")); + /// + public string GitCommitId => Environment.GetEnvironmentVariable("TRAVIS_COMMIT"); - public bool IsPullRequest => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TRAVIS_PULL_REQUEST_BRANCH")); + /// + public bool IsApplicable => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TRAVIS")); - public IReadOnlyDictionary SetCloudBuildNumber(string buildNumber, TextWriter stdout, TextWriter stderr) - { - return new Dictionary(); - } + /// + public bool IsPullRequest => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TRAVIS_PULL_REQUEST_BRANCH")); - public IReadOnlyDictionary SetCloudBuildVariable(string name, string value, TextWriter stdout, TextWriter stderr) - { - return new Dictionary(); - } + /// + public IReadOnlyDictionary SetCloudBuildNumber(string buildNumber, TextWriter stdout, TextWriter stderr) + { + return new Dictionary(); + } + + /// + public IReadOnlyDictionary SetCloudBuildVariable(string name, string value, TextWriter stdout, TextWriter stderr) + { + return new Dictionary(); } } diff --git a/src/NerdBank.GitVersioning/CloudBuildServices/VisualStudioTeamServices.cs b/src/NerdBank.GitVersioning/CloudBuildServices/VisualStudioTeamServices.cs index fcaf7f1d..f9ed964c 100644 --- a/src/NerdBank.GitVersioning/CloudBuildServices/VisualStudioTeamServices.cs +++ b/src/NerdBank.GitVersioning/CloudBuildServices/VisualStudioTeamServices.cs @@ -1,51 +1,59 @@ -namespace Nerdbank.GitVersioning.CloudBuildServices +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Nerdbank.GitVersioning.CloudBuildServices; + +/// +/// Cloud build handling for Azure DevOps. +/// +/// +/// The VSTS-specific properties referenced here are documented here. +/// +internal class VisualStudioTeamServices : ICloudBuild { - using System; - using System.Collections.Generic; - using System.IO; - - /// - /// - /// - /// - /// The VSTS-specific properties referenced here are documented here: - /// https://msdn.microsoft.com/en-us/Library/vs/alm/Build/scripts/variables - /// - internal class VisualStudioTeamServices : ICloudBuild - { - public bool IsPullRequest => BuildingRef?.StartsWith("refs/pull/") ?? false; + /// + public bool IsPullRequest => BuildingRef?.StartsWith("refs/pull/") ?? false; - public string BuildingTag => CloudBuild.IfStartsWith(BuildingRef, "refs/tags/"); + /// + public string BuildingTag => CloudBuild.IfStartsWith(BuildingRef, "refs/tags/"); - public string BuildingBranch => CloudBuild.IfStartsWith(BuildingRef, "refs/heads/"); + /// + public string BuildingBranch => CloudBuild.IfStartsWith(BuildingRef, "refs/heads/"); - public string GitCommitId => null; + /// + public string GitCommitId => null; - public bool IsApplicable => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("SYSTEM_TEAMPROJECTID")); + /// + public bool IsApplicable => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("SYSTEM_TEAMPROJECTID")); - private static string BuildingRef => Environment.GetEnvironmentVariable("BUILD_SOURCEBRANCH"); + private static string BuildingRef => Environment.GetEnvironmentVariable("BUILD_SOURCEBRANCH"); - public IReadOnlyDictionary SetCloudBuildNumber(string buildNumber, TextWriter stdout, TextWriter stderr) - { - (stdout ?? Console.Out).WriteLine($"##vso[build.updatebuildnumber]{buildNumber}"); - return GetDictionaryFor("Build.BuildNumber", buildNumber); - } + /// + public IReadOnlyDictionary SetCloudBuildNumber(string buildNumber, TextWriter stdout, TextWriter stderr) + { + (stdout ?? Console.Out).WriteLine($"##vso[build.updatebuildnumber]{buildNumber}"); + return GetDictionaryFor("Build.BuildNumber", buildNumber); + } - public IReadOnlyDictionary SetCloudBuildVariable(string name, string value, TextWriter stdout, TextWriter stderr) + /// + public IReadOnlyDictionary SetCloudBuildVariable(string name, string value, TextWriter stdout, TextWriter stderr) + { + Utilities.FileOperationWithRetry(() => { - Utilities.FileOperationWithRetry(() => - (stdout ?? Console.Out).WriteLine($"##vso[task.setvariable variable={name};]{value}")); - return GetDictionaryFor(name, value); - } + TextWriter output = stdout ?? Console.Out; + output.WriteLine($"##vso[task.setvariable variable={name};]{value}"); + output.WriteLine($"##vso[task.setvariable variable={name};isOutput=true;]{value}"); + }); + return GetDictionaryFor(name, value); + } - private static IReadOnlyDictionary GetDictionaryFor(string variableName, string value) + private static IReadOnlyDictionary GetDictionaryFor(string variableName, string value) + { + return new Dictionary(StringComparer.OrdinalIgnoreCase) { - return new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { GetEnvironmentVariableNameForVariable(variableName), value }, - }; - } - - private static string GetEnvironmentVariableNameForVariable(string name) => name.ToUpperInvariant().Replace('.', '_'); + { GetEnvironmentVariableNameForVariable(variableName), value }, + }; } + + private static string GetEnvironmentVariableNameForVariable(string name) => name.ToUpperInvariant().Replace('.', '_'); } diff --git a/src/NerdBank.GitVersioning/Commands/CloudCommand.cs b/src/NerdBank.GitVersioning/Commands/CloudCommand.cs index 8a78fa7f..518ab254 100644 --- a/src/NerdBank.GitVersioning/Commands/CloudCommand.cs +++ b/src/NerdBank.GitVersioning/Commands/CloudCommand.cs @@ -1,176 +1,173 @@ -namespace Nerdbank.GitVersioning.Commands +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Validation; + +namespace Nerdbank.GitVersioning.Commands; + +/// +/// Implementation of the "nbgv cloud" command that updates the build environments variables with version variables. +/// +public class CloudCommand { - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using Validation; + private readonly TextWriter stdout; + private readonly TextWriter stderr; /// - /// Implementation of the "nbgv cloud" command that updates the build environments variables with version variables. + /// Initializes a new instance of the class. /// - public class CloudCommand + /// The to write output to (e.g. ). + /// The to write error messages to (e.g. ). + public CloudCommand(TextWriter outputWriter = null, TextWriter errorWriter = null) + { + this.stdout = outputWriter ?? TextWriter.Null; + this.stderr = errorWriter ?? TextWriter.Null; + } + + /// + /// Defines the possible errors of the "cloud" command. + /// + public enum CloudCommandError { /// - /// Defines the possible errors of the "cloud" command + /// The specified CI system was not found. /// - public enum CloudCommandError - { - /// - /// The specified CI system was not found. - /// - NoCloudBuildProviderMatch, - - /// - /// A cloud variable was defined multiple times. - /// - DuplicateCloudVariable, - - /// - /// No supported cloud build environment could be detected. - /// - NoCloudBuildEnvDetected, - } + NoCloudBuildProviderMatch, /// - /// Exception indicating an error while setting build variables. + /// A cloud variable was defined multiple times. /// - public class CloudCommandException : Exception - { - /// - /// Gets the error that occurred. - /// - public CloudCommandError Error { get; } - - /// - /// Initializes a new instance of class. - /// - /// The message that describes the error. - /// The error that occurred. - public CloudCommandException(string message, CloudCommandError error) : base(message) => this.Error = error; - } - - private readonly TextWriter stdout; - private readonly TextWriter stderr; + DuplicateCloudVariable, /// - /// Initializes a new instance of . + /// No supported cloud build environment could be detected. /// - /// The to write output to (e.g. ). - /// The to write error messages to (e.g. ). - public CloudCommand(TextWriter outputWriter = null, TextWriter errorWriter = null) - { - this.stdout = outputWriter ?? TextWriter.Null; - this.stderr = errorWriter ?? TextWriter.Null; - } + NoCloudBuildEnvDetected, + } + private static string[] CloudProviderNames => CloudBuild.SupportedCloudBuilds.Select(cb => cb.GetType().Name).ToArray(); - /// - /// Adds version variables to the the current cloud build environment. - /// - /// Thrown when the build environment could not be updated. - /// - /// The path to the directory which may (or its ancestors may) define the version file. - /// - /// - /// Optionally adds an identifier to the build metadata part of a semantic version. - /// - /// - /// The string to use for the cloud build number. If not specified, the computed version will be used. - /// - /// - /// The CI system to activate. If not specified, auto-detection will be used. - /// - /// - /// Controls whether to define all version variables as cloud build variables. - /// - /// - /// Controls whether to define common version variables as cloud build variables. - /// - /// - /// Additional cloud build variables to define. - /// - /// - /// Force usage of LibGit2 for accessing the git repository. - /// - public void SetBuildVariables(string projectDirectory, IEnumerable metadata, string version, string ciSystem, bool allVars, bool commonVars, IEnumerable> additionalVariables, bool alwaysUseLibGit2) - { - Requires.NotNull(projectDirectory, nameof(projectDirectory)); - Requires.NotNull(additionalVariables, nameof(additionalVariables)); + /// + /// Adds version variables to the the current cloud build environment. + /// + /// Thrown when the build environment could not be updated. + /// + /// The path to the directory which may (or its ancestors may) define the version file. + /// + /// + /// Optionally adds an identifier to the build metadata part of a semantic version. + /// + /// + /// The string to use for the cloud build number. If not specified, the computed version will be used. + /// + /// + /// The CI system to activate. If not specified, auto-detection will be used. + /// + /// + /// Controls whether to define all version variables as cloud build variables. + /// + /// + /// Controls whether to define common version variables as cloud build variables. + /// + /// + /// Additional cloud build variables to define. + /// + /// + /// Force usage of LibGit2 for accessing the git repository. + /// + public void SetBuildVariables(string projectDirectory, IEnumerable metadata, string version, string ciSystem, bool allVars, bool commonVars, IEnumerable> additionalVariables, bool alwaysUseLibGit2) + { + Requires.NotNull(projectDirectory, nameof(projectDirectory)); + Requires.NotNull(additionalVariables, nameof(additionalVariables)); - ICloudBuild activeCloudBuild = CloudBuild.Active; - if (!string.IsNullOrEmpty(ciSystem)) + ICloudBuild activeCloudBuild = CloudBuild.Active; + if (!string.IsNullOrEmpty(ciSystem)) + { + int matchingIndex = Array.FindIndex(CloudProviderNames, m => string.Equals(m, ciSystem, StringComparison.OrdinalIgnoreCase)); + if (matchingIndex == -1) { - int matchingIndex = Array.FindIndex(CloudProviderNames, m => string.Equals(m, ciSystem, StringComparison.OrdinalIgnoreCase)); - if (matchingIndex == -1) - { - throw new CloudCommandException( - $"No cloud provider found by the name: \"{ciSystem}\"", - CloudCommandError.NoCloudBuildProviderMatch); - } - - activeCloudBuild = CloudBuild.SupportedCloudBuilds[matchingIndex]; + throw new CloudCommandException( + $"No cloud provider found by the name: \"{ciSystem}\"", + CloudCommandError.NoCloudBuildProviderMatch); } - using var context = GitContext.Create(projectDirectory, writable: alwaysUseLibGit2); - var oracle = new VersionOracle(context, cloudBuild: activeCloudBuild); - if (metadata is not null) - { - oracle.BuildMetadata.AddRange(metadata); - } + activeCloudBuild = CloudBuild.SupportedCloudBuilds[matchingIndex]; + } + + using var context = GitContext.Create(projectDirectory, engine: alwaysUseLibGit2 ? GitContext.Engine.ReadWrite : GitContext.Engine.ReadOnly); + var oracle = new VersionOracle(context, cloudBuild: activeCloudBuild); + if (metadata is not null) + { + oracle.BuildMetadata.AddRange(metadata); + } - var variables = new Dictionary(); - if (allVars) + var variables = new Dictionary(); + if (allVars) + { + foreach (KeyValuePair pair in oracle.CloudBuildAllVars) { - foreach (var pair in oracle.CloudBuildAllVars) - { - variables.Add(pair.Key, pair.Value); - } + variables.Add(pair.Key, pair.Value); } + } - if (commonVars) + if (commonVars) + { + foreach (KeyValuePair pair in oracle.CloudBuildVersionVars) { - foreach (var pair in oracle.CloudBuildVersionVars) - { - variables.Add(pair.Key, pair.Value); - } + variables.Add(pair.Key, pair.Value); } + } - foreach (var kvp in additionalVariables) + foreach (KeyValuePair kvp in additionalVariables) + { + if (variables.ContainsKey(kvp.Key)) { - if (variables.ContainsKey(kvp.Key)) - { - throw new CloudCommandException( - $"Cloud build variable \"{kvp.Key}\" specified more than once.", - CloudCommandError.DuplicateCloudVariable); - } - - variables[kvp.Key] = kvp.Value; + throw new CloudCommandException( + $"Cloud build variable \"{kvp.Key}\" specified more than once.", + CloudCommandError.DuplicateCloudVariable); } - if (activeCloudBuild is not null) + variables[kvp.Key] = kvp.Value; + } + + if (activeCloudBuild is not null) + { + if (string.IsNullOrEmpty(version)) { - if (string.IsNullOrEmpty(version)) - { - version = oracle.CloudBuildNumber; - } + version = oracle.CloudBuildNumber; + } - activeCloudBuild.SetCloudBuildNumber(version, this.stdout, this.stderr); + activeCloudBuild.SetCloudBuildNumber(version, this.stdout, this.stderr); - foreach (var pair in variables) - { - activeCloudBuild.SetCloudBuildVariable(pair.Key, pair.Value, this.stdout, this.stderr); - } - } - else + foreach (KeyValuePair pair in variables) { - throw new CloudCommandException( - "No cloud build detected.", - CloudCommandError.NoCloudBuildEnvDetected); + activeCloudBuild.SetCloudBuildVariable(pair.Key, pair.Value, this.stdout, this.stderr); } } + else + { + throw new CloudCommandException( + "No cloud build detected.", + CloudCommandError.NoCloudBuildEnvDetected); + } + } + /// + /// Exception indicating an error while setting build variables. + /// + public class CloudCommandException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The error that occurred. + public CloudCommandException(string message, CloudCommandError error) + : base(message) => this.Error = error; - private static string[] CloudProviderNames => CloudBuild.SupportedCloudBuilds.Select(cb => cb.GetType().Name).ToArray(); + /// + /// Gets the error that occurred. + /// + public CloudCommandError Error { get; } } } diff --git a/src/NerdBank.GitVersioning/DisabledGit/DisabledGitContext.cs b/src/NerdBank.GitVersioning/DisabledGit/DisabledGitContext.cs new file mode 100644 index 00000000..baf18012 --- /dev/null +++ b/src/NerdBank.GitVersioning/DisabledGit/DisabledGitContext.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +using System.Diagnostics; + +namespace Nerdbank.GitVersioning; + +[DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] +internal class DisabledGitContext : GitContext +{ + public DisabledGitContext(string workingTreePath) + : base(workingTreePath, null) + { + this.VersionFile = new DisabledGitVersionFile(this); + } + + public override VersionFile VersionFile { get; } + + public override string? GitCommitId => null; + + public override bool IsHead => false; + + public override DateTimeOffset? GitCommitDate => null; + + public override string? HeadCanonicalName => null; + + private string DebuggerDisplay => $"\"{this.WorkingTreePath}\" (disabled-git)"; + + public override void ApplyTag(string name) => throw new NotSupportedException(); + + public override void Stage(string path) => throw new NotSupportedException(); + + public override string GetShortUniqueCommitId(int minLength) => "nerdbankdisabled"; + + public override bool TrySelectCommit(string committish) => true; + + internal override int CalculateVersionHeight(VersionOptions? committedVersion, VersionOptions? workingVersion) => 0; + + internal override Version GetIdAsVersion(VersionOptions? committedVersion, VersionOptions? workingVersion, int versionHeight) => Version0; +} diff --git a/src/NerdBank.GitVersioning/DisabledGit/DisabledGitVersionFile.cs b/src/NerdBank.GitVersioning/DisabledGit/DisabledGitVersionFile.cs new file mode 100644 index 00000000..33fb5099 --- /dev/null +++ b/src/NerdBank.GitVersioning/DisabledGit/DisabledGitVersionFile.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace Nerdbank.GitVersioning; + +internal class DisabledGitVersionFile : VersionFile +{ + public DisabledGitVersionFile(GitContext context) + : base(context) + { + } + + protected new DisabledGitContext Context => (DisabledGitContext)base.Context; + + protected override VersionOptions? GetVersionCore(out string? actualDirectory) + { + actualDirectory = null; + return null; + } +} diff --git a/src/NerdBank.GitVersioning/FilterPath.cs b/src/NerdBank.GitVersioning/FilterPath.cs index 72322045..60029c27 100644 --- a/src/NerdBank.GitVersioning/FilterPath.cs +++ b/src/NerdBank.GitVersioning/FilterPath.cs @@ -1,290 +1,319 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + using System.Text; using Validation; -namespace Nerdbank.GitVersioning +namespace Nerdbank.GitVersioning; + +/// +/// A filter (include or exclude) representing a repo relative path. +/// +public class FilterPath { /// - /// A filter (include or exclude) representing a repo relative path. + /// Initializes a new instance of the class + /// from a pathspec-like string and a relative path within the repository. /// - public class FilterPath + /// + /// A string that supports some pathspec features. + /// This path is relative to . + /// + /// Examples: + /// + /// ../relative/inclusion.txt + /// :/absolute/inclusion.txt + /// :!relative/exclusion.txt + /// :^relative/exclusion.txt + /// :^/absolute/exclusion.txt + /// + /// + /// + /// Path (relative to the root of the repository) that is relative to. + /// Can be empty - which indicates is + /// relative to the root of the repository. + /// + /// Invalid path spec. + public FilterPath(string pathSpec, string relativeTo) { - /// - /// True if this represents an exclude filter. - /// - public bool IsExclude { get; } - - /// - /// if this represents an include filter. - /// - public bool IsInclude => !this.IsExclude; - - /// - /// Path relative to the repository root that this represents. - /// Directories are delimited with forward slashes. - /// - public string RepoRelativePath { get; } - - /// - /// True if this represents the root of the repository. - /// - public bool IsRoot => this.RepoRelativePath == ""; - - /// - /// Was the original pathspec parsed as a relative path? - /// - internal bool IsRelative { get; } - - /// - /// Normalizes a pathspec-like string into a root-relative path. - /// - /// - /// See for supported - /// formats of pathspecs. - /// - /// - /// Path that is relative to. - /// Can be empty - which indicates is - /// relative to the root of the repository. - /// - /// - /// Forward slash delimited string representing the root-relative path. - /// - private static (bool isRelative, string normalized) Normalize(string path, string relativeTo) + Requires.NotNullOrEmpty(pathSpec, nameof(pathSpec)); + Requires.NotNull(relativeTo, nameof(relativeTo)); + + if (pathSpec[0] == ':') { - // Path is absolute, nothing to do here - if (path[0] == '/' || path[0] == '\\') + if (pathSpec.Length > 1 && (pathSpec[1] == '^' || pathSpec[1] == '!')) { - return (false, path.Substring(1)); + this.IsExclude = true; + (this.IsRelative, this.RepoRelativePath) = Normalize(pathSpec.Substring(2), relativeTo); } - - var combined = relativeTo == "" ? path : relativeTo + '/' + path; - - return (true, string.Join("/", - combined - .Split(new[] {Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar}, - StringSplitOptions.RemoveEmptyEntries) - // Loop through each path segment... - .Aggregate(new Stack(), (parts, segment) => - { - switch (segment) - { - // If it refers to the current directory, skip it - case ".": - return parts; - - // If it refers to the parent directory, pop the most recent directory - case "..": - if (parts.Count == 0) - throw new FormatException($"Too many '..' in path '{combined}' - would escape the root of the repository."); - - parts.Pop(); - return parts; - - // Otherwise it's a directory/file name - add it to the stack - default: - parts.Push(segment); - return parts; - } - }) - // Reverse the stack, so it iterates root -> leaf - .Reverse() - )); - } - - /// - /// Construct a from a pathspec-like string and a - /// relative path within the repository. - /// - /// - /// A string that supports some pathspec features. - /// This path is relative to . - /// - /// Examples: - /// - ../relative/inclusion.txt - /// - :/absolute/inclusion.txt - /// - :!relative/exclusion.txt - /// - :^relative/exclusion.txt - /// - :^/absolute/exclusion.txt - /// - /// - /// Path (relative to the root of the repository) that is relative to. - /// Can be empty - which indicates is - /// relative to the root of the repository. - /// - /// Invalid path spec. - public FilterPath(string pathSpec, string relativeTo) - { - Requires.NotNullOrEmpty(pathSpec, nameof(pathSpec)); - Requires.NotNull(relativeTo, nameof(relativeTo)); - - if (pathSpec[0] == ':') + else if ((pathSpec.Length > 1 && pathSpec[1] == '/') || pathSpec[1] == '\\') { - if (pathSpec.Length > 1 && (pathSpec[1] == '^' || pathSpec[1] == '!')) - { - this.IsExclude = true; - (this.IsRelative, this.RepoRelativePath) = Normalize(pathSpec.Substring(2), relativeTo); - } - else if (pathSpec.Length > 1 && pathSpec[1] == '/' || pathSpec[1] == '\\') - { - this.RepoRelativePath = pathSpec.Substring(2); - } - else - { - throw new FormatException($"Unrecognized path spec '{pathSpec}'"); - } + this.RepoRelativePath = pathSpec.Substring(2); } else { - (this.IsRelative, this.RepoRelativePath) = Normalize(pathSpec, relativeTo); + throw new FormatException($"Unrecognized path spec '{pathSpec}'"); } + } + else + { + (this.IsRelative, this.RepoRelativePath) = Normalize(pathSpec, relativeTo); + } + + this.RepoRelativePath = + this.RepoRelativePath + .Replace('\\', '/') + .TrimEnd('/'); + } + + /// + /// Gets a value indicating whether represents an exclude filter. + /// + public bool IsExclude { get; } + + /// + /// Gets a value indicating whether represents an include filter. + /// + public bool IsInclude => !this.IsExclude; + + /// + /// Gets the path that this represents, relative to the repository root. + /// Directories are delimited with forward slashes. + /// + public string RepoRelativePath { get; } + + /// + /// Gets a value indicating whether represents the root of the repository. + /// + public bool IsRoot => this.RepoRelativePath == string.Empty; + + /// + /// Gets a value indicating whether the original pathspec was parsed as a relative path. + /// + internal bool IsRelative { get; } - this.RepoRelativePath = - this.RepoRelativePath - .Replace('\\', '/') - .TrimEnd('/'); + /// + /// Determines if should be excluded by this . + /// + /// Forward-slash delimited path (repo relative). + /// + /// Whether paths should be compared case insensitively. + /// Should be the 'core.ignorecase' config value for the repository. + /// + /// + /// True if this is an excluding filter that matches + /// , otherwise false. + /// + public bool Excludes(string repoRelativePath, bool ignoreCase) + { + if (repoRelativePath is null) + { + throw new ArgumentNullException(nameof(repoRelativePath)); } - /// - /// Determines if should be excluded by this . - /// - /// Forward-slash delimited path (repo relative). - /// - /// Whether paths should be compared case insensitively. - /// Should be the 'core.ignorecase' config value for the repository. - /// - /// - /// True if this is an excluding filter that matches - /// , otherwise false. - /// - public bool Excludes(string repoRelativePath, bool ignoreCase) + if (!this.IsExclude) { - if (repoRelativePath is null) - throw new ArgumentNullException(nameof(repoRelativePath)); + return false; + } - if (!this.IsExclude) return false; + StringComparison stringComparison = ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + return this.RepoRelativePath.Equals(repoRelativePath, stringComparison) || + repoRelativePath.StartsWith(this.RepoRelativePath + "/", stringComparison); + } + + /// + /// Determines if should be included by this . + /// + /// Forward-slash delimited path (repo relative). + /// + /// Whether paths should be compared case insensitively. + /// Should be the 'core.ignorecase' config value for the repository. + /// + /// + /// True if this is an including filter that matches + /// , otherwise false. + /// + public bool Includes(string repoRelativePath, bool ignoreCase) + { + if (repoRelativePath is null) + { + throw new ArgumentNullException(nameof(repoRelativePath)); + } - var stringComparison = ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; - return this.RepoRelativePath.Equals(repoRelativePath, stringComparison) || - repoRelativePath.StartsWith(this.RepoRelativePath + "/", - stringComparison); + if (!this.IsInclude) + { + return false; } - /// - /// Determines if should be included by this . - /// - /// Forward-slash delimited path (repo relative). - /// - /// Whether paths should be compared case insensitively. - /// Should be the 'core.ignorecase' config value for the repository. - /// - /// - /// True if this is an including filter that matches - /// , otherwise false. - /// - public bool Includes(string repoRelativePath, bool ignoreCase) + if (this.IsRoot) { - if (repoRelativePath is null) - throw new ArgumentNullException(nameof(repoRelativePath)); + return true; + } - if (!this.IsInclude) return false; - if (this.IsRoot) return true; + StringComparison stringComparison = ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + return this.RepoRelativePath.Equals(repoRelativePath, stringComparison) || + repoRelativePath.StartsWith(this.RepoRelativePath + "/", stringComparison); + } - var stringComparison = ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; - return this.RepoRelativePath.Equals(repoRelativePath, stringComparison) || - repoRelativePath.StartsWith(this.RepoRelativePath + "/", - stringComparison); + /// + /// Determines if children of may be included + /// by this . + /// + /// Forward-slash delimited path (repo relative). + /// + /// Whether paths should be compared case insensitively. + /// Should be the 'core.ignorecase' config value for the repository. + /// + /// + /// if this is an including filter that may match + /// children of , otherwise . + /// + public bool IncludesChildren(string repoRelativePath, bool ignoreCase) + { + if (repoRelativePath is null) + { + throw new ArgumentNullException(nameof(repoRelativePath)); + } + + if (!this.IsInclude) + { + return false; } - /// - /// Determines if children of may be included - /// by this . - /// - /// Forward-slash delimited path (repo relative). - /// - /// Whether paths should be compared case insensitively. - /// Should be the 'core.ignorecase' config value for the repository. - /// - /// - /// if this is an including filter that may match - /// children of , otherwise . - /// - public bool IncludesChildren(string repoRelativePath, bool ignoreCase) + if (this.IsRoot) { - if (repoRelativePath is null) - throw new ArgumentNullException(nameof(repoRelativePath)); + return true; + } + + StringComparison stringComparison = ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + return this.RepoRelativePath.StartsWith(repoRelativePath + "/", stringComparison); + } - if (!this.IsInclude) return false; - if (this.IsRoot) return true; + /// + /// Convert this path filter to a pathspec. + /// + /// + /// Repo-relative directory that relative pathspecs should be relative to. + /// Can be empty - which indicates this FilterPath is + /// relative to the root of the repository. + /// + /// String representation of a path filter (a pathspec). + public string ToPathSpec(string repoRelativeBaseDirectory) + { + Requires.NotNull(repoRelativeBaseDirectory, nameof(repoRelativeBaseDirectory)); - var stringComparison = ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; - return this.RepoRelativePath.StartsWith(repoRelativePath + "/", stringComparison); + var pathSpec = new StringBuilder(this.RepoRelativePath.Length + 2); + (bool _, string normalizedBaseDirectory) = + Normalize(repoRelativeBaseDirectory == string.Empty ? "." : repoRelativeBaseDirectory, null); + + if (this.IsExclude) + { + pathSpec.Append(":!"); } - private static (int dirsToAscend, StringBuilder result) GetRelativePath(string path, string relativeTo) + if (this.IsRelative) { - var pathParts = path.Split('/'); - var baseDirParts = relativeTo.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries); - - int commonParts; - for (commonParts = 0; - commonParts < Math.Min(pathParts.Length, baseDirParts.Length) && - pathParts[commonParts].Equals(baseDirParts[commonParts], StringComparison.OrdinalIgnoreCase); - ++commonParts) + (int dirsAscended, StringBuilder relativePath) = GetRelativePath(this.RepoRelativePath, normalizedBaseDirectory); + if (dirsAscended == 0 && !this.IsExclude) { + pathSpec.Append("./"); } - int dirsToAscend = baseDirParts.Length - commonParts; - - var result = new StringBuilder(path.Length + dirsToAscend * 3); - result.Insert(0, "../", dirsToAscend); - result.Append(string.Join("/", pathParts.Skip(commonParts))); - return (dirsToAscend, result); + pathSpec.Append(relativePath); } - - /// - /// Convert this path filter to a pathspec. - /// - /// - /// Repo-relative directory that relative pathspecs should be relative to. - /// Can be empty - which indicates this FilterPath is - /// relative to the root of the repository. - /// - /// String representation of a path filter (a pathspec) - public string ToPathSpec(string repoRelativeBaseDirectory) + else { - Requires.NotNull(repoRelativeBaseDirectory, nameof(repoRelativeBaseDirectory)); - - var pathSpec = new StringBuilder(this.RepoRelativePath.Length + 2); - var (_, normalizedBaseDirectory) = - Normalize(repoRelativeBaseDirectory == "" ? "." : repoRelativeBaseDirectory, null); + pathSpec.Append('/'); + pathSpec.Append(this.RepoRelativePath); + } - if (this.IsExclude) - pathSpec.Append(":!"); + return pathSpec.ToString(); + } - if (this.IsRelative) - { - var (dirsAscended, relativePath) = GetRelativePath(this.RepoRelativePath, normalizedBaseDirectory); - if (dirsAscended == 0 && !this.IsExclude) - pathSpec.Append("./"); - pathSpec.Append(relativePath); - } - else - { - pathSpec.Append('/'); - pathSpec.Append(this.RepoRelativePath); - } + /// + public override string ToString() + { + return this.RepoRelativePath; + } - return pathSpec.ToString(); + /// + /// Normalizes a pathspec-like string into a root-relative path. + /// + /// + /// See for supported + /// formats of pathspecs. + /// + /// + /// Path that is relative to. + /// Can be empty - which indicates is + /// relative to the root of the repository. + /// + /// + /// Forward slash delimited string representing the root-relative path. + /// + private static (bool IsRelative, string Normalized) Normalize(string path, string relativeTo) + { + // Path is absolute, nothing to do here + if (path[0] == '/' || path[0] == '\\') + { + return (false, path.Substring(1)); } - /// - public override string ToString() + string combined = relativeTo == string.Empty ? path : relativeTo + '/' + path; + + return (true, string.Join( + "/", + combined + .Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries) + + // Loop through each path segment... + .Aggregate(new Stack(), (parts, segment) => + { + switch (segment) + { + // If it refers to the current directory, skip it + case ".": + return parts; + + // If it refers to the parent directory, pop the most recent directory + case "..": + if (parts.Count == 0) + { + throw new FormatException($"Too many '..' in path '{combined}' - would escape the root of the repository."); + } + + parts.Pop(); + return parts; + + // Otherwise it's a directory/file name - add it to the stack + default: + parts.Push(segment); + return parts; + } + }) + + // Reverse the stack, so it iterates root -> leaf + .Reverse())); + } + + private static (int DirsToAscend, StringBuilder Result) GetRelativePath(string path, string relativeTo) + { + string[] pathParts = path.Split('/'); + string[] baseDirParts = relativeTo.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + + int commonParts; + for (commonParts = 0; + commonParts < Math.Min(pathParts.Length, baseDirParts.Length) && + pathParts[commonParts].Equals(baseDirParts[commonParts], StringComparison.OrdinalIgnoreCase); + ++commonParts) { - return this.RepoRelativePath; } + + int dirsToAscend = baseDirParts.Length - commonParts; + + var result = new StringBuilder(path.Length + (dirsToAscend * 3)); + result.Insert(0, "../", dirsToAscend); + result.Append(string.Join("/", pathParts.Skip(commonParts))); + return (dirsToAscend, result); } } diff --git a/src/NerdBank.GitVersioning/FilterPathJsonConverter.cs b/src/NerdBank.GitVersioning/FilterPathJsonConverter.cs index 304c41f0..06ae8e7a 100644 --- a/src/NerdBank.GitVersioning/FilterPathJsonConverter.cs +++ b/src/NerdBank.GitVersioning/FilterPathJsonConverter.cs @@ -1,48 +1,52 @@ -namespace Nerdbank.GitVersioning +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Reflection; +using Newtonsoft.Json; + +namespace Nerdbank.GitVersioning; + +internal class FilterPathJsonConverter : JsonConverter { - using System; - using System.Reflection; - using Newtonsoft.Json; + private readonly string repoRelativeBaseDirectory; - internal class FilterPathJsonConverter : JsonConverter + public FilterPathJsonConverter(string repoRelativeBaseDirectory) { - private readonly string repoRelativeBaseDirectory; + this.repoRelativeBaseDirectory = repoRelativeBaseDirectory; + } - public FilterPathJsonConverter(string repoRelativeBaseDirectory) + /// + public override bool CanConvert(Type objectType) => typeof(FilterPath).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo()); + + /// + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (objectType != typeof(FilterPath) || !(reader.Value is string value)) { - this.repoRelativeBaseDirectory = repoRelativeBaseDirectory; + throw new NotSupportedException(); } - public override bool CanConvert(Type objectType) => typeof(FilterPath).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo()); - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + if (this.repoRelativeBaseDirectory is null) { - if (objectType != typeof(FilterPath) || !(reader.Value is string value)) - { - throw new NotSupportedException(); - } + throw new ArgumentNullException(nameof(this.repoRelativeBaseDirectory), $"Base directory must not be null to be able to deserialize filter paths. Ensure that one was passed to {nameof(VersionOptions.GetJsonSettings)}, and that the version.json file is being written to a Git repository."); + } - if (this.repoRelativeBaseDirectory is null) - { - throw new ArgumentNullException(nameof(this.repoRelativeBaseDirectory), $"Base directory must not be null to be able to deserialize filter paths. Ensure that one was passed to {nameof(VersionOptions.GetJsonSettings)}, and that the version.json file is being written to a Git repository."); - } + return new FilterPath(value, this.repoRelativeBaseDirectory); + } - return new FilterPath(value, this.repoRelativeBaseDirectory); + /// + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (!(value is FilterPath filterPath)) + { + throw new NotSupportedException(); } - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + if (this.repoRelativeBaseDirectory is null) { - if (!(value is FilterPath filterPath)) - { - throw new NotSupportedException(); - } - - if (this.repoRelativeBaseDirectory is null) - { - throw new ArgumentNullException(nameof(this.repoRelativeBaseDirectory), $"Base directory must not be null to be able to serialize filter paths. Ensure that one was passed to {nameof(VersionOptions.GetJsonSettings)}, and that the version.json file is being written to a Git repository."); - } - - writer.WriteValue(filterPath.ToPathSpec(this.repoRelativeBaseDirectory)); + throw new ArgumentNullException(nameof(this.repoRelativeBaseDirectory), $"Base directory must not be null to be able to serialize filter paths. Ensure that one was passed to {nameof(VersionOptions.GetJsonSettings)}, and that the version.json file is being written to a Git repository."); } + + writer.WriteValue(filterPath.ToPathSpec(this.repoRelativeBaseDirectory)); } } diff --git a/src/NerdBank.GitVersioning/GitContext.cs b/src/NerdBank.GitVersioning/GitContext.cs index b11b7e5d..22e792f4 100644 --- a/src/NerdBank.GitVersioning/GitContext.cs +++ b/src/NerdBank.GitVersioning/GitContext.cs @@ -1,313 +1,333 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; using Validation; -namespace Nerdbank.GitVersioning +namespace Nerdbank.GitVersioning; + +/// +/// Represents a location and commit within a git repo and provides access to some version-related git activities. +/// +public abstract class GitContext : IDisposable { /// - /// Represents a location and commit within a git repo and provides access to some version-related git activities. + /// Maximum allowable value for the + /// and components. + /// + private protected const ushort MaximumBuildNumberOrRevisionComponent = 0xfffe; + + /// + /// The 0.0 semver. /// - public abstract class GitContext : IDisposable + private protected static readonly SemanticVersion SemVer0 = SemanticVersion.Parse("0.0"); + + /// + /// The 0.0 version. + /// + private protected static readonly Version Version0 = new Version(0, 0); + + private string repoRelativeProjectDirectory; + + /// Initializes a new instance of the class. + /// The absolute path to the root of the working tree. + /// The path to the .git folder. + protected GitContext(string workingTreePath, string? dotGitPath) + { + this.WorkingTreePath = workingTreePath; + this.repoRelativeProjectDirectory = string.Empty; + this.DotGitPath = dotGitPath; + } + + public enum Engine { /// - /// The 0.0 semver. + /// Uses the faster and platform independent managed git implementation. /// - private protected static readonly SemanticVersion SemVer0 = SemanticVersion.Parse("0.0"); + ReadOnly, /// - /// The 0.0 version. + /// Uses the LibGit2 engine. /// - private protected static readonly Version Version0 = new Version(0, 0); + ReadWrite, /// - /// Maximum allowable value for the - /// and components. + /// Uses a stubbed out engine that doesn't compute versions at all. /// - private protected const ushort MaximumBuildNumberOrRevisionComponent = 0xfffe; + Disabled, + } - private string repoRelativeProjectDirectory; + /// + /// Gets the absolute path to the base directory of the git working tree. + /// + public string WorkingTreePath { get; } - /// Initializes a new instance of the class. - /// The absolute path to the root of the working tree. - /// The path to the .git folder. - protected GitContext(string workingTreePath, string? dotGitPath) + /// + /// Gets or sets the path to the directory to read version information from, relative to the . + /// + public string RepoRelativeProjectDirectory + { + get => this.repoRelativeProjectDirectory; + set { - this.WorkingTreePath = workingTreePath; - this.repoRelativeProjectDirectory = string.Empty; - this.DotGitPath = dotGitPath; + Requires.NotNull(value, nameof(value)); + Requires.Argument(!Path.IsPathRooted(value), nameof(value), "Path must be relative to " + nameof(this.WorkingTreePath) + "."); + this.repoRelativeProjectDirectory = value; } + } - /// - /// Gets the absolute path to the base directory of the git working tree. - /// - public string WorkingTreePath { get; } - - /// - /// Gets the path to the directory to read version information from, relative to the . - /// - public string RepoRelativeProjectDirectory - { - get => this.repoRelativeProjectDirectory; - set - { - Requires.NotNull(value, nameof(value)); - Requires.Argument(!Path.IsPathRooted(value), nameof(value), "Path must be relative to " + nameof(this.WorkingTreePath) + "."); - this.repoRelativeProjectDirectory = value; - } - } + /// + /// Gets the absolute path to the directory to read version information from. + /// + public string AbsoluteProjectDirectory => Path.Combine(this.WorkingTreePath, this.RepoRelativeProjectDirectory); - /// - /// Gets the absolute path to the directory to read version information from. - /// - public string AbsoluteProjectDirectory => Path.Combine(this.WorkingTreePath, this.RepoRelativeProjectDirectory); + /// + /// Gets an instance of that will read version information from the context identified by this instance. + /// + public abstract VersionFile VersionFile { get; } - /// - /// Gets an instance of that will read version information from the context identified by this instance. - /// - public abstract VersionFile VersionFile { get; } + /// + /// Gets a value indicating whether a git repository was found at . + /// + public bool IsRepository => this.DotGitPath is object; - /// - /// Gets a value indicating whether a git repository was found at ; - /// - public bool IsRepository => this.DotGitPath is object; + /// + /// Gets the full SHA-1 id of the commit to be read. + /// + public abstract string? GitCommitId { get; } - /// - /// Gets the full SHA-1 id of the commit to be read. - /// - public abstract string? GitCommitId { get; } + /// + /// Gets a value indicating whether refers to the commit at HEAD. + /// + public abstract bool IsHead { get; } - /// - /// Gets a value indicating whether refers to the commit at HEAD. - /// - public abstract bool IsHead { get; } + /// + /// Gets a value indicating whether the repo is a shallow repo. + /// + public bool IsShallow => this.DotGitPath is object && File.Exists(Path.Combine(this.DotGitPath, "shallow")); - /// - /// Gets a value indicating whether the repo is a shallow repo. - /// - public bool IsShallow => this.DotGitPath is object && File.Exists(Path.Combine(this.DotGitPath, "shallow")); + /// + /// Gets the date that the commit identified by was created. + /// + public abstract DateTimeOffset? GitCommitDate { get; } - /// - /// Gets the date that the commit identified by was created. - /// - public abstract DateTimeOffset? GitCommitDate { get; } + /// + /// Gets the canonical name for HEAD's position (e.g. refs/heads/main). + /// + public abstract string? HeadCanonicalName { get; } - /// - /// Gets the canonical name for HEAD's position (e.g. refs/heads/master) - /// - public abstract string? HeadCanonicalName { get; } + /// + /// Gets the path to the .git folder. + /// + protected string? DotGitPath { get; } - /// - /// Gets the path to the .git folder. - /// - protected string? DotGitPath { get; } + /// + /// Creates a context for reading/writing version information at a given path and committish. + /// + /// The path to a directory for which version information is required. + /// The SHA-1 or ref for a git commit. + /// The git engine to use. + /// The newly created . + public static GitContext Create(string path, string? committish = null, Engine engine = Engine.ReadOnly) + { + Requires.NotNull(path, nameof(path)); - /// - public void Dispose() + if (TryFindGitPaths(path, out string? gitDirectory, out string? workingTreeDirectory, out string? workingTreeRelativePath)) { - this.Dispose(true); - GC.SuppressFinalize(this); + GitContext result = engine switch + { + Engine.Disabled => new DisabledGitContext(workingTreeDirectory), + Engine.ReadWrite => new LibGit2.LibGit2Context(workingTreeDirectory, gitDirectory, committish), + Engine.ReadOnly => new Managed.ManagedGitContext(workingTreeDirectory, gitDirectory, committish), + _ => throw new ArgumentException("Unrecognized value.", nameof(engine)), + }; + result.RepoRelativeProjectDirectory = workingTreeRelativePath; + return result; } - - /// - /// Creates a context for reading/writing version information at a given path and committish. - /// - /// The path to a directory for which version information is required. - /// The SHA-1 or ref for a git commit. - /// if mutating the git repository may be required; otherwise. - /// - public static GitContext Create(string path, string? committish = null, bool writable = false) + else { - Requires.NotNull(path, nameof(path)); - - if (TryFindGitPaths(path, out string? gitDirectory, out string? workingTreeDirectory, out string? workingTreeRelativePath)) + // Consider the working tree to be the entire volume. + string workingTree = path; + string? candidate; + while ((candidate = Path.GetDirectoryName(workingTree)) is object) { - GitContext result = writable - ? (GitContext)new LibGit2.LibGit2Context(workingTreeDirectory, gitDirectory, committish) - : new Managed.ManagedGitContext(workingTreeDirectory, gitDirectory, committish); - result.RepoRelativeProjectDirectory = workingTreeRelativePath; - return result; + workingTree = candidate; } - else - { - // Consider the working tree to be the entire volume. - string workingTree = path; - string? candidate; - while ((candidate = Path.GetDirectoryName(workingTree)) is object) - { - workingTree = candidate; - } - return new NoGitContext(workingTree) - { - RepoRelativeProjectDirectory = path.Substring(workingTree.Length), - }; - } + return new NoGitContext(workingTree) + { + RepoRelativeProjectDirectory = path.Substring(workingTree.Length), + }; } + } - /// - /// Sets the context to represent a particular git commit. - /// - /// Any committish string (e.g. commit id, branch, tag). - /// if the string was successfully parsed into a commit; otherwise. - public abstract bool TrySelectCommit(string committish); + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } - /// - /// Adds a tag with the given name to the commit identified by . - /// - /// The name of the tag. - /// May be thrown if the context was created without specifying write access was required. - /// Thrown if is . - public abstract void ApplyTag(string name); + /// + /// Sets the context to represent a particular git commit. + /// + /// Any committish string (e.g. commit id, branch, tag). + /// if the string was successfully parsed into a commit; otherwise. + public abstract bool TrySelectCommit(string committish); - /// - /// Adds the specified path to the stage for the working tree. - /// - /// The path to be staged. - /// May be thrown if the context was created without specifying write access was required. - public abstract void Stage(string path); + /// + /// Adds a tag with the given name to the commit identified by . + /// + /// The name of the tag. + /// May be thrown if the context was created without specifying write access was required. + /// Thrown if is . + public abstract void ApplyTag(string name); - /// - /// Gets the shortest string that uniquely identifies the . - /// - /// A minimum length. - /// A string that is at least in length but may be more as required to uniquely identify the git object identified by . - public abstract string GetShortUniqueCommitId(int minLength); + /// + /// Adds the specified path to the stage for the working tree. + /// + /// The path to be staged. + /// May be thrown if the context was created without specifying write access was required. + public abstract void Stage(string path); - internal abstract int CalculateVersionHeight(VersionOptions? committedVersion, VersionOptions? workingVersion); + /// + /// Gets the shortest string that uniquely identifies the . + /// + /// A minimum length. + /// A string that is at least in length but may be more as required to uniquely identify the git object identified by . + public abstract string GetShortUniqueCommitId(int minLength); - internal abstract Version GetIdAsVersion(VersionOptions? committedVersion, VersionOptions? workingVersion, int versionHeight); + internal static bool TryFindGitPaths(string? path, [NotNullWhen(true)] out string? gitDirectory, [NotNullWhen(true)] out string? workingTreeDirectory, [NotNullWhen(true)] out string? workingTreeRelativePath) + { + if (path is null || path.Length == 0) + { + gitDirectory = null; + workingTreeDirectory = null; + workingTreeRelativePath = null; + return false; + } - internal string GetRepoRelativePath(string absolutePath) + path = Path.GetFullPath(path); + (string GitDirectory, string WorkingTreeDirectory)? gitDirs = FindGitDir(path); + if (gitDirs is null) { - var repoRoot = this.WorkingTreePath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + gitDirectory = null; + workingTreeDirectory = null; + workingTreeRelativePath = null; + return false; + } - if (!absolutePath.StartsWith(repoRoot, StringComparison.OrdinalIgnoreCase)) - { - throw new ArgumentException($"Path '{absolutePath}' is not within repository '{repoRoot}'", nameof(absolutePath)); - } + gitDirectory = gitDirs.Value.GitDirectory; + workingTreeDirectory = gitDirs.Value.WorkingTreeDirectory; + workingTreeRelativePath = path.Substring(gitDirs.Value.WorkingTreeDirectory.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + return true; + } - return absolutePath.Substring(repoRoot.Length) - .TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - } + internal abstract int CalculateVersionHeight(VersionOptions? committedVersion, VersionOptions? workingVersion); - /// - /// Gets a value indicating whether the version file has changed in the working tree. - /// - /// - /// The commited . - /// - /// - /// The working version of . - /// - /// if the version file is dirty; otherwise. - protected static bool IsVersionFileChangedInWorkingTree(VersionOptions? committedVersion, VersionOptions? workingVersion) - { - if (workingVersion is object) - { - return !EqualityComparer.Default.Equals(workingVersion, committedVersion); - } + internal abstract Version GetIdAsVersion(VersionOptions? committedVersion, VersionOptions? workingVersion, int versionHeight); - // A missing working version is a change only if it was previously committed. - return committedVersion is object; - } + internal string GetRepoRelativePath(string absolutePath) + { + string? repoRoot = this.WorkingTreePath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - internal static bool TryFindGitPaths(string? path, [NotNullWhen(true)] out string? gitDirectory, [NotNullWhen(true)] out string? workingTreeDirectory, [NotNullWhen(true)] out string? workingTreeRelativePath) + if (!absolutePath.StartsWith(repoRoot, StringComparison.OrdinalIgnoreCase)) { - if (path is null || path.Length == 0) - { - gitDirectory = null; - workingTreeDirectory = null; - workingTreeRelativePath = null; - return false; - } + throw new ArgumentException($"Path '{absolutePath}' is not within repository '{repoRoot}'", nameof(absolutePath)); + } - path = Path.GetFullPath(path); - var gitDirs = FindGitDir(path); - if (gitDirs is null) - { - gitDirectory = null; - workingTreeDirectory = null; - workingTreeRelativePath = null; - return false; - } + return absolutePath.Substring(repoRoot.Length) + .TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + } - gitDirectory = gitDirs.Value.GitDirectory; - workingTreeDirectory = gitDirs.Value.WorkingTreeDirectory; - workingTreeRelativePath = path.Substring(gitDirs.Value.WorkingTreeDirectory.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - return true; + /// + /// Gets a value indicating whether the version file has changed in the working tree. + /// + /// + /// The commited . + /// + /// + /// The working version of . + /// + /// if the version file is dirty; otherwise. + protected static bool IsVersionFileChangedInWorkingTree(VersionOptions? committedVersion, VersionOptions? workingVersion) + { + if (workingVersion is object) + { + return !EqualityComparer.Default.Equals(workingVersion, committedVersion); } - private protected static void FindGitPaths(string path, out string gitDirectory, out string workingTreeDirectory, out string workingTreeRelativePath) + // A missing working version is a change only if it was previously committed. + return committedVersion is object; + } + + /// + /// Disposes of native and managed resources associated by this object. + /// + /// to dispose managed and native resources; to only dispose of native resources. + protected virtual void Dispose(bool disposing) + { + } + + private protected static void FindGitPaths(string path, out string gitDirectory, out string workingTreeDirectory, out string workingTreeRelativePath) + { + if (TryFindGitPaths(path, out string? gitDirectoryLocal, out string? workingTreeDirectoryLocal, out string? workingTreeRelativePathLocal)) { - if (TryFindGitPaths(path, out string? gitDirectoryLocal, out string? workingTreeDirectoryLocal, out string? workingTreeRelativePathLocal)) - { - gitDirectory = gitDirectoryLocal; - workingTreeDirectory = workingTreeDirectoryLocal; - workingTreeRelativePath = workingTreeRelativePathLocal; - } - else - { - throw new ArgumentException("Path is not within a git directory.", nameof(path)); - } + gitDirectory = gitDirectoryLocal; + workingTreeDirectory = workingTreeDirectoryLocal; + workingTreeRelativePath = workingTreeRelativePathLocal; } - - /// - /// Disposes of native and managed resources associated by this object. - /// - /// to dispose managed and native resources; to only dispose of native resources. - protected virtual void Dispose(bool disposing) + else { + throw new ArgumentException("Path is not within a git directory.", nameof(path)); } + } - /// - /// Searches a path and its ancestors for a directory with a .git subdirectory. - /// - /// The absolute path to start the search from. - /// The path to the .git folder and working tree, or if not found. - private static (string GitDirectory, string WorkingTreeDirectory)? FindGitDir(string path) + /// + /// Searches a path and its ancestors for a directory with a .git subdirectory. + /// + /// The absolute path to start the search from. + /// The path to the .git folder and working tree, or if not found. + private static (string GitDirectory, string WorkingTreeDirectory)? FindGitDir(string path) + { + string? startingDir = path; + while (startingDir is object) { - string? startingDir = path; - while (startingDir is object) + string? dirOrFilePath = Path.Combine(startingDir, ".git"); + if (Directory.Exists(dirOrFilePath)) { - var dirOrFilePath = Path.Combine(startingDir, ".git"); - if (Directory.Exists(dirOrFilePath)) - { - return (dirOrFilePath, Path.GetDirectoryName(dirOrFilePath)!); - } - else if (File.Exists(dirOrFilePath)) + return (dirOrFilePath, Path.GetDirectoryName(dirOrFilePath)!); + } + else if (File.Exists(dirOrFilePath)) + { + string? relativeGitDirPath = ReadGitDirFromFile(dirOrFilePath); + if (!string.IsNullOrWhiteSpace(relativeGitDirPath)) { - string? relativeGitDirPath = ReadGitDirFromFile(dirOrFilePath); - if (!string.IsNullOrWhiteSpace(relativeGitDirPath)) + string? fullGitDirPath = Path.GetFullPath(Path.Combine(startingDir, relativeGitDirPath)); + if (Directory.Exists(fullGitDirPath)) { - var fullGitDirPath = Path.GetFullPath(Path.Combine(startingDir, relativeGitDirPath)); - if (Directory.Exists(fullGitDirPath)) - { - return (fullGitDirPath, Path.GetDirectoryName(dirOrFilePath)!); - } + return (fullGitDirPath, Path.GetDirectoryName(dirOrFilePath)!); } } - - startingDir = Path.GetDirectoryName(startingDir); } - return null; + startingDir = Path.GetDirectoryName(startingDir); } - private static string? ReadGitDirFromFile(string fileName) - { - const string expectedPrefix = "gitdir: "; - var firstLineOfFile = File.ReadLines(fileName).FirstOrDefault(); - if (firstLineOfFile?.StartsWith(expectedPrefix) ?? false) - { - return firstLineOfFile.Substring(expectedPrefix.Length); // strip off the prefix, leaving just the path - } + return null; + } - return null; + private static string? ReadGitDirFromFile(string fileName) + { + const string expectedPrefix = "gitdir: "; + string? firstLineOfFile = File.ReadLines(fileName).FirstOrDefault(); + if (firstLineOfFile?.StartsWith(expectedPrefix) ?? false) + { + return firstLineOfFile.Substring(expectedPrefix.Length); // strip off the prefix, leaving just the path } + + return null; } } diff --git a/src/NerdBank.GitVersioning/GitException.cs b/src/NerdBank.GitVersioning/GitException.cs index 72802b6e..2a37dc93 100644 --- a/src/NerdBank.GitVersioning/GitException.cs +++ b/src/NerdBank.GitVersioning/GitException.cs @@ -1,89 +1,91 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable -using System; using System.Runtime.Serialization; -namespace Nerdbank.GitVersioning +namespace Nerdbank.GitVersioning; + +/// +/// The exception which is thrown by the managed Git layer. +/// +[Serializable] +public class GitException : Exception { /// - /// The exception which is thrown by the managed Git layer. + /// Initializes a new instance of the class. /// - [Serializable] - public class GitException : Exception + public GitException() { - /// - /// Initializes a new instance of the class. - /// - public GitException() - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// - public GitException(string message) - : base(message) - { - } + /// + /// Initializes a new instance of the class. + /// + /// + public GitException(string message) + : base(message) + { + } - /// - /// Initializes a new instance of the with an - /// error message and an inner message. - /// - /// - /// A message which describes the error. - /// - /// - /// The which caused this exception to be thrown. - /// - public GitException(string message, Exception innerException) - : base(message, innerException) - { - } + /// + /// Initializes a new instance of the class + /// with an error message and an inner message. + /// + /// + /// A message which describes the error. + /// + /// + /// The which caused this exception to be thrown. + /// + public GitException(string message, Exception innerException) + : base(message, innerException) + { + } - /// - /// Initializes a new instance of the class. - /// - protected GitException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - this.ErrorCode = (ErrorCodes)info.GetUInt32(nameof(this.ErrorCode)); - this.iSShallowClone = info.GetBoolean(nameof(this.iSShallowClone)); - } + /// + /// Initializes a new instance of the class. + /// + /// + protected GitException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + this.ErrorCode = (ErrorCodes)info.GetUInt32(nameof(this.ErrorCode)); + this.IsShallowClone = info.GetBoolean(nameof(this.IsShallowClone)); + } + /// + /// Describes specific error conditions that may warrant branching code paths. + /// + public enum ErrorCodes + { /// - /// Gets the error code for this exception. + /// No error code was specified. /// - public ErrorCodes ErrorCode { get; set; } + Unspecified = 0, /// - /// Gets a value indicating whether the exception was thrown from a shallow clone. + /// An object could not be found. /// - public bool iSShallowClone { get; set; } + ObjectNotFound, + } - /// - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); - info.AddValue(nameof(this.ErrorCode), (int)this.ErrorCode); - info.AddValue(nameof(this.iSShallowClone), this.iSShallowClone); - } + /// + /// Gets or sets the error code for this exception. + /// + public ErrorCodes ErrorCode { get; set; } - /// - /// Describes specific error conditions that may warrant branching code paths. - /// - public enum ErrorCodes - { - /// - /// No error code was specified. - /// - Unspecified = 0, + /// + /// Gets or sets a value indicating whether the exception was thrown from a shallow clone. + /// + public bool IsShallowClone { get; set; } - /// - /// An object could not be found. - /// - ObjectNotFound, - } + /// + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + info.AddValue(nameof(this.ErrorCode), (int)this.ErrorCode); + info.AddValue(nameof(this.IsShallowClone), this.IsShallowClone); } } diff --git a/src/NerdBank.GitVersioning/ICloudBuild.cs b/src/NerdBank.GitVersioning/ICloudBuild.cs index df8a69fa..15b76e42 100644 --- a/src/NerdBank.GitVersioning/ICloudBuild.cs +++ b/src/NerdBank.GitVersioning/ICloudBuild.cs @@ -1,57 +1,56 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Nerdbank.GitVersioning +#nullable enable + +namespace Nerdbank.GitVersioning; + +/// +/// Defines cloud build provider functionality. +/// +public interface ICloudBuild { - using System.Collections.Generic; - using System.IO; + /// + /// Gets a value indicating whether the active cloud build matches what this instance supports. + /// + bool IsApplicable { get; } + + /// + /// Gets a value indicating whether a cloud build is validating a pull request. + /// + bool IsPullRequest { get; } + + /// + /// Gets the branch being built by a cloud build, if applicable. + /// + string? BuildingBranch { get; } + + /// + /// Gets the tag being built by a cloud build, if applicable. + /// + string? BuildingTag { get; } + + /// + /// Gets the git commit ID being built by a cloud build, if applicable. + /// + string? GitCommitId { get; } + + /// + /// Sets the build number for the cloud build, if supported. + /// + /// The build number to set. + /// An optional redirection for what should be written to the standard out stream. + /// An optional redirection for what should be written to the standard error stream. + /// A dictionary of environment/build variables that the caller should set to update the environment to match the new settings. + IReadOnlyDictionary SetCloudBuildNumber(string buildNumber, TextWriter? stdout, TextWriter? stderr); /// - /// Defines cloud build provider functionality. + /// Sets a cloud build variable, if supported. /// - public interface ICloudBuild - { - /// - /// Gets a value indicating whether the active cloud build matches what this instance supports. - /// - bool IsApplicable { get; } - - /// - /// Gets a value indicating whether a cloud build is validating a pull request. - /// - bool IsPullRequest { get; } - - /// - /// Gets the branch being built by a cloud build, if applicable. - /// - string? BuildingBranch { get; } - - /// - /// Gets the tag being built by a cloud build, if applicable. - /// - string? BuildingTag { get; } - - /// - /// Gets the git commit ID being built by a cloud build, if applicable. - /// - string? GitCommitId { get; } - - /// - /// Sets the build number for the cloud build, if supported. - /// - /// The build number to set. - /// An optional redirection for what should be written to the standard out stream. - /// An optional redirection for what should be written to the standard error stream. - /// A dictionary of environment/build variables that the caller should set to update the environment to match the new settings. - IReadOnlyDictionary SetCloudBuildNumber(string buildNumber, TextWriter? stdout, TextWriter? stderr); - - /// - /// Sets a cloud build variable, if supported. - /// - /// The name of the variable. - /// The value for the variable. - /// An optional redirection for what should be written to the standard out stream. - /// An optional redirection for what should be written to the standard error stream. - /// A dictionary of environment/build variables that the caller should set to update the environment to match the new settings. - IReadOnlyDictionary SetCloudBuildVariable(string name, string value, TextWriter? stdout, TextWriter? stderr); - } + /// The name of the variable. + /// The value for the variable. + /// An optional redirection for what should be written to the standard out stream. + /// An optional redirection for what should be written to the standard error stream. + /// A dictionary of environment/build variables that the caller should set to update the environment to match the new settings. + IReadOnlyDictionary SetCloudBuildVariable(string name, string value, TextWriter? stdout, TextWriter? stderr); } diff --git a/src/NerdBank.GitVersioning/LibGit2/LibGit2Context.cs b/src/NerdBank.GitVersioning/LibGit2/LibGit2Context.cs index 65cd8bba..1aa804c1 100644 --- a/src/NerdBank.GitVersioning/LibGit2/LibGit2Context.cs +++ b/src/NerdBank.GitVersioning/LibGit2/LibGit2Context.cs @@ -1,177 +1,179 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable -using System; using System.Diagnostics; using LibGit2Sharp; -using Validation; -namespace Nerdbank.GitVersioning.LibGit2 +namespace Nerdbank.GitVersioning.LibGit2; + +/// +/// A git context implemented in terms of LibGit2Sharp. +/// +[DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] +public class LibGit2Context : GitContext { - /// - /// A git context implemented in terms of LibGit2Sharp. - /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] - public class LibGit2Context : GitContext + internal LibGit2Context(string workingTreeDirectory, string dotGitPath, string? committish = null) + : base(workingTreeDirectory, dotGitPath) { - internal LibGit2Context(string workingTreeDirectory, string dotGitPath, string? committish = null) - : base(workingTreeDirectory, dotGitPath) + this.Repository = OpenGitRepo(workingTreeDirectory, useDefaultConfigSearchPaths: true); + if (this.Repository.Info.WorkingDirectory is null) { - this.Repository = OpenGitRepo(workingTreeDirectory, useDefaultConfigSearchPaths: true); - if (this.Repository.Info.WorkingDirectory is null) - { - throw new ArgumentException("Bare repositories not supported.", nameof(workingTreeDirectory)); - } - - this.Commit = committish is null ? this.Repository.Head.Tip : this.Repository.Lookup(committish); - if (this.Commit is null && committish is object) - { - throw new ArgumentException("No matching commit found.", nameof(committish)); - } + throw new ArgumentException("Bare repositories not supported.", nameof(workingTreeDirectory)); + } - this.VersionFile = new LibGit2VersionFile(this); + this.Commit = committish is null ? this.Repository.Head.Tip : this.Repository.Lookup(committish); + if (this.Commit is null && committish is object) + { + throw new ArgumentException("No matching commit found.", nameof(committish)); } - /// - public override VersionFile VersionFile { get; } + this.VersionFile = new LibGit2VersionFile(this); + } - /// - public Repository Repository { get; } + /// + public override VersionFile VersionFile { get; } - /// - public Commit? Commit { get; private set; } + public Repository Repository { get; } - /// - public override string? GitCommitId => this.Commit?.Sha; + public Commit? Commit { get; private set; } - /// - public override bool IsHead => this.Repository.Head?.Tip?.Equals(this.Commit) ?? false; + /// + public override string? GitCommitId => this.Commit?.Sha; - /// - public override DateTimeOffset? GitCommitDate => this.Commit?.Author.When; + /// + public override bool IsHead => this.Repository.Head?.Tip?.Equals(this.Commit) ?? false; - /// - public override string HeadCanonicalName => this.Repository.Head.CanonicalName; + /// + public override DateTimeOffset? GitCommitDate => this.Commit?.Author.When; - private string DebuggerDisplay => $"\"{this.WorkingTreePath}\" (libgit2)"; + /// + public override string HeadCanonicalName => this.Repository.Head.CanonicalName; - /// - public override void ApplyTag(string name) => this.Repository.Tags.Add(name, this.Commit); + private string DebuggerDisplay => $"\"{this.WorkingTreePath}\" (libgit2)"; - /// - public override bool TrySelectCommit(string committish) + /// Initializes a new instance of the class. + /// The path to the .git directory or somewhere in a git working tree. + /// The SHA-1 or ref for a git commit. + /// The new instance. + public static LibGit2Context Create(string path, string? committish = null) + { + FindGitPaths(path, out string? gitDirectory, out string? workingTreeDirectory, out string? workingTreeRelativePath); + return new LibGit2Context(workingTreeDirectory, gitDirectory, committish) { - try - { - this.Repository.RevParse(committish, out Reference? reference, out GitObject obj); - if (obj is Commit commit) - { - this.Commit = commit; - return true; - } - } - catch (NotFoundException) - { - } - - return false; - } - - /// - public override void Stage(string path) => global::LibGit2Sharp.Commands.Stage(this.Repository, path); + RepoRelativeProjectDirectory = workingTreeRelativePath, + }; + } - /// - public override string GetShortUniqueCommitId(int minLength) => this.Repository.ObjectDatabase.ShortenObjectId(this.Commit, minLength); + /// + public override void ApplyTag(string name) => this.Repository.Tags.Add(name, this.Commit); - internal override int CalculateVersionHeight(VersionOptions? committedVersion, VersionOptions? workingVersion) + /// + public override bool TrySelectCommit(string committish) + { + try { - var headCommitVersion = committedVersion?.Version ?? SemVer0; - - if (IsVersionFileChangedInWorkingTree(committedVersion, workingVersion)) + this.Repository.RevParse(committish, out Reference? reference, out GitObject obj); + if (obj is Commit commit) { - var workingCopyVersion = workingVersion?.Version?.Version; - - if (workingCopyVersion is null || !workingCopyVersion.Equals(headCommitVersion)) - { - // The working copy has changed the major.minor version. - // So by definition the version height is 0, since no commit represents it yet. - return 0; - } + this.Commit = commit; + return true; } - - return LibGit2GitExtensions.GetVersionHeight(this); } - - internal override System.Version GetIdAsVersion(VersionOptions? committedVersion, VersionOptions? workingVersion, int versionHeight) + catch (NotFoundException) { - VersionOptions? version = IsVersionFileChangedInWorkingTree(committedVersion, workingVersion) ? workingVersion : committedVersion; - - return this.Commit.GetIdAsVersionHelper(version, versionHeight); } - /// The path to the .git directory or somewhere in a git working tree. - /// The SHA-1 or ref for a git commit. - public static LibGit2Context Create(string path, string? committish = null) + return false; + } + + /// + public override void Stage(string path) => global::LibGit2Sharp.Commands.Stage(this.Repository, path); + + /// + public override string GetShortUniqueCommitId(int minLength) => this.Repository.ObjectDatabase.ShortenObjectId(this.Commit, minLength); + + /// + /// Opens a found at or above a specified path. + /// + /// The path to the .git directory or the working directory. + /// + /// Specifies whether to use default settings for looking up global and system settings. + /// + /// By default ( == ), the repository will be configured to only + /// use the repository-level configuration ignoring system or user-level configuration (set using git config --global. + /// Thus only settings explicitly set for the repo will be available. + /// + /// + /// For example using Repository.Configuration.Get{string}("user.name") to get the user's name will + /// return the value set in the repository config or if the user name has not been explicitly set for the repository. + /// + /// + /// When the caller specifies to use the default configuration search paths ( == ) + /// both repository level and global configuration will be available to the repo as well. + /// + /// + /// In this mode, using Repository.Configuration.Get{string}("user.name") will return the + /// value set in the user's global git configuration unless set on the repository level, + /// matching the behavior of the git command. + /// + /// + /// The found for the specified path, or if no git repo is found. + internal static Repository OpenGitRepo(string path, bool useDefaultConfigSearchPaths = false) + { + if (useDefaultConfigSearchPaths) { - FindGitPaths(path, out string? gitDirectory, out string? workingTreeDirectory, out string? workingTreeRelativePath); - return new LibGit2Context(workingTreeDirectory, gitDirectory, committish) - { - RepoRelativeProjectDirectory = workingTreeRelativePath, - }; + // pass null to reset to defaults + GlobalSettings.SetConfigSearchPaths(ConfigurationLevel.Global, null); + GlobalSettings.SetConfigSearchPaths(ConfigurationLevel.System, null); } - - /// - protected override void Dispose(bool disposing) + else { - if (disposing) - { - this.Repository.Dispose(); - } - - base.Dispose(disposing); + // Override Config Search paths to empty path to avoid new Repository instance to lookup for Global\System .gitconfig file + GlobalSettings.SetConfigSearchPaths(ConfigurationLevel.Global, string.Empty); + GlobalSettings.SetConfigSearchPaths(ConfigurationLevel.System, string.Empty); } - /// - /// Opens a found at or above a specified path. - /// - /// The path to the .git directory or the working directory. - /// - /// Specifies whether to use default settings for looking up global and system settings. - /// - /// By default ( == false), the repository will be configured to only - /// use the repository-level configuration ignoring system or user-level configuration (set using git config --global. - /// Thus only settings explicitly set for the repo will be available. - /// - /// - /// For example using Repository.Configuration.Get{string}("user.name") to get the user's name will - /// return the value set in the repository config or null if the user name has not been explicitly set for the repository. - /// - /// - /// When the caller specifies to use the default configuration search paths ( == true) - /// both repository level and global configuration will be available to the repo as well. - /// - /// - /// In this mode, using Repository.Configuration.Get{string}("user.name") will return the - /// value set in the user's global git configuration unless set on the repository level, - /// matching the behavior of the git command. - /// - /// - /// The found for the specified path, or null if no git repo is found. - internal static Repository OpenGitRepo(string path, bool useDefaultConfigSearchPaths = false) + return new Repository(path); + } + + /// + internal override int CalculateVersionHeight(VersionOptions? committedVersion, VersionOptions? workingVersion) + { + SemanticVersion? headCommitVersion = committedVersion?.Version ?? SemVer0; + + if (IsVersionFileChangedInWorkingTree(committedVersion, workingVersion)) { - if (useDefaultConfigSearchPaths) - { - // pass null to reset to defaults - GlobalSettings.SetConfigSearchPaths(ConfigurationLevel.Global, null); - GlobalSettings.SetConfigSearchPaths(ConfigurationLevel.System, null); - } - else + System.Version? workingCopyVersion = workingVersion?.Version?.Version; + + if (workingCopyVersion is null || !workingCopyVersion.Equals(headCommitVersion)) { - // Override Config Search paths to empty path to avoid new Repository instance to lookup for Global\System .gitconfig file - GlobalSettings.SetConfigSearchPaths(ConfigurationLevel.Global, string.Empty); - GlobalSettings.SetConfigSearchPaths(ConfigurationLevel.System, string.Empty); + // The working copy has changed the major.minor version. + // So by definition the version height is 0, since no commit represents it yet. + return 0; } + } - return new Repository(path); + return LibGit2GitExtensions.GetVersionHeight(this); + } + + /// + internal override System.Version GetIdAsVersion(VersionOptions? committedVersion, VersionOptions? workingVersion, int versionHeight) + { + VersionOptions? version = IsVersionFileChangedInWorkingTree(committedVersion, workingVersion) ? workingVersion : committedVersion; + + return this.Commit.GetIdAsVersionHelper(version, versionHeight); + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.Repository.Dispose(); } + + base.Dispose(disposing); } } diff --git a/src/NerdBank.GitVersioning/LibGit2/LibGit2GitExtensions.cs b/src/NerdBank.GitVersioning/LibGit2/LibGit2GitExtensions.cs index 76eff1f3..e4221cd9 100644 --- a/src/NerdBank.GitVersioning/LibGit2/LibGit2GitExtensions.cs +++ b/src/NerdBank.GitVersioning/LibGit2/LibGit2GitExtensions.cs @@ -1,580 +1,610 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Nerdbank.GitVersioning.LibGit2 +using System.Buffers.Binary; +using System.Runtime.InteropServices; +using System.Text; +using LibGit2Sharp; +using Validation; +using Windows.Win32; +using Version = System.Version; + +#nullable enable + +namespace Nerdbank.GitVersioning.LibGit2; + +/// +/// Git extension methods. +/// +public static class LibGit2GitExtensions { - using System; - using System.Buffers.Binary; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Runtime.InteropServices; - using System.Text; - using LibGit2Sharp; - using Validation; - using Version = System.Version; + /// + /// Maximum allowable value for the + /// and components. + /// + private const ushort MaximumBuildNumberOrRevisionComponent = 0xfffe; /// - /// Git extension methods. + /// The 0.0 version. /// - public static class LibGit2GitExtensions + private static readonly Version Version0 = new Version(0, 0); + + /// + /// The 0.0 semver. + /// + private static readonly SemanticVersion SemVer0 = SemanticVersion.Parse("0.0"); + + private static readonly LibGit2Sharp.CompareOptions DiffOptions = new LibGit2Sharp.CompareOptions() { - /// - /// The 0.0 version. - /// - private static readonly Version Version0 = new Version(0, 0); + // When calculating the height of a commit, we do not care if a file has been renamed only if it has been added or removed. + // Calculating similarities can consume significant amounts of CPU, so disable it. + Similarity = SimilarityOptions.None, + ContextLines = 0, + }; - /// - /// The 0.0 semver. - /// - private static readonly SemanticVersion SemVer0 = SemanticVersion.Parse("0.0"); +#if !NETCOREAPP + private static FreeLibrarySafeHandle? nativeLibrary; +#endif - private static readonly LibGit2Sharp.CompareOptions DiffOptions = new LibGit2Sharp.CompareOptions() - { - // When calculating the height of a commit, we do not care if a file has been renamed only if it has been added or removed. - // Calculating similarities can consume significant amounts of CPU, so disable it. - Similarity = SimilarityOptions.None, - ContextLines = 0 - }; - - /// - /// Maximum allowable value for the - /// and components. - /// - private const ushort MaximumBuildNumberOrRevisionComponent = 0xfffe; - - /// - /// Gets the number of commits in the longest single path between - /// the specified commit and the most distant ancestor (inclusive) - /// that set the version to the value at . - /// - /// The git context to read from. - /// Optional base version to calculate the height. If not specified, the base version will be calculated by scanning the repository. - /// The height of the commit. Always a positive integer. - internal static int GetVersionHeight(LibGit2Context context, Version? baseVersion = null) - { - if (context.Commit is null) - { - return 0; - } + /// + /// Gets the number of commits in the longest single path between + /// the specified commit and the most distant ancestor (inclusive). + /// + /// The git context to read from. + /// + /// A function that returns when we reach a commit that + /// should not be included in the height calculation. + /// May be null to count the height to the original commit. + /// + /// The height of the commit. Always a positive integer. + public static int GetHeight(LibGit2Context context, Func? continueStepping = null) + { + Verify.Operation(context.Commit is object, "No commit is selected."); + var tracker = new GitWalkTracker(context); + return GetCommitHeight(context.Commit, tracker, continueStepping); + } - var tracker = new GitWalkTracker(context); + /// + /// Takes the first 2 bytes of a commit ID (i.e. first 4 characters of its hex-encoded SHA) + /// and returns them as an 16-bit unsigned integer. + /// + /// The commit to identify with an integer. + /// The unsigned integer which identifies a commit. + public static ushort GetTruncatedCommitIdAsUInt16(this Commit commit) + { + Requires.NotNull(commit, nameof(commit)); + return BinaryPrimitives.ReadUInt16BigEndian(commit.Id.RawId); + } - var versionOptions = tracker.GetVersion(context.Commit); - if (versionOptions is null) - { - return 0; - } + /// + /// Looks up the commit that matches a specified version number. + /// + /// The git context to read from. + /// The version previously obtained from . + /// The matching commit, or if no match is found. + /// + /// Thrown in the very rare situation that more than one matching commit is found. + /// + public static Commit? GetCommitFromVersion(LibGit2Context context, Version version) + { + // Note we'll accept no match, or one match. But we throw if there is more than one match. + return GetCommitsFromVersion(context, version).SingleOrDefault(); + } - var baseSemVer = - baseVersion is not null ? SemanticVersion.Parse(baseVersion.ToString()) : - versionOptions.Version ?? SemVer0; + /// + /// Looks up the commits that match a specified version number. + /// + /// The git context to read from. + /// The version previously obtained from . + /// The matching commits, or an empty enumeration if no match is found. + public static IEnumerable GetCommitsFromVersion(LibGit2Context context, Version version) + { + Requires.NotNull(context, nameof(context)); + Requires.NotNull(version, nameof(version)); + + var tracker = new GitWalkTracker(context); + IEnumerable? possibleCommits = from commit in GetCommitsReachableFromRefs(context.Repository) + let commitVersionOptions = tracker.GetVersion(commit) + where commitVersionOptions?.Version?.IsMatchingVersion(version) is true + where !IsCommitIdMismatch(version, commitVersionOptions, commit) + where !IsVersionHeightMismatch(version, commitVersionOptions, commit, tracker) + select commit; + + return possibleCommits; + } - var versionHeightPosition = versionOptions.VersionHeightPosition; - if (versionHeightPosition.HasValue) - { - int height = GetHeight(context, c => CommitMatchesVersion(c, baseSemVer, versionHeightPosition.Value, tracker)); - return height; - } + /// + /// Finds the directory that contains the appropriate native libgit2 module. + /// + /// The path to the directory that contains the runtimes folder. + /// Receives the directory that native binaries are expected. + public static string? FindLibGit2NativeBinaries(string basePath) + { + string arch = RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant(); - return 0; - } + // TODO: learn how to detect when to use "linux-musl". + string? os = + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "win" : + RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "linux" : + RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "osx" : + null; - /// - /// Gets the number of commits in the longest single path between - /// the specified commit and the most distant ancestor (inclusive). - /// - /// The git context to read from. - /// - /// A function that returns false when we reach a commit that - /// should not be included in the height calculation. - /// May be null to count the height to the original commit. - /// - /// The height of the commit. Always a positive integer. - public static int GetHeight(LibGit2Context context, Func? continueStepping = null) + if (os is null) { - Verify.Operation(context.Commit is object, "No commit is selected."); - var tracker = new GitWalkTracker(context); - return GetCommitHeight(context.Commit, tracker, continueStepping); + return null; } - /// - /// Takes the first 2 bytes of a commit ID (i.e. first 4 characters of its hex-encoded SHA) - /// and returns them as an 16-bit unsigned integer. - /// - /// The commit to identify with an integer. - /// The unsigned integer which identifies a commit. - public static ushort GetTruncatedCommitIdAsUInt16(this Commit commit) + string candidatePath = Path.Combine(basePath, "runtimes", $"{os}-{arch}", "native"); + return Directory.Exists(candidatePath) ? candidatePath : null; + } + + /// + /// Loads the git2 native library from the expected path so that it's available later when needed. + /// + /// + /// + /// This method should only be called on .NET Framework, and only loads the module when on Windows. + /// + public static void LoadNativeBinary(string basePath) + { +#if NETCOREAPP + throw new PlatformNotSupportedException(); +#else + if (nativeLibrary is null && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - Requires.NotNull(commit, nameof(commit)); - return BinaryPrimitives.ReadUInt16BigEndian(commit.Id.RawId); + if (FindLibGit2NativeBinaries(basePath) is string directoryPath) + { + if (Directory.EnumerateFiles(directoryPath).FirstOrDefault() is string filePath) + { + nativeLibrary = PInvoke.LoadLibrary(filePath); + } + } } +#endif + } - /// - /// Returns the repository that belongs to. - /// - /// Member of the repository. - /// Repository that belongs to. - private static IRepository GetRepository(this IBelongToARepository repositoryMember) + /// + /// Gets the number of commits in the longest single path between + /// the specified commit and the most distant ancestor (inclusive) + /// that set the version to the value at . + /// + /// The git context to read from. + /// Optional base version to calculate the height. If not specified, the base version will be calculated by scanning the repository. + /// The height of the commit. Always a positive integer. + internal static int GetVersionHeight(LibGit2Context context, Version? baseVersion = null) + { + if (context.Commit is null) { - return repositoryMember.Repository; + return 0; } - /// - /// Looks up the commit that matches a specified version number. - /// - /// The git context to read from. - /// The version previously obtained from . - /// The matching commit, or null if no match is found. - /// - /// Thrown in the very rare situation that more than one matching commit is found. - /// - public static Commit GetCommitFromVersion(LibGit2Context context, Version version) + var tracker = new GitWalkTracker(context); + + VersionOptions? versionOptions = tracker.GetVersion(context.Commit); + if (versionOptions is null) { - // Note we'll accept no match, or one match. But we throw if there is more than one match. - return GetCommitsFromVersion(context, version).SingleOrDefault(); + return 0; } - /// - /// Looks up the commits that match a specified version number. - /// - /// The git context to read from. - /// The version previously obtained from . - /// The matching commits, or an empty enumeration if no match is found. - public static IEnumerable GetCommitsFromVersion(LibGit2Context context, Version version) + SemanticVersion? baseSemVer = + baseVersion is not null ? SemanticVersion.Parse(baseVersion.ToString()) : + versionOptions.Version ?? SemVer0; + + SemanticVersion.Position? versionHeightPosition = versionOptions.VersionHeightPosition; + if (versionHeightPosition.HasValue) { - Requires.NotNull(context, nameof(context)); - Requires.NotNull(version, nameof(version)); - - var tracker = new GitWalkTracker(context); - var possibleCommits = from commit in GetCommitsReachableFromRefs(context.Repository) - let commitVersionOptions = tracker.GetVersion(commit) - where commitVersionOptions?.Version?.IsMatchingVersion(version) is true - where !IsCommitIdMismatch(version, commitVersionOptions, commit) - where !IsVersionHeightMismatch(version, commitVersionOptions, commit, tracker) - select commit; - - return possibleCommits; + int height = GetHeight(context, c => CommitMatchesVersion(c, baseSemVer, versionHeightPosition.Value, tracker)); + return height; } - /// - /// Finds the directory that contains the appropriate native libgit2 module. - /// - /// The path to the directory that contains the lib folder. - /// Receives the directory that native binaries are expected. - public static string? FindLibGit2NativeBinaries(string basePath) + return 0; + } + + /// + /// Encodes a commit from history in a + /// so that the original commit can be found later. + /// + /// The commit whose ID and position in history is to be encoded. + /// The version options applicable at this point (either from commit or working copy). + /// The version height, previously calculated by a call to . + /// + /// A version whose and + /// components are calculated based on the commit. + /// + /// + /// In the returned version, the component is + /// the height of the git commit while the + /// component is the first four bytes of the git commit id (forced to be a positive integer). + /// + internal static Version GetIdAsVersionHelper(this Commit? commit, VersionOptions? versionOptions, int versionHeight) + { + Version? baseVersion = versionOptions?.Version?.Version ?? Version0; + int buildNumber = baseVersion.Build; + int revision = baseVersion.Revision; + + // Don't use the ?? coalescing operator here because the position property getters themselves can return null, which should NOT be overridden with our default. + // The default value is only appropriate if versionOptions itself is null. + SemanticVersion.Position? versionHeightPosition = versionOptions is not null ? versionOptions.VersionHeightPosition : SemanticVersion.Position.Build; + SemanticVersion.Position? commitIdPosition = versionOptions is not null ? versionOptions.GitCommitIdPosition : SemanticVersion.Position.Revision; + + // The compiler (due to WinPE header requirements) only allows 16-bit version components, + // and forbids 0xffff as a value. + if (versionHeightPosition.HasValue) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + int adjustedVersionHeight = versionHeight == 0 ? 0 : versionHeight + (versionOptions?.VersionHeightOffset ?? 0); + Verify.Operation(adjustedVersionHeight <= MaximumBuildNumberOrRevisionComponent, "Git height is {0}, which is greater than the maximum allowed {0}.", adjustedVersionHeight, MaximumBuildNumberOrRevisionComponent); + switch (versionHeightPosition.Value) { - return Path.Combine(basePath, "lib", "win32", IntPtr.Size == 4 ? "x86" : "x64"); + case SemanticVersion.Position.Build: + buildNumber = adjustedVersionHeight; + break; + case SemanticVersion.Position.Revision: + revision = adjustedVersionHeight; + break; } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return Path.Combine(basePath, "lib", "linux", IntPtr.Size == 4 ? "x86" : "x86_64"); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - return Path.Combine(basePath, "lib", "osx", RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "arm_64" : "x86_64"); - } - - return null; } - /// - /// Tests whether a commit is of a specified version, comparing major and minor components - /// with the version.txt file defined by that commit. - /// - /// The commit to test. - /// The version to test for in the commit - /// The last component of the version to include in the comparison. - /// The caching tracker for storing or fetching version information per commit. - /// true if the matches the major and minor components of . - private static bool CommitMatchesVersion(this Commit commit, SemanticVersion expectedVersion, SemanticVersion.Position comparisonPrecision, GitWalkTracker tracker) + if (commitIdPosition.HasValue) { - Requires.NotNull(commit, nameof(commit)); - Requires.NotNull(expectedVersion, nameof(expectedVersion)); - - var commitVersionData = tracker.GetVersion(commit); - var semVerFromFile = commitVersionData?.Version; - if (semVerFromFile is null) + switch (commitIdPosition.Value) { - return false; - } - - // If the version height position moved, that's an automatic reset in version height. - if (commitVersionData!.VersionHeightPosition != comparisonPrecision) - { - return false; + case SemanticVersion.Position.Revision: + revision = commit is object + ? Math.Min(MaximumBuildNumberOrRevisionComponent, commit.GetTruncatedCommitIdAsUInt16()) + : 0; + break; } - - return !SemanticVersion.WillVersionChangeResetVersionHeight(commitVersionData.Version, expectedVersion, comparisonPrecision); } - /// - /// Tests whether a commit's version-spec matches a given version-spec. - /// - /// The commit to test. - /// The version to test for in the commit - /// The last component of the version to include in the comparison. - /// The caching tracker for storing or fetching version information per commit. - /// true if the matches the major and minor components of . - private static bool CommitMatchesVersion(this Commit commit, Version expectedVersion, SemanticVersion.Position comparisonPrecision, GitWalkTracker tracker) - { - Requires.NotNull(commit, nameof(commit)); - Requires.NotNull(expectedVersion, nameof(expectedVersion)); + return VersionExtensions.Create(baseVersion.Major, baseVersion.Minor, buildNumber, revision); + } - var commitVersionData = tracker.GetVersion(commit); - var semVerFromFile = commitVersionData?.Version; - if (semVerFromFile is null) - { - return false; - } + /// + /// Returns the repository that belongs to. + /// + /// Member of the repository. + /// Repository that belongs to. + private static IRepository GetRepository(this IBelongToARepository repositoryMember) + { + return repositoryMember.Repository; + } - for (SemanticVersion.Position position = SemanticVersion.Position.Major; position <= comparisonPrecision; position++) - { - int expectedValue = SemanticVersion.ReadVersionPosition(expectedVersion, position); - int actualValue = SemanticVersion.ReadVersionPosition(semVerFromFile.Version, position); - if (expectedValue != actualValue) - { - return false; - } - } + /// + /// Tests whether a commit is of a specified version, comparing major and minor components + /// with the version.txt file defined by that commit. + /// + /// The commit to test. + /// The version to test for in the commit. + /// The last component of the version to include in the comparison. + /// The caching tracker for storing or fetching version information per commit. + /// if the matches the major and minor components of . + private static bool CommitMatchesVersion(this Commit commit, SemanticVersion expectedVersion, SemanticVersion.Position comparisonPrecision, GitWalkTracker tracker) + { + Requires.NotNull(commit, nameof(commit)); + Requires.NotNull(expectedVersion, nameof(expectedVersion)); - return true; + VersionOptions? commitVersionData = tracker.GetVersion(commit); + SemanticVersion? semVerFromFile = commitVersionData?.Version; + if (semVerFromFile is null) + { + return false; } - private static bool IsVersionHeightMismatch(Version version, VersionOptions versionOptions, Commit commit, GitWalkTracker tracker) + // If the version height position moved, that's an automatic reset in version height. + if (commitVersionData!.VersionHeightPosition != comparisonPrecision) { - Requires.NotNull(version, nameof(version)); - Requires.NotNull(versionOptions, nameof(versionOptions)); - Requires.NotNull(commit, nameof(commit)); + return false; + } - // The version.Build or version.Revision MAY represent the version height. - var position = versionOptions.VersionHeightPosition; - if (position.HasValue && position.Value <= SemanticVersion.Position.Revision) - { - int expectedVersionHeight = SemanticVersion.ReadVersionPosition(version, position.Value); + return !SemanticVersion.WillVersionChangeResetVersionHeight(commitVersionData.Version, expectedVersion, comparisonPrecision); + } - var actualVersionOffset = versionOptions.VersionHeightOffsetOrDefault; - var actualVersionHeight = GetCommitHeight(commit, tracker, c => CommitMatchesVersion(c, version, position.Value - 1, tracker)); - return expectedVersionHeight != actualVersionHeight + actualVersionOffset; - } + /// + /// Tests whether a commit's version-spec matches a given version-spec. + /// + /// The commit to test. + /// The version to test for in the commit. + /// The last component of the version to include in the comparison. + /// The caching tracker for storing or fetching version information per commit. + /// if the matches the major and minor components of . + private static bool CommitMatchesVersion(this Commit commit, Version expectedVersion, SemanticVersion.Position comparisonPrecision, GitWalkTracker tracker) + { + Requires.NotNull(commit, nameof(commit)); + Requires.NotNull(expectedVersion, nameof(expectedVersion)); - // It's not a mismatch, since for this commit, the version height wasn't recorded in the 4-integer version. + VersionOptions? commitVersionData = tracker.GetVersion(commit); + SemanticVersion? semVerFromFile = commitVersionData?.Version; + if (semVerFromFile is null) + { return false; } - private static bool IsCommitIdMismatch(Version version, VersionOptions versionOptions, Commit commit) + for (SemanticVersion.Position position = SemanticVersion.Position.Major; position <= comparisonPrecision; position++) { - Requires.NotNull(version, nameof(version)); - Requires.NotNull(versionOptions, nameof(versionOptions)); - Requires.NotNull(commit, nameof(commit)); - - // The version.Revision MAY represent the first 2 bytes of the git commit ID, but not if 3 integers were specified in version.json, - // since in that case the 4th integer is the version height. But we won't know till we read the version.json file, so for now, - var position = versionOptions.GitCommitIdPosition; - if (position.HasValue && position.Value <= SemanticVersion.Position.Revision) + int expectedValue = SemanticVersion.ReadVersionPosition(expectedVersion, position); + int actualValue = SemanticVersion.ReadVersionPosition(semVerFromFile.Version, position); + if (expectedValue != actualValue) { - // prepare for it to be the commit ID. - // The revision is a 16-bit unsigned integer, but is not allowed to be 0xffff. - // So if the value is 0xfffe, consider that the actual last bit is insignificant - // since the original git commit ID could have been either 0xffff or 0xfffe. - var expectedCommitIdLeadingValue = SemanticVersion.ReadVersionPosition(version, position.Value); - if (expectedCommitIdLeadingValue != -1) - { - ushort objectIdLeadingValue = (ushort)expectedCommitIdLeadingValue; - ushort objectIdMask = (ushort)(objectIdLeadingValue == MaximumBuildNumberOrRevisionComponent ? 0xfffe : 0xffff); - - // Accept a big endian match or a little endian match. - // Nerdbank.GitVersioning up to v3.4 would produce versions based on the endianness of the CPU it ran on (typically little endian). - // Starting with v3.5, it deterministically used big endian. In order for `nbgv get-commits` to match on versions computed before and after the change, - // we match on either endian setting. - return !(commit.Id.StartsWith(objectIdLeadingValue, bigEndian: true, objectIdMask) || commit.Id.StartsWith(objectIdLeadingValue, bigEndian: false, objectIdMask)); - } + return false; } - - // It's not a mismatch, since for this commit, the commit ID wasn't recorded in the 4-integer version. - return false; } - /// - /// Tests whether an object's ID starts with the specified 16-bits, or a subset of them. - /// - /// The object whose ID is to be tested. - /// The leading 16-bits to be tested. - /// The mask that indicates which bits should be compared. - /// to read the first two bytes as big endian (v3.5+ behavior); to use little endian (v3.4 and earlier behavior). - /// True if the object's ID starts with after applying the . - private static bool StartsWith(this ObjectId @object, ushort leadingBytes, bool bigEndian, ushort bitMask = 0xffff) - { - ushort truncatedObjectId = bigEndian ? BinaryPrimitives.ReadUInt16BigEndian(@object.RawId) : BinaryPrimitives.ReadUInt16LittleEndian(@object.RawId); - return (truncatedObjectId & bitMask) == leadingBytes; - } + return true; + } - /// - /// Encodes a byte array as hex. - /// - /// The buffer to encode. - /// A hexidecimal string. - private static string EncodeAsHex(byte[] buffer) - { - Requires.NotNull(buffer, nameof(buffer)); + private static bool IsVersionHeightMismatch(Version version, VersionOptions versionOptions, Commit commit, GitWalkTracker tracker) + { + Requires.NotNull(version, nameof(version)); + Requires.NotNull(versionOptions, nameof(versionOptions)); + Requires.NotNull(commit, nameof(commit)); - var sb = new StringBuilder(); - for (int i = 0; i < buffer.Length; i++) - { - sb.AppendFormat("{0:x2}", buffer[i]); - } + // The version.Build or version.Revision MAY represent the version height. + SemanticVersion.Position? position = versionOptions.VersionHeightPosition; + if (position.HasValue && position.Value <= SemanticVersion.Position.Revision) + { + int expectedVersionHeight = SemanticVersion.ReadVersionPosition(version, position.Value); - return sb.ToString(); + int actualVersionOffset = versionOptions.VersionHeightOffsetOrDefault; + int actualVersionHeight = GetCommitHeight(commit, tracker, c => CommitMatchesVersion(c, version, position.Value - 1, tracker)); + return expectedVersionHeight != actualVersionHeight + actualVersionOffset; } - /// - /// Gets the number of commits in the longest single path between - /// the specified branch's head and the most distant ancestor (inclusive). - /// - /// The commit to measure the height of. - /// The caching tracker for storing or fetching version information per commit. - /// - /// A function that returns false when we reach a commit that - /// should not be included in the height calculation. - /// May be null to count the height to the original commit. - /// - /// The height of the branch. - private static int GetCommitHeight(Commit startingCommit, GitWalkTracker tracker, Func? continueStepping) - { - Requires.NotNull(startingCommit, nameof(startingCommit)); - Requires.NotNull(tracker, nameof(tracker)); + // It's not a mismatch, since for this commit, the version height wasn't recorded in the 4-integer version. + return false; + } - if (continueStepping is object && !continueStepping(startingCommit)) + private static bool IsCommitIdMismatch(Version version, VersionOptions versionOptions, Commit commit) + { + Requires.NotNull(version, nameof(version)); + Requires.NotNull(versionOptions, nameof(versionOptions)); + Requires.NotNull(commit, nameof(commit)); + + // The version.Revision MAY represent the first 2 bytes of the git commit ID, but not if 3 integers were specified in version.json, + // since in that case the 4th integer is the version height. But we won't know till we read the version.json file, so for now, + SemanticVersion.Position? position = versionOptions.GitCommitIdPosition; + if (position.HasValue && position.Value <= SemanticVersion.Position.Revision) + { + // prepare for it to be the commit ID. + // The revision is a 16-bit unsigned integer, but is not allowed to be 0xffff. + // So if the value is 0xfffe, consider that the actual last bit is insignificant + // since the original git commit ID could have been either 0xffff or 0xfffe. + int expectedCommitIdLeadingValue = SemanticVersion.ReadVersionPosition(version, position.Value); + if (expectedCommitIdLeadingValue != -1) { - return 0; + ushort objectIdLeadingValue = (ushort)expectedCommitIdLeadingValue; + ushort objectIdMask = (ushort)(objectIdLeadingValue == MaximumBuildNumberOrRevisionComponent ? 0xfffe : 0xffff); + + // Accept a big endian match or a little endian match. + // Nerdbank.GitVersioning up to v3.4 would produce versions based on the endianness of the CPU it ran on (typically little endian). + // Starting with v3.5, it deterministically used big endian. In order for `nbgv get-commits` to match on versions computed before and after the change, + // we match on either endian setting. + return !(commit.Id.StartsWith(objectIdLeadingValue, bigEndian: true, objectIdMask) || commit.Id.StartsWith(objectIdLeadingValue, bigEndian: false, objectIdMask)); } + } - var commitsToEvaluate = new Stack(); - bool TryCalculateHeight(Commit commit) - { - // Get max height among all parents, or schedule all missing parents for their own evaluation and return false. - int maxHeightAmongParents = 0; - bool parentMissing = false; - foreach (Commit parent in commit.Parents) - { - if (!tracker.TryGetVersionHeight(parent, out int parentHeight)) - { - if (continueStepping is object && !continueStepping(parent)) - { - // This parent isn't supposed to contribute to height. - continue; - } - - commitsToEvaluate.Push(parent); - parentMissing = true; - } - else - { - maxHeightAmongParents = Math.Max(maxHeightAmongParents, parentHeight); - } - } + // It's not a mismatch, since for this commit, the commit ID wasn't recorded in the 4-integer version. + return false; + } - if (parentMissing) - { - return false; - } + /// + /// Tests whether an object's ID starts with the specified 16-bits, or a subset of them. + /// + /// The object whose ID is to be tested. + /// The leading 16-bits to be tested. + /// to read the first two bytes as big endian (v3.5+ behavior); to use little endian (v3.4 and earlier behavior). + /// The mask that indicates which bits should be compared. + /// True if the object's ID starts with after applying the . + private static bool StartsWith(this ObjectId @object, ushort leadingBytes, bool bigEndian, ushort bitMask = 0xffff) + { + ushort truncatedObjectId = bigEndian ? BinaryPrimitives.ReadUInt16BigEndian(@object.RawId) : BinaryPrimitives.ReadUInt16LittleEndian(@object.RawId); + return (truncatedObjectId & bitMask) == leadingBytes; + } - var versionOptions = tracker.GetVersion(commit); - var pathFilters = versionOptions?.PathFilters; + /// + /// Encodes a byte array as hex. + /// + /// The buffer to encode. + /// A hexidecimal string. + private static string EncodeAsHex(byte[] buffer) + { + Requires.NotNull(buffer, nameof(buffer)); - var includePaths = - pathFilters - ?.Where(filter => !filter.IsExclude) - .Select(filter => filter.RepoRelativePath) - .ToList(); + var sb = new StringBuilder(); + for (int i = 0; i < buffer.Length; i++) + { + sb.AppendFormat("{0:x2}", buffer[i]); + } - var excludePaths = pathFilters?.Where(filter => filter.IsExclude).ToList(); + return sb.ToString(); + } - var ignoreCase = commit.GetRepository().Config.Get("core.ignorecase")?.Value ?? false; - bool ContainsRelevantChanges(IEnumerable changes) => - excludePaths?.Count == 0 - ? changes.Any() - // If there is a single change that isn't excluded, - // then this commit is relevant. - : changes.Any(change => !excludePaths.Any(exclude => exclude.Excludes(change.Path, ignoreCase))); + /// + /// Gets the number of commits in the longest single path between + /// the specified branch's head and the most distant ancestor (inclusive). + /// + /// The commit to measure the height of. + /// The caching tracker for storing or fetching version information per commit. + /// + /// A function that returns when we reach a commit that + /// should not be included in the height calculation. + /// May be null to count the height to the original commit. + /// + /// The height of the branch. + private static int GetCommitHeight(Commit startingCommit, GitWalkTracker tracker, Func? continueStepping) + { + Requires.NotNull(startingCommit, nameof(startingCommit)); + Requires.NotNull(tracker, nameof(tracker)); - int height = 1; + if (continueStepping is object && !continueStepping(startingCommit)) + { + return 0; + } - if (includePaths is not null) + var commitsToEvaluate = new Stack(); + bool TryCalculateHeight(Commit commit) + { + // Get max height among all parents, or schedule all missing parents for their own evaluation and return false. + int maxHeightAmongParents = 0; + bool parentMissing = false; + foreach (Commit parent in commit.Parents) + { + if (!tracker.TryGetVersionHeight(parent, out int parentHeight)) { - // If there are no include paths, or any of the include - // paths refer to the root of the repository, then do not - // filter the diff at all. - var diffInclude = - includePaths.Count == 0 || pathFilters.Any(filter => filter.IsRoot) - ? null - : includePaths; - - // If the diff between this commit and any of its parents - // touches a path that we care about, bump the height. - // A no-parent commit is relevant if it introduces anything in the filtered path. - var relevantCommit = - commit.Parents.Any() - ? commit.Parents.Any(parent => ContainsRelevantChanges(commit.GetRepository().Diff - .Compare(parent.Tree, commit.Tree, diffInclude, DiffOptions))) - : ContainsRelevantChanges(commit.GetRepository().Diff - .Compare(null, commit.Tree, diffInclude, DiffOptions)); - - if (!relevantCommit) + if (continueStepping is object && !continueStepping(parent)) { - height = 0; + // This parent isn't supposed to contribute to height. + continue; } - } - tracker.RecordHeight(commit, height + maxHeightAmongParents); - return true; + commitsToEvaluate.Push(parent); + parentMissing = true; + } + else + { + maxHeightAmongParents = Math.Max(maxHeightAmongParents, parentHeight); + } } - commitsToEvaluate.Push(startingCommit); - while (commitsToEvaluate.Count > 0) + if (parentMissing) { - Commit commit = commitsToEvaluate.Peek(); - if (tracker.TryGetVersionHeight(commit, out _) || TryCalculateHeight(commit)) - { - commitsToEvaluate.Pop(); - } + return false; } - Assumes.True(tracker.TryGetVersionHeight(startingCommit, out int result)); - return result; - } + VersionOptions? versionOptions = tracker.GetVersion(commit); + IReadOnlyList? pathFilters = versionOptions?.PathFilters; - /// - /// Enumerates over the set of commits in the repository that are reachable from any named reference. - /// - /// The repo to search. - /// An enumerate of commits. - private static IEnumerable GetCommitsReachableFromRefs(Repository repo) - { - Requires.NotNull(repo, nameof(repo)); + var includePaths = + pathFilters + ?.Where(filter => !filter.IsExclude) + .Select(filter => filter.RepoRelativePath) + .ToList(); + + var excludePaths = pathFilters?.Where(filter => filter.IsExclude).ToList(); + + bool ignoreCase = commit.GetRepository().Config.Get("core.ignorecase")?.Value ?? false; + bool ContainsRelevantChanges(IEnumerable changes) => + excludePaths?.Count == 0 + ? changes.Any() + //// If there is a single change that isn't excluded, + //// then this commit is relevant. + : changes.Any(change => !excludePaths!.Any(exclude => exclude.Excludes(change.Path, ignoreCase))); - var visitedCommitIds = new HashSet(); - var breadthFirstQueue = new Queue(); + int height = 1; - // Start the discovery with HEAD, and all commits that have refs pointing to them. - breadthFirstQueue.Enqueue(repo.Head.Tip); - foreach (var reference in repo.Refs) + if (includePaths is not null) { - var commit = reference.ResolveToDirectReference()?.Target as Commit; - if (commit is object) + // If there are no include paths, or any of the include + // paths refer to the root of the repository, then do not + // filter the diff at all. + List? diffInclude = + includePaths.Count == 0 || pathFilters!.Any(filter => filter.IsRoot) + ? null + : includePaths; + + // If the diff between this commit and any of its parents + // touches a path that we care about, bump the height. + // A no-parent commit is relevant if it introduces anything in the filtered path. + bool relevantCommit = + commit.Parents.Any() + ? commit.Parents.Any(parent => ContainsRelevantChanges(commit.GetRepository().Diff + .Compare(parent.Tree, commit.Tree, diffInclude, DiffOptions))) + : ContainsRelevantChanges(commit.GetRepository().Diff + .Compare(null, commit.Tree, diffInclude, DiffOptions)); + + if (!relevantCommit) { - breadthFirstQueue.Enqueue(commit); + height = 0; } } - while (breadthFirstQueue.Count > 0) + tracker.RecordHeight(commit, height + maxHeightAmongParents); + return true; + } + + commitsToEvaluate.Push(startingCommit); + while (commitsToEvaluate.Count > 0) + { + Commit commit = commitsToEvaluate.Peek(); + if (tracker.TryGetVersionHeight(commit, out _) || TryCalculateHeight(commit)) { - Commit head = breadthFirstQueue.Dequeue(); - if (visitedCommitIds.Add(head.Id)) - { - yield return head; - foreach (Commit parent in head.Parents) - { - breadthFirstQueue.Enqueue(parent); - } - } + commitsToEvaluate.Pop(); } } - /// - /// Encodes a commit from history in a - /// so that the original commit can be found later. - /// - /// The commit whose ID and position in history is to be encoded. - /// The version options applicable at this point (either from commit or working copy). - /// The version height, previously calculated by a call to . - /// - /// A version whose and - /// components are calculated based on the commit. - /// - /// - /// In the returned version, the component is - /// the height of the git commit while the - /// component is the first four bytes of the git commit id (forced to be a positive integer). - /// - internal static Version GetIdAsVersionHelper(this Commit? commit, VersionOptions? versionOptions, int versionHeight) + Assumes.True(tracker.TryGetVersionHeight(startingCommit, out int result)); + return result; + } + + /// + /// Enumerates over the set of commits in the repository that are reachable from any named reference. + /// + /// The repo to search. + /// An enumerate of commits. + private static IEnumerable GetCommitsReachableFromRefs(Repository repo) + { + Requires.NotNull(repo, nameof(repo)); + + var visitedCommitIds = new HashSet(); + var breadthFirstQueue = new Queue(); + + // Start the discovery with HEAD, and all commits that have refs pointing to them. + breadthFirstQueue.Enqueue(repo.Head.Tip); + foreach (Reference? reference in repo.Refs) { - var baseVersion = versionOptions?.Version?.Version ?? Version0; - int buildNumber = baseVersion.Build; - int revision = baseVersion.Revision; - - // Don't use the ?? coalescing operator here because the position property getters themselves can return null, which should NOT be overridden with our default. - // The default value is only appropriate if versionOptions itself is null. - var versionHeightPosition = versionOptions is not null ? versionOptions.VersionHeightPosition : SemanticVersion.Position.Build; - var commitIdPosition = versionOptions is not null ? versionOptions.GitCommitIdPosition : SemanticVersion.Position.Revision; - - // The compiler (due to WinPE header requirements) only allows 16-bit version components, - // and forbids 0xffff as a value. - if (versionHeightPosition.HasValue) + var commit = reference.ResolveToDirectReference()?.Target as Commit; + if (commit is object) { - int adjustedVersionHeight = versionHeight == 0 ? 0 : versionHeight + (versionOptions?.VersionHeightOffset ?? 0); - Verify.Operation(adjustedVersionHeight <= MaximumBuildNumberOrRevisionComponent, "Git height is {0}, which is greater than the maximum allowed {0}.", adjustedVersionHeight, MaximumBuildNumberOrRevisionComponent); - switch (versionHeightPosition.Value) - { - case SemanticVersion.Position.Build: - buildNumber = adjustedVersionHeight; - break; - case SemanticVersion.Position.Revision: - revision = adjustedVersionHeight; - break; - } + breadthFirstQueue.Enqueue(commit); } + } - if (commitIdPosition.HasValue) + while (breadthFirstQueue.Count > 0) + { + Commit head = breadthFirstQueue.Dequeue(); + if (visitedCommitIds.Add(head.Id)) { - switch (commitIdPosition.Value) + yield return head; + foreach (Commit parent in head.Parents) { - case SemanticVersion.Position.Revision: - revision = commit is object - ? Math.Min(MaximumBuildNumberOrRevisionComponent, commit.GetTruncatedCommitIdAsUInt16()) - : 0; - break; + breadthFirstQueue.Enqueue(parent); } } - - return VersionExtensions.Create(baseVersion.Major, baseVersion.Minor, buildNumber, revision); } + } - private class GitWalkTracker - { - private readonly Dictionary commitVersionCache = new Dictionary(); - private readonly Dictionary blobVersionCache = new Dictionary(); - private readonly Dictionary heights = new Dictionary(); - private readonly LibGit2Context context; + private class GitWalkTracker + { + private readonly Dictionary commitVersionCache = new Dictionary(); + private readonly Dictionary blobVersionCache = new Dictionary(); + private readonly Dictionary heights = new Dictionary(); + private readonly LibGit2Context context; - internal GitWalkTracker(LibGit2Context context) - { - this.context = context; - } + internal GitWalkTracker(LibGit2Context context) + { + this.context = context; + } - internal bool TryGetVersionHeight(Commit commit, out int height) => this.heights.TryGetValue(commit.Id, out height); + internal bool TryGetVersionHeight(Commit commit, out int height) => this.heights.TryGetValue(commit.Id, out height); - internal void RecordHeight(Commit commit, int height) => this.heights.Add(commit.Id, height); + internal void RecordHeight(Commit commit, int height) => this.heights.Add(commit.Id, height); - internal VersionOptions? GetVersion(Commit commit) + internal VersionOptions? GetVersion(Commit commit) + { + if (!this.commitVersionCache.TryGetValue(commit.Id, out VersionOptions? options)) { - if (!this.commitVersionCache.TryGetValue(commit.Id, out VersionOptions? options)) + try { - try - { - options = ((LibGit2VersionFile)this.context.VersionFile).GetVersion(commit, this.context.RepoRelativeProjectDirectory, this.blobVersionCache, out string? actualDirectory); - } - catch (Exception ex) - { - throw new InvalidOperationException($"Unable to get version from commit: {commit.Id.Sha}", ex); - } - - this.commitVersionCache.Add(commit.Id, options); + options = ((LibGit2VersionFile)this.context.VersionFile).GetVersion(commit, this.context.RepoRelativeProjectDirectory, this.blobVersionCache, out string? actualDirectory); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Unable to get version from commit: {commit.Id.Sha}", ex); } - return options; + this.commitVersionCache.Add(commit.Id, options); } + + return options; } } } diff --git a/src/NerdBank.GitVersioning/LibGit2/LibGit2VersionFile.cs b/src/NerdBank.GitVersioning/LibGit2/LibGit2VersionFile.cs index ae077ec3..d903e216 100644 --- a/src/NerdBank.GitVersioning/LibGit2/LibGit2VersionFile.cs +++ b/src/NerdBank.GitVersioning/LibGit2/LibGit2VersionFile.cs @@ -1,142 +1,141 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Nerdbank.GitVersioning.LibGit2 -{ - using System; - using System.Collections.Generic; - using System.IO; - using LibGit2Sharp; - using Newtonsoft.Json; +using LibGit2Sharp; +using Newtonsoft.Json; + +#nullable enable +namespace Nerdbank.GitVersioning.LibGit2; + +/// +/// Exposes queries and mutations on a version.json or version.txt file, +/// implemented in terms of libgit2sharp. +/// +internal class LibGit2VersionFile : VersionFile +{ /// - /// Exposes queries and mutations on a version.json or version.txt file, - /// implemented in terms of libgit2sharp. + /// A sequence of possible filenames for the version file in preferred order. /// - internal class LibGit2VersionFile : VersionFile - { - /// - /// A sequence of possible filenames for the version file in preferred order. - /// - public static readonly IReadOnlyList PreferredFileNames = new[] { JsonFileName, TxtFileName }; + public static readonly IReadOnlyList PreferredFileNames = new[] { JsonFileName, TxtFileName }; - internal LibGit2VersionFile(LibGit2Context context) - : base(context) - { - } - - protected new LibGit2Context Context => (LibGit2Context)base.Context; + internal LibGit2VersionFile(LibGit2Context context) + : base(context) + { + } - protected override VersionOptions? GetVersionCore(out string? actualDirectory) => this.GetVersion(this.Context.Commit!, this.Context.RepoRelativeProjectDirectory, null, out actualDirectory); + protected new LibGit2Context Context => (LibGit2Context)base.Context; - /// - /// Reads the version.json file and returns the deserialized from it. - /// - /// The commit to read from. - /// The directory to consider when searching for the version.txt file. - /// An optional blob cache for storing the raw parse results of a version.txt or version.json file (before any inherit merge operations are applied). - /// Receives the full path to the directory in which the version file was found. - /// The version information read from the file. - internal VersionOptions? GetVersion(Commit commit, string repoRelativeProjectDirectory, Dictionary? blobVersionCache, out string? actualDirectory) + /// + /// Reads the version.json file and returns the deserialized from it. + /// + /// The commit to read from. + /// The directory to consider when searching for the version.txt file. + /// An optional blob cache for storing the raw parse results of a version.txt or version.json file (before any inherit merge operations are applied). + /// Receives the full path to the directory in which the version file was found. + /// The version information read from the file. + internal VersionOptions? GetVersion(Commit commit, string repoRelativeProjectDirectory, Dictionary? blobVersionCache, out string? actualDirectory) + { + string? searchDirectory = repoRelativeProjectDirectory ?? string.Empty; + while (searchDirectory is object) { - string? searchDirectory = repoRelativeProjectDirectory ?? string.Empty; - while (searchDirectory is object) - { - string? parentDirectory = searchDirectory.Length > 0 ? Path.GetDirectoryName(searchDirectory) : null; + string? parentDirectory = searchDirectory.Length > 0 ? Path.GetDirectoryName(searchDirectory) : null; - string candidatePath = Path.Combine(searchDirectory, TxtFileName).Replace('\\', '/'); - var versionTxtBlob = commit.Tree[candidatePath]?.Target as Blob; - if (versionTxtBlob is object) + string candidatePath = Path.Combine(searchDirectory, TxtFileName).Replace('\\', '/'); + var versionTxtBlob = commit.Tree[candidatePath]?.Target as Blob; + if (versionTxtBlob is object) + { + if (blobVersionCache is null || !blobVersionCache.TryGetValue(versionTxtBlob.Id, out VersionOptions? result)) { - if (blobVersionCache is null || !blobVersionCache.TryGetValue(versionTxtBlob.Id, out VersionOptions? result)) + result = TryReadVersionFile(new StreamReader(versionTxtBlob.GetContentStream())); + if (blobVersionCache is object) { - result = TryReadVersionFile(new StreamReader(versionTxtBlob.GetContentStream())); - if (blobVersionCache is object) - { - result?.Freeze(); - blobVersionCache.Add(versionTxtBlob.Id, result); - } + result?.Freeze(); + blobVersionCache.Add(versionTxtBlob.Id, result); } + } - if (result is object) - { - IBelongToARepository commitAsRepoMember = commit; - actualDirectory = Path.Combine(commitAsRepoMember.Repository.Info.WorkingDirectory, searchDirectory); - return result; - } + if (result is object) + { + IBelongToARepository commitAsRepoMember = commit; + actualDirectory = Path.Combine(commitAsRepoMember.Repository.Info.WorkingDirectory, searchDirectory); + return result; } + } - candidatePath = Path.Combine(searchDirectory, JsonFileName).Replace('\\', '/'); - var versionJsonBlob = commit.Tree[candidatePath]?.Target as LibGit2Sharp.Blob; - if (versionJsonBlob is object) + candidatePath = Path.Combine(searchDirectory, JsonFileName).Replace('\\', '/'); + var versionJsonBlob = commit.Tree[candidatePath]?.Target as LibGit2Sharp.Blob; + if (versionJsonBlob is object) + { + string? versionJsonContent = null; + if (blobVersionCache is null || !blobVersionCache.TryGetValue(versionJsonBlob.Id, out VersionOptions? result)) { - string? versionJsonContent = null; - if (blobVersionCache is null || !blobVersionCache.TryGetValue(versionJsonBlob.Id, out VersionOptions? result)) + using (var sr = new StreamReader(versionJsonBlob.GetContentStream())) { - using (var sr = new StreamReader(versionJsonBlob.GetContentStream())) - { - versionJsonContent = sr.ReadToEnd(); - } + versionJsonContent = sr.ReadToEnd(); + } - try - { - result = TryReadVersionJsonContent(versionJsonContent, searchDirectory); - } - catch (FormatException ex) - { - throw new FormatException( - $"Failure while reading {JsonFileName} from commit {this.Context.GitCommitId}. " + - "Fix this commit with rebase if this is an error, or review this doc on how to migrate to Nerdbank.GitVersioning: " + - "https://github.com/dotnet/Nerdbank.GitVersioning/blob/master/doc/migrating.md", ex); - } + try + { + result = TryReadVersionJsonContent(versionJsonContent, searchDirectory); + } + catch (FormatException ex) + { + throw new FormatException( + $"Failure while reading {JsonFileName} from commit {this.Context.GitCommitId}. " + + "Fix this commit with rebase if this is an error, or review this doc on how to migrate to Nerdbank.GitVersioning: " + + "https://github.com/dotnet/Nerdbank.GitVersioning/blob/main/doc/migrating.md", + ex); + } - if (blobVersionCache is object) - { - result?.Freeze(); - blobVersionCache.Add(versionJsonBlob.Id, result); - } + if (blobVersionCache is object) + { + result?.Freeze(); + blobVersionCache.Add(versionJsonBlob.Id, result); } + } - if (result?.Inherit ?? false) + if (result?.Inherit ?? false) + { + if (parentDirectory is object) { - if (parentDirectory is object) + result = this.GetVersion(commit, parentDirectory, blobVersionCache, out actualDirectory); + if (result is object) { - result = this.GetVersion(commit, parentDirectory, blobVersionCache, out actualDirectory); - if (result is object) + if (versionJsonContent is null) { - if (versionJsonContent is null) - { - // We reused a cache VersionOptions, but now we need the actual JSON string. - using (var sr = new StreamReader(versionJsonBlob.GetContentStream())) - { - versionJsonContent = sr.ReadToEnd(); - } - } - - if (result.IsFrozen) - { - result = new VersionOptions(result); - } - - JsonConvert.PopulateObject(versionJsonContent, result, VersionOptions.GetJsonSettings(repoRelativeBaseDirectory: searchDirectory)); - return result; + // We reused a cache VersionOptions, but now we need the actual JSON string. + using var sr = new StreamReader(versionJsonBlob.GetContentStream()); + versionJsonContent = sr.ReadToEnd(); } - } - throw new InvalidOperationException($"\"{candidatePath}\" inherits from a parent directory version.json file but none exists."); - } - else if (result is object) - { - IBelongToARepository commitAsRepoMember = commit; - actualDirectory = Path.Combine(commitAsRepoMember.Repository.Info.WorkingDirectory, searchDirectory); - return result; + if (result.IsFrozen) + { + result = new VersionOptions(result); + } + + JsonConvert.PopulateObject(versionJsonContent, result, VersionOptions.GetJsonSettings(repoRelativeBaseDirectory: searchDirectory)); + return result; + } } - } - searchDirectory = parentDirectory; + throw new InvalidOperationException($"\"{candidatePath}\" inherits from a parent directory version.json file but none exists."); + } + else if (result is object) + { + IBelongToARepository commitAsRepoMember = commit; + actualDirectory = Path.Combine(commitAsRepoMember.Repository.Info.WorkingDirectory, searchDirectory); + return result; + } } - actualDirectory = null; - return null; + searchDirectory = parentDirectory; } + + actualDirectory = null; + return null; } + + /// + protected override VersionOptions? GetVersionCore(out string? actualDirectory) => this.GetVersion(this.Context.Commit!, this.Context.RepoRelativeProjectDirectory, null, out actualDirectory); } diff --git a/src/NerdBank.GitVersioning/Managed/GitExtensions.cs b/src/NerdBank.GitVersioning/Managed/GitExtensions.cs new file mode 100644 index 00000000..173ffaf3 --- /dev/null +++ b/src/NerdBank.GitVersioning/Managed/GitExtensions.cs @@ -0,0 +1,345 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +using Nerdbank.GitVersioning.ManagedGit; +using Validation; + +namespace Nerdbank.GitVersioning.Managed; + +internal static class GitExtensions +{ + /// + /// The 0.0 semver. + /// + private static readonly SemanticVersion SemVer0 = SemanticVersion.Parse("0.0"); + + /// + /// Gets the number of commits in the longest single path between + /// the specified commit and the most distant ancestor (inclusive). + /// + /// The git context. + /// + /// A function that returns when we reach a commit that + /// should not be included in the height calculation. + /// May be null to count the height to the original commit. + /// + /// The height of the commit. Always a positive integer. + public static int GetHeight(ManagedGitContext context, Func? continueStepping = null) + { + Verify.Operation(context.Commit.HasValue, "No commit is selected."); + var tracker = new GitWalkTracker(context); + return GetCommitHeight(context.Repository, context.Commit.Value, tracker, continueStepping); + } + + /// + /// Takes the first 2 bytes of a commit ID (i.e. first 4 characters of its hex-encoded SHA) + /// and returns them as an 16-bit unsigned integer. + /// + /// The commit to identify with an integer. + /// The unsigned integer which identifies a commit. + public static ushort GetTruncatedCommitIdAsUInt16(this GitCommit commit) + { + return commit.Sha.AsUInt16(); + } + + /// + /// Gets the number of commits in the longest single path between + /// the specified commit and the most distant ancestor (inclusive) + /// that set the version to the value at . + /// + /// The git context for which to calculate the height. + /// Optional base version to calculate the height. If not specified, the base version will be calculated by scanning the repository. + /// The height of the commit. Always a positive integer. + internal static int GetVersionHeight(ManagedGitContext context, Version? baseVersion = null) + { + if (context.Commit is null) + { + return 0; + } + + var tracker = new GitWalkTracker(context); + + VersionOptions? versionOptions = tracker.GetVersion(context.Commit.Value); + if (versionOptions is null) + { + return 0; + } + + SemanticVersion? baseSemVer = + baseVersion is not null ? SemanticVersion.Parse(baseVersion.ToString()) : + versionOptions.Version ?? SemVer0; + + SemanticVersion.Position? versionHeightPosition = versionOptions.VersionHeightPosition; + if (versionHeightPosition.HasValue) + { + int height = GetHeight(context, c => CommitMatchesVersion(c, baseSemVer, versionHeightPosition.Value, tracker)); + return height; + } + + return 0; + } + + /// + /// Gets the number of commits in the longest single path between + /// the specified branch's head and the most distant ancestor (inclusive). + /// + /// The Git repository. + /// The commit to measure the height of. + /// The caching tracker for storing or fetching version information per commit. + /// + /// A function that returns when we reach a commit that + /// should not be included in the height calculation. + /// May be null to count the height to the original commit. + /// + /// The height of the branch. + private static int GetCommitHeight(GitRepository repository, GitCommit startingCommit, GitWalkTracker tracker, Func? continueStepping) + { + if (continueStepping is object && !continueStepping(startingCommit)) + { + return 0; + } + + var commitsToEvaluate = new Stack(); + bool TryCalculateHeight(GitCommit commit) + { + // Get max height among all parents, or schedule all missing parents for their own evaluation and return false. + int maxHeightAmongParents = 0; + bool parentMissing = false; + foreach (GitObjectId parentId in commit.Parents) + { + GitCommit parent = repository.GetCommit(parentId); + if (!tracker.TryGetVersionHeight(parent, out int parentHeight)) + { + if (continueStepping is object && !continueStepping(parent)) + { + // This parent isn't supposed to contribute to height. + continue; + } + + commitsToEvaluate.Push(parent); + parentMissing = true; + } + else + { + maxHeightAmongParents = Math.Max(maxHeightAmongParents, parentHeight); + } + } + + if (parentMissing) + { + return false; + } + + VersionOptions? versionOptions = tracker.GetVersion(commit); + IReadOnlyList? pathFilters = versionOptions?.PathFilters; + + var includePaths = + pathFilters + ?.Where(filter => !filter.IsExclude) + .Select(filter => filter.RepoRelativePath) + .ToList(); + + var excludePaths = pathFilters?.Where(filter => filter.IsExclude).ToList(); + + bool ignoreCase = repository.IgnoreCase; + + int height = 1; + + if (pathFilters is not null) + { + // If the diff between this commit and any of its parents + // touches a path that we care about, bump the height. + bool relevantCommit = false, anyParents = false; + foreach (GitObjectId parentId in commit.Parents) + { + anyParents = true; + GitCommit parent = repository.GetCommit(parentId); + if (IsRelevantCommit(repository, commit, parent, pathFilters)) + { + // No need to scan further, as a positive match will never turn negative. + relevantCommit = true; + break; + } + } + + if (!anyParents) + { + // A no-parent commit is relevant if it introduces anything in the filtered path. + relevantCommit = IsRelevantCommit(repository, commit, parent: default(GitCommit), pathFilters); + } + + if (!relevantCommit) + { + height = 0; + } + } + + tracker.RecordHeight(commit, height + maxHeightAmongParents); + return true; + } + + commitsToEvaluate.Push(startingCommit); + while (commitsToEvaluate.Count > 0) + { + GitCommit commit = commitsToEvaluate.Peek(); + if (tracker.TryGetVersionHeight(commit, out _) || TryCalculateHeight(commit)) + { + commitsToEvaluate.Pop(); + } + } + + Assumes.True(tracker.TryGetVersionHeight(startingCommit, out int result)); + return result; + } + + private static bool IsRelevantCommit(GitRepository repository, GitCommit commit, GitCommit parent, IReadOnlyList filters) + { + return IsRelevantCommit( + repository, + repository.GetTree(commit.Tree), + parent != default ? repository.GetTree(parent.Tree) : null, + relativePath: string.Empty, + filters); + } + + private static bool IsRelevantCommit(GitRepository repository, GitTree tree, GitTree? parent, string relativePath, IReadOnlyList filters) + { + // Walk over all child nodes in the current tree. If a child node was found in the parent, + // remove it, so that after the iteration the parent contains all nodes which have been + // deleted. + foreach (KeyValuePair child in tree.Children) + { + GitTreeEntry? entry = child.Value; + GitTreeEntry? parentEntry = null; + + // If the entry is not present in the parent commit, it was added; + // if the Sha does not match, it was modified. + if (parent is null || + !parent.Children.TryGetValue(child.Key, out parentEntry) || + parentEntry.Sha != child.Value.Sha) + { + // Determine whether the change was relevant. + string? fullPath = $"{relativePath}{entry.Name}"; + + bool isRelevant = + //// Either there are no include filters at all (i.e. everything is included), or there's an explicit include filter + (!filters.Any(f => f.IsInclude) || filters.Any(f => f.Includes(fullPath, repository.IgnoreCase)) + || (!entry.IsFile && filters.Any(f => f.IncludesChildren(fullPath, repository.IgnoreCase)))) + //// The path is not excluded by any filters + && !filters.Any(f => f.Excludes(fullPath, repository.IgnoreCase)); + + // If the change was relevant, and the item is a directory, we need to recurse. + if (isRelevant && !entry.IsFile) + { + isRelevant = IsRelevantCommit( + repository, + repository.GetTree(entry.Sha), + parentEntry is null ? GitTree.Empty : repository.GetTree(parentEntry.Sha), + $"{fullPath}/", + filters); + } + + // Quit as soon as any relevant change has been detected. + if (isRelevant) + { + return true; + } + } + + if (parentEntry is not null) + { + Assumes.NotNull(parent); + parent.Children.Remove(child.Key); + } + } + + // Inspect removed entries (i.e. present in parent but not in the current tree) + if (parent is not null) + { + foreach (KeyValuePair child in parent.Children) + { + // Determine whether the change was relevant. + string? fullPath = Path.Combine(relativePath, child.Key); + + bool isRelevant = + filters.Any(f => f.Includes(fullPath, repository.IgnoreCase)) + && !filters.Any(f => f.Excludes(fullPath, repository.IgnoreCase)); + + if (isRelevant) + { + return true; + } + } + } + + // No relevant changes have been detected + return false; + } + + /// + /// Tests whether a commit is of a specified version, comparing major and minor components + /// with the version.txt file defined by that commit. + /// + /// The commit to test. + /// The version to test for in the commit. + /// The last component of the version to include in the comparison. + /// The caching tracker for storing or fetching version information per commit. + /// if the matches the major and minor components of . + private static bool CommitMatchesVersion(GitCommit commit, SemanticVersion expectedVersion, SemanticVersion.Position comparisonPrecision, GitWalkTracker tracker) + { + Requires.NotNull(expectedVersion, nameof(expectedVersion)); + + VersionOptions? commitVersionData = tracker.GetVersion(commit); + SemanticVersion? semVerFromFile = commitVersionData?.Version; + if (commitVersionData is null || semVerFromFile is null) + { + return false; + } + + // If the version height position moved, that's an automatic reset in version height. + if (commitVersionData.VersionHeightPosition != comparisonPrecision) + { + return false; + } + + return !SemanticVersion.WillVersionChangeResetVersionHeight(commitVersionData.Version, expectedVersion, comparisonPrecision); + } + + private class GitWalkTracker + { + private readonly Dictionary commitVersionCache = new Dictionary(); + private readonly Dictionary blobVersionCache = new Dictionary(); + private readonly Dictionary heights = new Dictionary(); + private readonly ManagedGitContext context; + + internal GitWalkTracker(ManagedGitContext context) + { + this.context = context; + } + + internal bool TryGetVersionHeight(GitCommit commit, out int height) => this.heights.TryGetValue(commit.Sha, out height); + + internal void RecordHeight(GitCommit commit, int height) => this.heights.Add(commit.Sha, height); + + internal VersionOptions? GetVersion(GitCommit commit) + { + if (!this.commitVersionCache.TryGetValue(commit.Sha, out VersionOptions? options)) + { + try + { + options = ((ManagedVersionFile)this.context.VersionFile).GetVersion(commit, this.context.RepoRelativeProjectDirectory, this.blobVersionCache, out string? actualDirectory); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Unable to get version from commit: {commit.Sha}", ex); + } + + this.commitVersionCache.Add(commit.Sha, options); + } + + return options; + } + } +} diff --git a/src/NerdBank.GitVersioning/Managed/ManagedGitContext.cs b/src/NerdBank.GitVersioning/Managed/ManagedGitContext.cs index a6d4fab5..aa4cc107 100644 --- a/src/NerdBank.GitVersioning/Managed/ManagedGitContext.cs +++ b/src/NerdBank.GitVersioning/Managed/ManagedGitContext.cs @@ -1,191 +1,192 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Text; using Nerdbank.GitVersioning.ManagedGit; using Validation; -namespace Nerdbank.GitVersioning.Managed +namespace Nerdbank.GitVersioning.Managed; + +/// +/// A git context implemented without any native code dependency. +/// +[DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] +public class ManagedGitContext : GitContext { - /// - /// A git context implemented without any native code dependency. - /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] - public class ManagedGitContext : GitContext + internal ManagedGitContext(string workingDirectory, string dotGitPath, string? committish = null) + : base(workingDirectory, dotGitPath) { - internal ManagedGitContext(string workingDirectory, string dotGitPath, string? committish = null) - : base(workingDirectory, dotGitPath) + var repo = GitRepository.Create(workingDirectory); + if (repo is null) { - GitRepository? repo = GitRepository.Create(workingDirectory); - if (repo is null) - { - throw new ArgumentException("No git repo found here.", nameof(workingDirectory)); - } - - this.Commit = committish is null ? repo.GetHeadCommit() : (repo.Lookup(committish) is { } objectId ? (GitCommit?)repo.GetCommit(objectId) : null); - if (this.Commit is null && committish is object) - { - throw new ArgumentException("No matching commit found.", nameof(committish)); - } + throw new ArgumentException("No git repo found here.", nameof(workingDirectory)); + } - this.Repository = repo; - this.VersionFile = new ManagedVersionFile(this); + this.Commit = committish is null ? repo.GetHeadCommit() : (repo.Lookup(committish) is { } objectId ? (GitCommit?)repo.GetCommit(objectId) : null); + if (this.Commit is null && committish is object) + { + throw new ArgumentException("No matching commit found.", nameof(committish)); } - /// - public GitRepository Repository { get; } + this.Repository = repo; + this.VersionFile = new ManagedVersionFile(this); + } - /// - public GitCommit? Commit { get; private set; } + public GitRepository Repository { get; } - /// - public override VersionFile VersionFile { get; } + public GitCommit? Commit { get; private set; } - /// - public override string? GitCommitId => this.Commit?.Sha.ToString(); + /// + public override VersionFile VersionFile { get; } - /// - public override bool IsHead => this.Repository.GetHeadCommit().Equals(this.Commit); + /// + public override string? GitCommitId => this.Commit?.Sha.ToString(); - /// - public override DateTimeOffset? GitCommitDate => this.Commit is { } commit ? (commit.Author?.Date ?? this.Repository.GetCommit(commit.Sha, readAuthor: true).Author?.Date) : null; + /// + public override bool IsHead => this.Repository.GetHeadCommit().Equals(this.Commit); - /// - public override string HeadCanonicalName => this.Repository.GetHeadAsReferenceOrSha().ToString() ?? throw new InvalidOperationException("Unable to determine the HEAD position."); + /// + public override DateTimeOffset? GitCommitDate => this.Commit is { } commit ? (commit.Author?.Date ?? this.Repository.GetCommit(commit.Sha, readAuthor: true).Author?.Date) : null; - private string DebuggerDisplay => $"\"{this.WorkingTreePath}\" (managed)"; + /// + public override string HeadCanonicalName => this.Repository.GetHeadAsReferenceOrSha().ToString() ?? throw new InvalidOperationException("Unable to determine the HEAD position."); - /// - public override void ApplyTag(string name) => throw new NotSupportedException(); + private string DebuggerDisplay => $"\"{this.WorkingTreePath}\" (managed)"; - /// - public override bool TrySelectCommit(string committish) + /// Initializes a new instance of the class. + /// The path to the .git directory or somewhere in a git working tree. + /// The SHA-1 or ref for a git commit. + /// The new instance. + public static ManagedGitContext Create(string path, string? committish = null) + { + FindGitPaths(path, out string? gitDirectory, out string? workingTreeDirectory, out string? workingTreeRelativePath); + return new ManagedGitContext(workingTreeDirectory, gitDirectory, committish) { - if (this.Repository.Lookup(committish) is { } objectId) - { - this.Commit = this.Repository.GetCommit(objectId); - return true; - } - - return false; - } + RepoRelativeProjectDirectory = workingTreeRelativePath, + }; + } - /// - public override void Stage(string path) => throw new NotSupportedException(); + /// + public override void ApplyTag(string name) => throw new NotSupportedException(); - /// The path to the .git directory or somewhere in a git working tree. - /// The SHA-1 or ref for a git commit. - public static ManagedGitContext Create(string path, string? committish = null) + /// + public override bool TrySelectCommit(string committish) + { + if (this.Repository.Lookup(committish) is { } objectId) { - FindGitPaths(path, out string? gitDirectory, out string? workingTreeDirectory, out string? workingTreeRelativePath); - return new ManagedGitContext(workingTreeDirectory, gitDirectory, committish) - { - RepoRelativeProjectDirectory = workingTreeRelativePath, - }; + this.Commit = this.Repository.GetCommit(objectId); + return true; } - internal override int CalculateVersionHeight(VersionOptions? committedVersion, VersionOptions? workingVersion) + return false; + } + + /// + public override void Stage(string path) => throw new NotSupportedException(); + + /// + public override string GetShortUniqueCommitId(int minLength) + { + Verify.Operation(this.Commit is object, "No commit is selected."); + return this.Repository.ShortenObjectId(this.Commit.Value.Sha, minLength); + } + + /// + internal override int CalculateVersionHeight(VersionOptions? committedVersion, VersionOptions? workingVersion) + { + SemanticVersion? headCommitVersion = committedVersion?.Version ?? SemVer0; + + if (IsVersionFileChangedInWorkingTree(committedVersion, workingVersion)) { - var headCommitVersion = committedVersion?.Version ?? SemVer0; + Version? workingCopyVersion = workingVersion?.Version?.Version; - if (IsVersionFileChangedInWorkingTree(committedVersion, workingVersion)) + if (workingCopyVersion is null || !workingCopyVersion.Equals(headCommitVersion)) { - var workingCopyVersion = workingVersion?.Version?.Version; - - if (workingCopyVersion is null || !workingCopyVersion.Equals(headCommitVersion)) - { - // The working copy has changed the major.minor version. - // So by definition the version height is 0, since no commit represents it yet. - return 0; - } + // The working copy has changed the major.minor version. + // So by definition the version height is 0, since no commit represents it yet. + return 0; } - - return GitExtensions.GetVersionHeight(this); } - internal override Version GetIdAsVersion(VersionOptions? committedVersion, VersionOptions? workingVersion, int versionHeight) - { - var version = IsVersionFileChangedInWorkingTree(committedVersion, workingVersion) ? workingVersion : committedVersion; + return GitExtensions.GetVersionHeight(this); + } - return this.GetIdAsVersionHelper(version, versionHeight); - } + /// + internal override Version GetIdAsVersion(VersionOptions? committedVersion, VersionOptions? workingVersion, int versionHeight) + { + VersionOptions? version = IsVersionFileChangedInWorkingTree(committedVersion, workingVersion) ? workingVersion : committedVersion; - /// - public override string GetShortUniqueCommitId(int minLength) + return this.GetIdAsVersionHelper(version, versionHeight); + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) { - Verify.Operation(this.Commit is object, "No commit is selected."); - return this.Repository.ShortenObjectId(this.Commit.Value.Sha, minLength); + this.Repository.Dispose(); } - /// - protected override void Dispose(bool disposing) + base.Dispose(disposing); + } + + /// + /// Encodes a commit from history in a + /// so that the original commit can be found later. + /// + /// The version options applicable at this point (either from commit or working copy). + /// The version height, previously calculated. + /// + /// A version whose and + /// components are calculated based on the commit. + /// + /// + /// In the returned version, the component is + /// the height of the git commit while the + /// component is the first four bytes of the git commit id (forced to be a positive integer). + /// + private Version GetIdAsVersionHelper(VersionOptions? versionOptions, int versionHeight) + { + Version? baseVersion = versionOptions?.Version?.Version ?? Version0; + int buildNumber = baseVersion.Build; + int revision = baseVersion.Revision; + + // Don't use the ?? coalescing operator here because the position property getters themselves can return null, which should NOT be overridden with our default. + // The default value is only appropriate if versionOptions itself is null. + SemanticVersion.Position? versionHeightPosition = versionOptions is not null ? versionOptions.VersionHeightPosition : SemanticVersion.Position.Build; + SemanticVersion.Position? commitIdPosition = versionOptions is not null ? versionOptions.GitCommitIdPosition : SemanticVersion.Position.Revision; + + // The compiler (due to WinPE header requirements) only allows 16-bit version components, + // and forbids 0xffff as a value. + if (versionHeightPosition.HasValue) { - if (disposing) + int adjustedVersionHeight = versionHeight == 0 ? 0 : versionHeight + (versionOptions?.VersionHeightOffset ?? 0); + Verify.Operation(adjustedVersionHeight <= MaximumBuildNumberOrRevisionComponent, "Git height is {0}, which is greater than the maximum allowed {0}.", adjustedVersionHeight, MaximumBuildNumberOrRevisionComponent); + switch (versionHeightPosition.Value) { - this.Repository.Dispose(); + case SemanticVersion.Position.Build: + buildNumber = adjustedVersionHeight; + break; + case SemanticVersion.Position.Revision: + revision = adjustedVersionHeight; + break; } - - base.Dispose(disposing); } - /// - /// Encodes a commit from history in a - /// so that the original commit can be found later. - /// - /// The version options applicable at this point (either from commit or working copy). - /// The version height, previously calculated. - /// - /// A version whose and - /// components are calculated based on the commit. - /// - /// - /// In the returned version, the component is - /// the height of the git commit while the - /// component is the first four bytes of the git commit id (forced to be a positive integer). - /// - private Version GetIdAsVersionHelper(VersionOptions? versionOptions, int versionHeight) + if (commitIdPosition.HasValue) { - var baseVersion = versionOptions?.Version?.Version ?? Version0; - int buildNumber = baseVersion.Build; - int revision = baseVersion.Revision; - - // Don't use the ?? coalescing operator here because the position property getters themselves can return null, which should NOT be overridden with our default. - // The default value is only appropriate if versionOptions itself is null. - var versionHeightPosition = versionOptions is not null ? versionOptions.VersionHeightPosition : SemanticVersion.Position.Build; - var commitIdPosition = versionOptions is not null ? versionOptions.GitCommitIdPosition : SemanticVersion.Position.Revision; - - // The compiler (due to WinPE header requirements) only allows 16-bit version components, - // and forbids 0xffff as a value. - if (versionHeightPosition.HasValue) + switch (commitIdPosition.Value) { - int adjustedVersionHeight = versionHeight == 0 ? 0 : versionHeight + (versionOptions?.VersionHeightOffset ?? 0); - Verify.Operation(adjustedVersionHeight <= MaximumBuildNumberOrRevisionComponent, "Git height is {0}, which is greater than the maximum allowed {0}.", adjustedVersionHeight, MaximumBuildNumberOrRevisionComponent); - switch (versionHeightPosition.Value) - { - case SemanticVersion.Position.Build: - buildNumber = adjustedVersionHeight; - break; - case SemanticVersion.Position.Revision: - revision = adjustedVersionHeight; - break; - } + case SemanticVersion.Position.Revision: + revision = this.Commit.HasValue + ? Math.Min(MaximumBuildNumberOrRevisionComponent, this.Commit.Value.GetTruncatedCommitIdAsUInt16()) + : 0; + break; } - - if (commitIdPosition.HasValue) - { - switch (commitIdPosition.Value) - { - case SemanticVersion.Position.Revision: - revision = this.Commit.HasValue - ? Math.Min(MaximumBuildNumberOrRevisionComponent, this.Commit.Value.GetTruncatedCommitIdAsUInt16()) - : 0; - break; - } - } - - return VersionExtensions.Create(baseVersion.Major, baseVersion.Minor, buildNumber, revision); } + + return VersionExtensions.Create(baseVersion.Major, baseVersion.Minor, buildNumber, revision); } } diff --git a/src/NerdBank.GitVersioning/Managed/ManagedGitExtensions.cs b/src/NerdBank.GitVersioning/Managed/ManagedGitExtensions.cs deleted file mode 100644 index e51bf4ea..00000000 --- a/src/NerdBank.GitVersioning/Managed/ManagedGitExtensions.cs +++ /dev/null @@ -1,348 +0,0 @@ -#nullable enable - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using Nerdbank.GitVersioning.ManagedGit; -using Validation; - -namespace Nerdbank.GitVersioning.Managed -{ - internal static class GitExtensions - { - /// - /// The 0.0 semver. - /// - private static readonly SemanticVersion SemVer0 = SemanticVersion.Parse("0.0"); - - /// - /// Gets the number of commits in the longest single path between - /// the specified commit and the most distant ancestor (inclusive) - /// that set the version to the value at . - /// - /// The git context for which to calculate the height. - /// Optional base version to calculate the height. If not specified, the base version will be calculated by scanning the repository. - /// The height of the commit. Always a positive integer. - internal static int GetVersionHeight(ManagedGitContext context, Version? baseVersion = null) - { - if (context.Commit is null) - { - return 0; - } - - var tracker = new GitWalkTracker(context); - - var versionOptions = tracker.GetVersion(context.Commit.Value); - if (versionOptions is null) - { - return 0; - } - - var baseSemVer = - baseVersion is not null ? SemanticVersion.Parse(baseVersion.ToString()) : - versionOptions.Version ?? SemVer0; - - var versionHeightPosition = versionOptions.VersionHeightPosition; - if (versionHeightPosition.HasValue) - { - int height = GetHeight(context, c => CommitMatchesVersion(c, baseSemVer, versionHeightPosition.Value, tracker)); - return height; - } - - return 0; - } - - /// - /// Tests whether a commit is of a specified version, comparing major and minor components - /// with the version.txt file defined by that commit. - /// - /// The commit to test. - /// The version to test for in the commit - /// The last component of the version to include in the comparison. - /// The caching tracker for storing or fetching version information per commit. - /// true if the matches the major and minor components of . - private static bool CommitMatchesVersion(GitCommit commit, SemanticVersion expectedVersion, SemanticVersion.Position comparisonPrecision, GitWalkTracker tracker) - { - Requires.NotNull(expectedVersion, nameof(expectedVersion)); - - var commitVersionData = tracker.GetVersion(commit); - var semVerFromFile = commitVersionData?.Version; - if (commitVersionData is null || semVerFromFile is null) - { - return false; - } - - // If the version height position moved, that's an automatic reset in version height. - if (commitVersionData.VersionHeightPosition != comparisonPrecision) - { - return false; - } - - return !SemanticVersion.WillVersionChangeResetVersionHeight(commitVersionData.Version, expectedVersion, comparisonPrecision); - } - - /// - /// Gets the number of commits in the longest single path between - /// the specified commit and the most distant ancestor (inclusive). - /// - /// The git context. - /// - /// A function that returns false when we reach a commit that - /// should not be included in the height calculation. - /// May be null to count the height to the original commit. - /// - /// The height of the commit. Always a positive integer. - public static int GetHeight(ManagedGitContext context, Func? continueStepping = null) - { - Verify.Operation(context.Commit.HasValue, "No commit is selected."); - var tracker = new GitWalkTracker(context); - return GetCommitHeight(context.Repository, context.Commit.Value, tracker, continueStepping); - } - - /// - /// Gets the number of commits in the longest single path between - /// the specified branch's head and the most distant ancestor (inclusive). - /// - /// The Git repository. - /// The commit to measure the height of. - /// The caching tracker for storing or fetching version information per commit. - /// - /// A function that returns false when we reach a commit that - /// should not be included in the height calculation. - /// May be null to count the height to the original commit. - /// - /// The height of the branch. - private static int GetCommitHeight(GitRepository repository, GitCommit startingCommit, GitWalkTracker tracker, Func? continueStepping) - { - if (continueStepping is object && !continueStepping(startingCommit)) - { - return 0; - } - - var commitsToEvaluate = new Stack(); - bool TryCalculateHeight(GitCommit commit) - { - // Get max height among all parents, or schedule all missing parents for their own evaluation and return false. - int maxHeightAmongParents = 0; - bool parentMissing = false; - foreach (GitObjectId parentId in commit.Parents) - { - var parent = repository.GetCommit(parentId); - if (!tracker.TryGetVersionHeight(parent, out int parentHeight)) - { - if (continueStepping is object && !continueStepping(parent)) - { - // This parent isn't supposed to contribute to height. - continue; - } - - commitsToEvaluate.Push(parent); - parentMissing = true; - } - else - { - maxHeightAmongParents = Math.Max(maxHeightAmongParents, parentHeight); - } - } - - if (parentMissing) - { - return false; - } - - var versionOptions = tracker.GetVersion(commit); - var pathFilters = versionOptions?.PathFilters; - - var includePaths = - pathFilters - ?.Where(filter => !filter.IsExclude) - .Select(filter => filter.RepoRelativePath) - .ToList(); - - var excludePaths = pathFilters?.Where(filter => filter.IsExclude).ToList(); - - var ignoreCase = repository.IgnoreCase; - - int height = 1; - - if (pathFilters is not null) - { - // If the diff between this commit and any of its parents - // touches a path that we care about, bump the height. - bool relevantCommit = false, anyParents = false; - foreach (GitObjectId parentId in commit.Parents) - { - anyParents = true; - GitCommit parent = repository.GetCommit(parentId); - if (IsRelevantCommit(repository, commit, parent, pathFilters)) - { - // No need to scan further, as a positive match will never turn negative. - relevantCommit = true; - break; - } - } - - if (!anyParents) - { - // A no-parent commit is relevant if it introduces anything in the filtered path. - relevantCommit = IsRelevantCommit(repository, commit, parent: default(GitCommit), pathFilters); - } - - if (!relevantCommit) - { - height = 0; - } - } - - tracker.RecordHeight(commit, height + maxHeightAmongParents); - return true; - } - - commitsToEvaluate.Push(startingCommit); - while (commitsToEvaluate.Count > 0) - { - GitCommit commit = commitsToEvaluate.Peek(); - if (tracker.TryGetVersionHeight(commit, out _) || TryCalculateHeight(commit)) - { - commitsToEvaluate.Pop(); - } - } - - Assumes.True(tracker.TryGetVersionHeight(startingCommit, out int result)); - return result; - } - - private static bool IsRelevantCommit(GitRepository repository, GitCommit commit, GitCommit parent, IReadOnlyList filters) - { - return IsRelevantCommit( - repository, - repository.GetTree(commit.Tree), - parent != default ? repository.GetTree(parent.Tree) : null, - relativePath: string.Empty, - filters); - } - - private static bool IsRelevantCommit(GitRepository repository, GitTree tree, GitTree? parent, string relativePath, IReadOnlyList filters) - { - // Walk over all child nodes in the current tree. If a child node was found in the parent, - // remove it, so that after the iteration the parent contains all nodes which have been - // deleted. - foreach (var child in tree.Children) - { - var entry = child.Value; - GitTreeEntry? parentEntry = null; - - // If the entry is not present in the parent commit, it was added; - // if the Sha does not match, it was modified. - if (parent is null || - !parent.Children.TryGetValue(child.Key, out parentEntry) || - parentEntry.Sha != child.Value.Sha) - { - // Determine whether the change was relevant. - var fullPath = $"{relativePath}{entry.Name}"; - - bool isRelevant = - // Either there are no include filters at all (i.e. everything is included), or there's an explicit include filter - (!filters.Any(f => f.IsInclude) || filters.Any(f => f.Includes(fullPath, repository.IgnoreCase)) - || (!entry.IsFile && filters.Any(f => f.IncludesChildren(fullPath, repository.IgnoreCase)))) - // The path is not excluded by any filters - && !filters.Any(f => f.Excludes(fullPath, repository.IgnoreCase)); - - // If the change was relevant, and the item is a directory, we need to recurse. - if (isRelevant && !entry.IsFile) - { - isRelevant = IsRelevantCommit( - repository, - repository.GetTree(entry.Sha), - parentEntry is null ? GitTree.Empty : repository.GetTree(parentEntry.Sha), - $"{fullPath}/", - filters); - } - - // Quit as soon as any relevant change has been detected. - if (isRelevant) - { - return true; - } - } - - if (parentEntry is not null) - { - Assumes.NotNull(parent); - parent.Children.Remove(child.Key); - } - } - - // Inspect removed entries (i.e. present in parent but not in the current tree) - if (parent is not null) - { - foreach (var child in parent.Children) - { - // Determine whether the change was relevant. - var fullPath = Path.Combine(relativePath, child.Key); - - bool isRelevant = - filters.Any(f => f.Includes(fullPath, repository.IgnoreCase)) - && !filters.Any(f => f.Excludes(fullPath, repository.IgnoreCase)); - - if (isRelevant) - { - return true; - } - } - } - - // No relevant changes have been detected - return false; - } - - /// - /// Takes the first 2 bytes of a commit ID (i.e. first 4 characters of its hex-encoded SHA) - /// and returns them as an 16-bit unsigned integer. - /// - /// The commit to identify with an integer. - /// The unsigned integer which identifies a commit. - public static ushort GetTruncatedCommitIdAsUInt16(this GitCommit commit) - { - return commit.Sha.AsUInt16(); - } - - private class GitWalkTracker - { - private readonly Dictionary commitVersionCache = new Dictionary(); - private readonly Dictionary blobVersionCache = new Dictionary(); - private readonly Dictionary heights = new Dictionary(); - private readonly ManagedGitContext context; - - internal GitWalkTracker(ManagedGitContext context) - { - this.context = context; - } - - internal bool TryGetVersionHeight(GitCommit commit, out int height) => this.heights.TryGetValue(commit.Sha, out height); - - internal void RecordHeight(GitCommit commit, int height) => this.heights.Add(commit.Sha, height); - - internal VersionOptions? GetVersion(GitCommit commit) - { - if (!this.commitVersionCache.TryGetValue(commit.Sha, out VersionOptions? options)) - { - try - { - options = ((ManagedVersionFile)this.context.VersionFile).GetVersion(commit, this.context.RepoRelativeProjectDirectory, this.blobVersionCache, out string? actualDirectory); - } - catch (Exception ex) - { - throw new InvalidOperationException($"Unable to get version from commit: {commit.Sha}", ex); - } - - this.commitVersionCache.Add(commit.Sha, options); - } - - return options; - } - } - } -} diff --git a/src/NerdBank.GitVersioning/Managed/ManagedVersionFile.cs b/src/NerdBank.GitVersioning/Managed/ManagedVersionFile.cs index 06e9e8ee..bce00516 100644 --- a/src/NerdBank.GitVersioning/Managed/ManagedVersionFile.cs +++ b/src/NerdBank.GitVersioning/Managed/ManagedVersionFile.cs @@ -1,186 +1,182 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Nerdbank.GitVersioning.Managed +using System.Text; +using Nerdbank.GitVersioning.ManagedGit; +using Newtonsoft.Json; + +#nullable enable + +namespace Nerdbank.GitVersioning.Managed; + +/// +/// Exposes queries and mutations on a version.json or version.txt file, +/// implemented in terms of our private managed git implementation. +/// +internal class ManagedVersionFile : VersionFile { - using System; - using System.Collections.Generic; - using System.IO; - using System.Text; - using Nerdbank.GitVersioning; - using Nerdbank.GitVersioning.ManagedGit; - using Newtonsoft.Json; - using Validation; + /// + /// The filename of the version.txt file, as a byte array. + /// + private static readonly byte[] TxtFileNameBytes = Encoding.ASCII.GetBytes(TxtFileName); /// - /// Exposes queries and mutations on a version.json or version.txt file, - /// implemented in terms of our private managed git implementation. + /// The filename of the version.json file, as a byte array. /// - internal class ManagedVersionFile : VersionFile - { - /// - /// The filename of the version.txt file, as a byte array. - /// - private static readonly byte[] TxtFileNameBytes = Encoding.ASCII.GetBytes(TxtFileName); - - /// - /// The filename of the version.json file, as a byte array. - /// - private static readonly byte[] JsonFileNameBytes = Encoding.ASCII.GetBytes(JsonFileName); - - /// - /// Initializes a new instance of the class. - /// - /// - public ManagedVersionFile(GitContext context) - : base(context) - { - } + private static readonly byte[] JsonFileNameBytes = Encoding.ASCII.GetBytes(JsonFileName); - protected new ManagedGitContext Context => (ManagedGitContext)base.Context; + /// + /// Initializes a new instance of the class. + /// + /// + public ManagedVersionFile(GitContext context) + : base(context) + { + } - protected override VersionOptions? GetVersionCore(out string? actualDirectory) => this.GetVersion(this.Context.Commit!.Value, this.Context.RepoRelativeProjectDirectory, null, out actualDirectory); + protected new ManagedGitContext Context => (ManagedGitContext)base.Context; - /// - /// Reads the version.json file and returns the deserialized from it. - /// - /// The commit to read from. - /// The directory to consider when searching for the version.txt file. - /// An optional blob cache for storing the raw parse results of a version.txt or version.json file (before any inherit merge operations are applied). - /// Receives the full path to the directory in which the version file was found. - /// The version information read from the file. - internal VersionOptions? GetVersion(GitCommit commit, string repoRelativeProjectDirectory, Dictionary? blobVersionCache, out string? actualDirectory) - { - Stack directories = new Stack(); + /// + /// Reads the version.json file and returns the deserialized from it. + /// + /// The commit to read from. + /// The directory to consider when searching for the version.txt file. + /// An optional blob cache for storing the raw parse results of a version.txt or version.json file (before any inherit merge operations are applied). + /// Receives the full path to the directory in which the version file was found. + /// The version information read from the file. + internal VersionOptions? GetVersion(GitCommit commit, string repoRelativeProjectDirectory, Dictionary? blobVersionCache, out string? actualDirectory) + { + var directories = new Stack(); - string? currentDirectory = repoRelativeProjectDirectory; + string? currentDirectory = repoRelativeProjectDirectory; - while (!string.IsNullOrEmpty(currentDirectory)) - { - directories.Push(Path.GetFileName(currentDirectory)); - currentDirectory = Path.GetDirectoryName(currentDirectory); - } + while (!string.IsNullOrEmpty(currentDirectory)) + { + directories.Push(Path.GetFileName(currentDirectory)); + currentDirectory = Path.GetDirectoryName(currentDirectory); + } - GitObjectId tree = commit.Tree; - string? searchDirectory = string.Empty; - string? parentDirectory = null; + GitObjectId tree = commit.Tree; + string? searchDirectory = string.Empty; + string? parentDirectory = null; - VersionOptions? finalResult = null; - actualDirectory = null; + VersionOptions? finalResult = null; + actualDirectory = null; - while (tree != GitObjectId.Empty) + while (tree != GitObjectId.Empty) + { + GitObjectId versionTxtBlob = this.Context.Repository.GetTreeEntry(tree, TxtFileNameBytes); + if (versionTxtBlob != GitObjectId.Empty) { - var versionTxtBlob = this.Context.Repository.GetTreeEntry(tree, TxtFileNameBytes); - if (versionTxtBlob != GitObjectId.Empty) + if (blobVersionCache is null || !blobVersionCache.TryGetValue(versionTxtBlob, out VersionOptions? result)) { - if (blobVersionCache is null || !blobVersionCache.TryGetValue(versionTxtBlob, out VersionOptions? result)) + result = TryReadVersionFile(new StreamReader(this.Context.Repository.GetObjectBySha(versionTxtBlob, "blob")!)); + if (blobVersionCache is object) { - result = TryReadVersionFile(new StreamReader(this.Context.Repository.GetObjectBySha(versionTxtBlob, "blob")!)); - if (blobVersionCache is object) - { - result?.Freeze(); - blobVersionCache.Add(versionTxtBlob, result); - } + result?.Freeze(); + blobVersionCache.Add(versionTxtBlob, result); } + } - if (result is object) - { - finalResult = result; - actualDirectory = Path.Combine(this.Context.WorkingTreePath, searchDirectory); - } + if (result is object) + { + finalResult = result; + actualDirectory = Path.Combine(this.Context.WorkingTreePath, searchDirectory); } + } - var versionJsonBlob = this.Context.Repository.GetTreeEntry(tree, JsonFileNameBytes); - if (versionJsonBlob != GitObjectId.Empty) + GitObjectId versionJsonBlob = this.Context.Repository.GetTreeEntry(tree, JsonFileNameBytes); + if (versionJsonBlob != GitObjectId.Empty) + { + string? versionJsonContent = null; + if (blobVersionCache is null || !blobVersionCache.TryGetValue(versionJsonBlob, out VersionOptions? result)) { - string? versionJsonContent = null; - if (blobVersionCache is null || !blobVersionCache.TryGetValue(versionJsonBlob, out VersionOptions? result)) + using (var sr = new StreamReader(this.Context.Repository.GetObjectBySha(versionJsonBlob, "blob")!)) { - using (var sr = new StreamReader(this.Context.Repository.GetObjectBySha(versionJsonBlob, "blob")!)) - { - versionJsonContent = sr.ReadToEnd(); - } + versionJsonContent = sr.ReadToEnd(); + } - try - { - result = TryReadVersionJsonContent(versionJsonContent, searchDirectory); - } - catch (FormatException ex) - { - throw new FormatException( - $"Failure while reading {JsonFileName} from commit {this.Context.GitCommitId}. " + - "Fix this commit with rebase if this is an error, or review this doc on how to migrate to Nerdbank.GitVersioning: " + - "https://github.com/dotnet/Nerdbank.GitVersioning/blob/master/doc/migrating.md", ex); - } + try + { + result = TryReadVersionJsonContent(versionJsonContent, searchDirectory); + } + catch (FormatException ex) + { + throw new FormatException( + $"Failure while reading {JsonFileName} from commit {this.Context.GitCommitId}. " + + "Fix this commit with rebase if this is an error, or review this doc on how to migrate to Nerdbank.GitVersioning: " + + "https://github.com/dotnet/Nerdbank.GitVersioning/blob/main/doc/migrating.md", + ex); + } - if (blobVersionCache is object) - { - result?.Freeze(); - blobVersionCache.Add(versionJsonBlob, result); - } + if (blobVersionCache is object) + { + result?.Freeze(); + blobVersionCache.Add(versionJsonBlob, result); } + } - if (result?.Inherit ?? false) + if (result?.Inherit ?? false) + { + if (parentDirectory is object) { - if (parentDirectory is object) + result = this.GetVersion(commit, parentDirectory, blobVersionCache, out string? resultingDirectory); + if (result is object) { - result = this.GetVersion(commit, parentDirectory, blobVersionCache, out string? resultingDirectory); - if (result is object) + if (versionJsonContent is null) { - if (versionJsonContent is null) - { - // We reused a cache VersionOptions, but now we need the actual JSON string. - using (var sr = new StreamReader(this.Context.Repository.GetObjectBySha(versionJsonBlob, "blob")!)) - { - versionJsonContent = sr.ReadToEnd(); - } - } - - if (result.IsFrozen) - { - result = new VersionOptions(result); - } - - JsonConvert.PopulateObject(versionJsonContent, result, VersionOptions.GetJsonSettings(repoRelativeBaseDirectory: searchDirectory)); - finalResult = result; + // We reused a cache VersionOptions, but now we need the actual JSON string. + using var sr = new StreamReader(this.Context.Repository.GetObjectBySha(versionJsonBlob, "blob")!); + versionJsonContent = sr.ReadToEnd(); } - else + + if (result.IsFrozen) { - var candidatePath = Path.Combine(searchDirectory, JsonFileName); - throw new InvalidOperationException($"\"{candidatePath}\" inherits from a parent directory version.json file but none exists."); + result = new VersionOptions(result); } + + JsonConvert.PopulateObject(versionJsonContent, result, VersionOptions.GetJsonSettings(repoRelativeBaseDirectory: searchDirectory)); + finalResult = result; } else { - var candidatePath = Path.Combine(searchDirectory, JsonFileName); + string? candidatePath = Path.Combine(searchDirectory, JsonFileName); throw new InvalidOperationException($"\"{candidatePath}\" inherits from a parent directory version.json file but none exists."); } } - - if (result is object) + else { - actualDirectory = Path.Combine(this.Context.WorkingTreePath, searchDirectory); - finalResult = result; + string? candidatePath = Path.Combine(searchDirectory, JsonFileName); + throw new InvalidOperationException($"\"{candidatePath}\" inherits from a parent directory version.json file but none exists."); } } - - if (directories.Count > 0) - { - var directoryName = directories.Pop(); - tree = this.Context.Repository.GetTreeEntry(tree, GitRepository.Encoding.GetBytes(directoryName)); - parentDirectory = searchDirectory; - searchDirectory = Path.Combine(searchDirectory, directoryName); - } - else + if (result is object) { - tree = GitObjectId.Empty; - parentDirectory = null; - searchDirectory = null; - break; + actualDirectory = Path.Combine(this.Context.WorkingTreePath, searchDirectory); + finalResult = result; } } - return finalResult; + if (directories.Count > 0) + { + string? directoryName = directories.Pop(); + tree = this.Context.Repository.GetTreeEntry(tree, GitRepository.Encoding.GetBytes(directoryName)); + parentDirectory = searchDirectory; + searchDirectory = Path.Combine(searchDirectory, directoryName); + } + else + { + tree = GitObjectId.Empty; + parentDirectory = null; + searchDirectory = null; + break; + } } + + return finalResult; } + + /// + protected override VersionOptions? GetVersionCore(out string? actualDirectory) => this.GetVersion(this.Context.Commit!.Value, this.Context.RepoRelativeProjectDirectory, null, out actualDirectory); } diff --git a/src/NerdBank.GitVersioning/ManagedGit/DeltaInstruction.cs b/src/NerdBank.GitVersioning/ManagedGit/DeltaInstruction.cs index 149da05a..d26ec03d 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/DeltaInstruction.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/DeltaInstruction.cs @@ -1,27 +1,29 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Nerdbank.GitVersioning.ManagedGit +#nullable enable + +namespace Nerdbank.GitVersioning.ManagedGit; + +/// +/// Represents an instruction in a deltified stream. +/// +/// +public struct DeltaInstruction { /// - /// Represents an instruction in a deltified stream. + /// Gets or sets the type of the current instruction. /// - /// - public struct DeltaInstruction - { - /// - /// Gets or sets the type of the current instruction. - /// - public DeltaInstructionType InstructionType; + public DeltaInstructionType InstructionType; - /// - /// If the is , - /// the offset of the base stream to start copying from. - /// - public int Offset; + /// + /// If the is , + /// the offset of the base stream to start copying from. + /// + public int Offset; - /// - /// The number of bytes to copy or insert. - /// - public int Size; - } + /// + /// The number of bytes to copy or insert. + /// + public int Size; } diff --git a/src/NerdBank.GitVersioning/ManagedGit/DeltaInstructionType.cs b/src/NerdBank.GitVersioning/ManagedGit/DeltaInstructionType.cs index 8373bf82..efabe4d1 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/DeltaInstructionType.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/DeltaInstructionType.cs @@ -1,21 +1,23 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Nerdbank.GitVersioning.ManagedGit +#nullable enable + +namespace Nerdbank.GitVersioning.ManagedGit; + +/// +/// Enumerates the various instruction types which can be found in a deltafied stream. +/// +/// +public enum DeltaInstructionType { /// - /// Enumerates the various instruction types which can be found in a deltafied stream. + /// Instructs the caller to insert a new byte range into the object. /// - /// - public enum DeltaInstructionType - { - /// - /// Instructs the caller to insert a new byte range into the object. - /// - Insert = 0, + Insert = 0, - /// - /// Instructs the caller to copy a byte range from the source object. - /// - Copy = 1, - } + /// + /// Instructs the caller to copy a byte range from the source object. + /// + Copy = 1, } diff --git a/src/NerdBank.GitVersioning/ManagedGit/DeltaStreamReader.cs b/src/NerdBank.GitVersioning/ManagedGit/DeltaStreamReader.cs index a23020a9..2c06ae78 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/DeltaStreamReader.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/DeltaStreamReader.cs @@ -1,179 +1,178 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; -using System.IO; +#nullable enable -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +/// +/// Reads delta instructions from a . +/// +/// +public static class DeltaStreamReader { /// - /// Reads delta instructions from a . + /// Reads the next instruction from a . /// - /// - public static class DeltaStreamReader + /// + /// The stream from which to read the instruction. + /// + /// + /// The next instruction if found; otherwise, . + /// + public static DeltaInstruction? Read(Stream stream) { - /// - /// Reads the next instruction from a . - /// - /// - /// The stream from which to read the instruction. - /// - /// - /// The next instruction if found; otherwise, . - /// - public static DeltaInstruction? Read(Stream stream) + int next = stream.ReadByte(); + + if (next == -1) { - int next = stream.ReadByte(); + return null; + } + + byte instruction = (byte)next; + + DeltaInstruction value; + value.Offset = 0; + value.Size = 0; + + value.InstructionType = (DeltaInstructionType)((instruction & 0b1000_0000) >> 7); - if (next == -1) + if (value.InstructionType == DeltaInstructionType.Insert) + { + value.Size = instruction & 0b0111_1111; + } + else if (value.InstructionType == DeltaInstructionType.Copy) + { + // offset1 + if ((instruction & 0b0000_0001) != 0) { - return null; + value.Offset |= (byte)stream.ReadByte(); } - byte instruction = (byte)next; + // offset2 + if ((instruction & 0b0000_0010) != 0) + { + value.Offset |= (byte)stream.ReadByte() << 8; + } - DeltaInstruction value; - value.Offset = 0; - value.Size = 0; + // offset3 + if ((instruction & 0b0000_0100) != 0) + { + value.Offset |= (byte)stream.ReadByte() << 16; + } - value.InstructionType = (DeltaInstructionType)((instruction & 0b1000_0000) >> 7); + // offset4 + if ((instruction & 0b0000_1000) != 0) + { + value.Offset |= (byte)stream.ReadByte() << 24; + } - if (value.InstructionType == DeltaInstructionType.Insert) + // size1 + if ((instruction & 0b0001_0000) != 0) { - value.Size = instruction & 0b0111_1111; + value.Size = (byte)stream.ReadByte(); } - else if (value.InstructionType == DeltaInstructionType.Copy) + + // size2 + if ((instruction & 0b0010_0000) != 0) { - // offset1 - if ((instruction & 0b0000_0001) != 0) - { - value.Offset |= (byte)stream.ReadByte(); - } - - // offset2 - if ((instruction & 0b0000_0010) != 0) - { - value.Offset |= ((byte)stream.ReadByte() << 8); - } - - // offset3 - if ((instruction & 0b0000_0100) != 0) - { - value.Offset |= ((byte)stream.ReadByte() << 16); - } - - // offset4 - if ((instruction & 0b0000_1000) != 0) - { - value.Offset |= ((byte)stream.ReadByte() << 24); - } - - // size1 - if ((instruction & 0b0001_0000) != 0) - { - value.Size = (byte)stream.ReadByte(); - } - - // size2 - if ((instruction & 0b0010_0000) != 0) - { - value.Size |= ((byte)stream.ReadByte() << 8); - } - - // size3 - if ((instruction & 0b0100_0000) != 0) - { - value.Size |= ((byte)stream.ReadByte() << 16); - } - - // Size zero is automatically converted to 0x10000. - if (value.Size == 0) - { - value.Size = 0x10000; - } + value.Size |= (byte)stream.ReadByte() << 8; } - return value; + // size3 + if ((instruction & 0b0100_0000) != 0) + { + value.Size |= (byte)stream.ReadByte() << 16; + } + + // Size zero is automatically converted to 0x10000. + if (value.Size == 0) + { + value.Size = 0x10000; + } + } + + return value; + } + + /// + /// Reads the next instruction from a . + /// + /// + /// The stream from which to read the instruction. + /// + /// + /// The next instruction if found; otherwise, . + /// + public static DeltaInstruction? Read(ref ReadOnlyMemory stream) + { + if (stream.Length == 0) + { + return null; } - /// - /// Reads the next instruction from a . - /// - /// - /// The stream from which to read the instruction. - /// - /// - /// The next instruction if found; otherwise, . - /// - public static DeltaInstruction? Read(ref ReadOnlyMemory stream) + ReadOnlySpan span = stream.Span; + int i = 0; + int next = span[i++]; + + byte instruction = (byte)next; + + DeltaInstruction value; + value.Offset = 0; + value.Size = 0; + + value.InstructionType = (DeltaInstructionType)((instruction & 0b1000_0000) >> 7); + + if (value.InstructionType == DeltaInstructionType.Insert) + { + value.Size = instruction & 0b0111_1111; + } + else if (value.InstructionType == DeltaInstructionType.Copy) { - if (stream.Length == 0) + // offset1 + if ((instruction & 0b0000_0001) != 0) { - return null; + value.Offset |= span[i++]; } - var span = stream.Span; - int i = 0; - int next = span[i++]; - - byte instruction = (byte)next; + // offset2 + if ((instruction & 0b0000_0010) != 0) + { + value.Offset |= span[i++] << 8; + } - DeltaInstruction value; - value.Offset = 0; - value.Size = 0; + // offset3 + if ((instruction & 0b0000_0100) != 0) + { + value.Offset |= span[i++] << 16; + } - value.InstructionType = (DeltaInstructionType)((instruction & 0b1000_0000) >> 7); + // offset4 + if ((instruction & 0b0000_1000) != 0) + { + value.Offset |= span[i++] << 24; + } - if (value.InstructionType == DeltaInstructionType.Insert) + // size1 + if ((instruction & 0b0001_0000) != 0) { - value.Size = instruction & 0b0111_1111; + value.Size = span[i++]; } - else if (value.InstructionType == DeltaInstructionType.Copy) + + // size2 + if ((instruction & 0b0010_0000) != 0) { - // offset1 - if ((instruction & 0b0000_0001) != 0) - { - value.Offset |= span[i++]; - } - - // offset2 - if ((instruction & 0b0000_0010) != 0) - { - value.Offset |= (span[i++] << 8); - } - - // offset3 - if ((instruction & 0b0000_0100) != 0) - { - value.Offset |= (span[i++] << 16); - } - - // offset4 - if ((instruction & 0b0000_1000) != 0) - { - value.Offset |= (span[i++] << 24); - } - - // size1 - if ((instruction & 0b0001_0000) != 0) - { - value.Size = span[i++]; - } - - // size2 - if ((instruction & 0b0010_0000) != 0) - { - value.Size |= (span[i++] << 8); - } - - // size3 - if ((instruction & 0b0100_0000) != 0) - { - value.Size |= (span[i++] << 16); - } + value.Size |= span[i++] << 8; } - stream = stream.Slice(i); - return value; + // size3 + if ((instruction & 0b0100_0000) != 0) + { + value.Size |= span[i++] << 16; + } } + + stream = stream.Slice(i); + return value; } } diff --git a/src/NerdBank.GitVersioning/ManagedGit/FileHelpers.cs b/src/NerdBank.GitVersioning/ManagedGit/FileHelpers.cs index e1d9713b..e370deba 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/FileHelpers.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/FileHelpers.cs @@ -1,108 +1,116 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable -using System; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; using Windows.Win32; +using Windows.Win32.Foundation; using Windows.Win32.Storage.FileSystem; -using Windows.Win32.System.SystemServices; -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +internal static class FileHelpers { - internal static class FileHelpers - { - private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - /// - /// Opens the file with a given path, if it exists. - /// - /// The path to the file. - /// The stream to open to, if the file exists. - /// if the file exists; otherwise . - internal static bool TryOpen(string path, out FileStream? stream) + /// + /// Opens the file with a given path, if it exists. + /// + /// The path to the file. + /// The stream to open to, if the file exists. + /// if the file exists; otherwise . + internal static bool TryOpen(string path, out FileStream? stream) + { +#if NET5_0_OR_GREATER + if (OperatingSystem.IsWindowsVersionAtLeast(5, 1, 2600)) +#else + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) +#endif { - if (IsWindows) - { - var handle = PInvoke.CreateFile(path, FILE_ACCESS_FLAGS.FILE_GENERIC_READ, FILE_SHARE_MODE.FILE_SHARE_READ, lpSecurityAttributes: null, FILE_CREATION_DISPOSITION.OPEN_EXISTING, FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL, null); + SafeFileHandle? handle = PInvoke.CreateFile(path, (uint)FILE_ACCESS_RIGHTS.FILE_GENERIC_READ, FILE_SHARE_MODE.FILE_SHARE_READ, lpSecurityAttributes: null, FILE_CREATION_DISPOSITION.OPEN_EXISTING, FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL, null); - if (!handle.IsInvalid) - { - var fileHandle = new SafeFileHandle(handle.DangerousGetHandle(), ownsHandle: true); - handle.SetHandleAsInvalid(); - stream = new FileStream(fileHandle, System.IO.FileAccess.Read); - return true; - } - else - { - stream = null; - return false; - } + if (!handle.IsInvalid) + { + var fileHandle = new SafeFileHandle(handle.DangerousGetHandle(), ownsHandle: true); + handle.SetHandleAsInvalid(); + stream = new FileStream(fileHandle, System.IO.FileAccess.Read); + return true; } else { - if (!File.Exists(path)) - { - stream = null; - return false; - } - - stream = File.OpenRead(path); - return true; + stream = null; + return false; } } + else + { + if (!File.Exists(path)) + { + stream = null; + return false; + } + + stream = File.OpenRead(path); + return true; + } + } - /// - /// Opens the file with a given path, if it exists. - /// - /// The path to the file, as a null-terminated UTF-16 character array. - /// The stream to open to, if the file exists. - /// if the file exists; otherwise . - internal static unsafe bool TryOpen(ReadOnlySpan path, [NotNullWhen(true)] out FileStream? stream) + /// + /// Opens the file with a given path, if it exists. + /// + /// The path to the file, as a null-terminated UTF-16 character array. + /// The stream to open to, if the file exists. + /// if the file exists; otherwise . + internal static unsafe bool TryOpen(ReadOnlySpan path, [NotNullWhen(true)] out FileStream? stream) + { +#if NET5_0_OR_GREATER + if (OperatingSystem.IsWindowsVersionAtLeast(5, 1, 2600)) +#else + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) +#endif { - if (IsWindows) + HANDLE handle; + fixed (char* pPath = &path[0]) { - HANDLE handle; - fixed (char* pPath = &path[0]) - { - handle = PInvoke.CreateFile(pPath, FILE_ACCESS_FLAGS.FILE_GENERIC_READ, FILE_SHARE_MODE.FILE_SHARE_READ, null, FILE_CREATION_DISPOSITION.OPEN_EXISTING, FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL, default); - } + handle = PInvoke.CreateFile(pPath, (uint)FILE_ACCESS_RIGHTS.FILE_GENERIC_READ, FILE_SHARE_MODE.FILE_SHARE_READ, null, FILE_CREATION_DISPOSITION.OPEN_EXISTING, FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL, default); + } - if (!handle.Equals(Constants.INVALID_HANDLE_VALUE)) - { - var fileHandle = new SafeFileHandle(handle, ownsHandle: true); - stream = new FileStream(fileHandle, System.IO.FileAccess.Read); - return true; - } - else - { - stream = null; - return false; - } + if (!handle.Equals(HANDLE.INVALID_HANDLE_VALUE)) + { + var fileHandle = new SafeFileHandle(handle, ownsHandle: true); + stream = new FileStream(fileHandle, System.IO.FileAccess.Read); + return true; } else { - // Make sure to trim the trailing \0 - string fullPath = GetUtf16String(path.Slice(0, path.Length - 1)); - - if (!File.Exists(fullPath)) - { - stream = null; - return false; - } - - stream = File.OpenRead(fullPath); - return true; + stream = null; + return false; } } - - private static unsafe string GetUtf16String(ReadOnlySpan chars) + else { - fixed (char* pChars = chars) + // Make sure to trim the trailing \0 + string fullPath = GetUtf16String(path.Slice(0, path.Length - 1)); + + if (!File.Exists(fullPath)) { - return new string(pChars, 0, chars.Length); + stream = null; + return false; } + + stream = File.OpenRead(fullPath); + return true; + } + } + + private static unsafe string GetUtf16String(ReadOnlySpan chars) + { + fixed (char* pChars = chars) + { + return new string(pChars, 0, chars.Length); } } } diff --git a/src/NerdBank.GitVersioning/ManagedGit/GitCommit.cs b/src/NerdBank.GitVersioning/ManagedGit/GitCommit.cs index 2e27acd5..2e01be1d 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/GitCommit.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/GitCommit.cs @@ -1,175 +1,173 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable -using System; using System.Collections; -using System.Collections.Generic; -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +/// +/// Represents a Git commit, as stored in the Git object database. +/// +public struct GitCommit : IEquatable { /// - /// Represents a Git commit, as stored in the Git object database. + /// Gets or sets the of the file tree which represents directory + /// structure of the repository at the time of the commit. /// - public struct GitCommit : IEquatable - { - /// - /// Gets or sets the of the file tree which represents directory - /// structure of the repository at the time of the commit. - /// - public GitObjectId Tree { get; set; } + public GitObjectId Tree { get; set; } - /// - /// Gets or sets a which uniquely identifies the . - /// - public GitObjectId Sha { get; set; } + /// + /// Gets or sets a which uniquely identifies the . + /// + public GitObjectId Sha { get; set; } - /// - /// Gets or sets the first parent of this commit. - /// - public GitObjectId? FirstParent { get; set; } + /// + /// Gets or sets the first parent of this commit. + /// + public GitObjectId? FirstParent { get; set; } - /// - /// Gets or sets the second parent of this commit. - /// - public GitObjectId? SecondParent { get; set; } + /// + /// Gets or sets the second parent of this commit. + /// + public GitObjectId? SecondParent { get; set; } - /// - /// Gets or sets additional parents (3rd parent and on) of this commit, if any. - /// - public List? AdditionalParents { get; set; } + /// + /// Gets or sets additional parents (3rd parent and on) of this commit, if any. + /// + public List? AdditionalParents { get; set; } - /// - /// Gets an enumerator for parents of this commit. - /// - public ParentEnumerable Parents => new ParentEnumerable(this); + /// + /// Gets an enumerator for parents of this commit. + /// + public ParentEnumerable Parents => new ParentEnumerable(this); - /// - /// Gets or sets the author of this commit. - /// - public GitSignature? Author { get; set; } + /// + /// Gets or sets the author of this commit. + /// + public GitSignature? Author { get; set; } - /// - public override bool Equals(object? obj) - { - if (obj is GitCommit) - { - return this.Equals((GitCommit)obj); - } + public static bool operator ==(GitCommit left, GitCommit right) + { + return Equals(left, right); + } - return false; - } + public static bool operator !=(GitCommit left, GitCommit right) + { + return !Equals(left, right); + } - /// - public bool Equals(GitCommit other) + /// + public override bool Equals(object? obj) + { + if (obj is GitCommit) { - return this.Sha.Equals(other.Sha); + return this.Equals((GitCommit)obj); } - /// - public static bool operator ==(GitCommit left, GitCommit right) - { - return Equals(left, right); - } + return false; + } - /// - public static bool operator !=(GitCommit left, GitCommit right) - { - return !Equals(left, right); - } + /// + public bool Equals(GitCommit other) + { + return this.Sha.Equals(other.Sha); + } - /// - public override int GetHashCode() - { - return this.Sha.GetHashCode(); - } + /// + public override int GetHashCode() + { + return this.Sha.GetHashCode(); + } - /// - public override string ToString() - { - return $"Git Commit: {this.Sha}"; - } + /// + public override string ToString() + { + return $"Git Commit: {this.Sha}"; + } + + /// + /// An enumerable for parents of a commit. + /// + public struct ParentEnumerable : IEnumerable + { + private readonly GitCommit owner; /// - /// An enumerable for parents of a commit. + /// Initializes a new instance of the struct. /// - public struct ParentEnumerable : IEnumerable + /// The commit whose parents are to be enumerated. + public ParentEnumerable(GitCommit owner) { - private readonly GitCommit owner; + this.owner = owner; + } - /// - /// Initializes an instance of the struct. - /// - /// The commit whose parents are to be enumerated. - public ParentEnumerable(GitCommit owner) - { - this.owner = owner; - } + /// + public IEnumerator GetEnumerator() => new ParentEnumerator(this.owner); - /// - public IEnumerator GetEnumerator() => new ParentEnumerator(this.owner); + /// + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + } - /// - IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - } + /// + /// An enumerator for a commit's parents. + /// + public struct ParentEnumerator : IEnumerator + { + private readonly GitCommit owner; + + private int position; /// - /// An enumerator for a commit's parents. + /// Initializes a new instance of the struct. /// - public struct ParentEnumerator : IEnumerator + /// The commit whose parents are to be enumerated. + public ParentEnumerator(GitCommit owner) { - private readonly GitCommit owner; - - private int position; - - /// - /// Initializes an instance of the struct. - /// - /// The commit whose parents are to be enumerated. - public ParentEnumerator(GitCommit owner) - { - this.owner = owner; - this.position = -1; - } + this.owner = owner; + this.position = -1; + } - /// - public GitObjectId Current + /// + public GitObjectId Current + { + get { - get + if (this.position < 0) { - if (this.position < 0) - { - throw new InvalidOperationException("Call MoveNext first."); - } - - return this.position switch - { - 0 => this.owner.FirstParent ?? throw new InvalidOperationException("No more elements."), - 1 => this.owner.SecondParent ?? throw new InvalidOperationException("No more elements."), - _ => this.owner.AdditionalParents?[this.position - 2] ?? throw new InvalidOperationException("No more elements."), - }; + throw new InvalidOperationException("Call MoveNext first."); } - } - - /// - object IEnumerator.Current => this.Current; - /// - public void Dispose() - { - } - - /// - public bool MoveNext() - { - return ++this.position switch + return this.position switch { - 0 => this.owner.FirstParent.HasValue, - 1 => this.owner.SecondParent.HasValue, - _ => this.owner.AdditionalParents?.Count > this.position - 2, + 0 => this.owner.FirstParent ?? throw new InvalidOperationException("No more elements."), + 1 => this.owner.SecondParent ?? throw new InvalidOperationException("No more elements."), + _ => this.owner.AdditionalParents?[this.position - 2] ?? throw new InvalidOperationException("No more elements."), }; } + } + + /// + object IEnumerator.Current => this.Current; + + /// + public void Dispose() + { + } - /// - public void Reset() => this.position = -1; + /// + public bool MoveNext() + { + return ++this.position switch + { + 0 => this.owner.FirstParent.HasValue, + 1 => this.owner.SecondParent.HasValue, + _ => this.owner.AdditionalParents?.Count > this.position - 2, + }; } + + /// + public void Reset() => this.position = -1; } } diff --git a/src/NerdBank.GitVersioning/ManagedGit/GitCommitReader.cs b/src/NerdBank.GitVersioning/ManagedGit/GitCommitReader.cs index 8af84e05..e4629e2a 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/GitCommitReader.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/GitCommitReader.cs @@ -1,186 +1,184 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable -using System; using System.Buffers; -using System.Collections.Generic; using System.Diagnostics; -using System.IO; -using System.Linq; -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +/// +/// Reads a object. +/// +public static class GitCommitReader { + private const int TreeLineLength = 46; + private const int ParentLineLength = 48; + + private static readonly byte[] TreeStart = GitRepository.Encoding.GetBytes("tree "); + private static readonly byte[] ParentStart = GitRepository.Encoding.GetBytes("parent "); + private static readonly byte[] AuthorStart = GitRepository.Encoding.GetBytes("author "); + /// - /// Reads a object. + /// Reads a object from a . /// - public static class GitCommitReader + /// + /// A which contains the in its text representation. + /// + /// + /// The of the commit. + /// + /// + /// A value indicating whether to populate the field. + /// + /// + /// The . + /// + public static GitCommit Read(Stream stream, GitObjectId sha, bool readAuthor = false) { - private static readonly byte[] TreeStart = GitRepository.Encoding.GetBytes("tree "); - private static readonly byte[] ParentStart = GitRepository.Encoding.GetBytes("parent "); - private static readonly byte[] AuthorStart = GitRepository.Encoding.GetBytes("author "); - - private const int TreeLineLength = 46; - private const int ParentLineLength = 48; - - /// - /// Reads a object from a . - /// - /// - /// A which contains the in its text representation. - /// - /// - /// The of the commit. - /// - /// - /// A value indicating whether to populate the field. - /// - /// - /// The . - /// - public static GitCommit Read(Stream stream, GitObjectId sha, bool readAuthor = false) + if (stream is null) { - if (stream is null) - { - throw new ArgumentNullException(nameof(stream)); - } + throw new ArgumentNullException(nameof(stream)); + } - byte[] buffer = ArrayPool.Shared.Rent((int)stream.Length); + byte[] buffer = ArrayPool.Shared.Rent((int)stream.Length); - try - { - Span span = buffer.AsSpan(0, (int)stream.Length); - stream.ReadAll(span); + try + { + Span span = buffer.AsSpan(0, (int)stream.Length); + stream.ReadAll(span); - return Read(span, sha, readAuthor); - } - finally - { - ArrayPool.Shared.Return(buffer); - } + return Read(span, sha, readAuthor); } - - /// - /// Reads a object from a . - /// - /// - /// A which contains the in its text representation. - /// - /// - /// The of the commit. - /// - /// - /// A value indicating whether to populate the field. - /// - /// - /// The . - /// - public static GitCommit Read(ReadOnlySpan commit, GitObjectId sha, bool readAuthor = false) + finally { - var buffer = commit; + ArrayPool.Shared.Return(buffer); + } + } - var tree = ReadTree(buffer.Slice(0, TreeLineLength)); + /// + /// Reads a object from a . + /// + /// + /// A which contains the in its text representation. + /// + /// + /// The of the commit. + /// + /// + /// A value indicating whether to populate the field. + /// + /// + /// The . + /// + public static GitCommit Read(ReadOnlySpan commit, GitObjectId sha, bool readAuthor = false) + { + ReadOnlySpan buffer = commit; - buffer = buffer.Slice(TreeLineLength); + GitObjectId tree = ReadTree(buffer.Slice(0, TreeLineLength)); - GitObjectId? firstParent = null, secondParent = null; - List? additionalParents = null; - List parents = new List(); - while (TryReadParent(buffer, out GitObjectId parent)) + buffer = buffer.Slice(TreeLineLength); + + GitObjectId? firstParent = null, secondParent = null; + List? additionalParents = null; + var parents = new List(); + while (TryReadParent(buffer, out GitObjectId parent)) + { + if (!firstParent.HasValue) { - if (!firstParent.HasValue) - { - firstParent = parent; - } - else if (!secondParent.HasValue) - { - secondParent = parent; - } - else - { - additionalParents ??= new List(); - additionalParents.Add(parent); - } - - buffer = buffer.Slice(ParentLineLength); + firstParent = parent; } - - GitSignature signature = default; - - if (readAuthor && !TryReadAuthor(buffer, out signature)) + else if (!secondParent.HasValue) { - throw new GitException(); + secondParent = parent; } - - return new GitCommit() + else { - Sha = sha, - FirstParent = firstParent, - SecondParent = secondParent, - AdditionalParents = additionalParents, - Tree = tree, - Author = readAuthor ? signature : (GitSignature?)null, - }; + additionalParents ??= new List(); + additionalParents.Add(parent); + } + + buffer = buffer.Slice(ParentLineLength); } - private static GitObjectId ReadTree(ReadOnlySpan line) + GitSignature signature = default; + + if (readAuthor && !TryReadAuthor(buffer, out signature)) { - // Format: tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579\n - // 47 bytes: - // tree: 5 bytes - // space: 1 byte - // hash: 40 bytes - // \n: 1 byte - Debug.Assert(line.Slice(0, TreeStart.Length).SequenceEqual(TreeStart)); - Debug.Assert(line[TreeLineLength - 1] == (byte)'\n'); - - return GitObjectId.ParseHex(line.Slice(TreeStart.Length, 40)); + throw new GitException(); } - private static bool TryReadParent(ReadOnlySpan line, out GitObjectId parent) + return new GitCommit() { - // Format: "parent ef079ebcca375f6fd54aa0cb9f35e3ecc2bb66e7\n" - parent = GitObjectId.Empty; + Sha = sha, + FirstParent = firstParent, + SecondParent = secondParent, + AdditionalParents = additionalParents, + Tree = tree, + Author = readAuthor ? signature : null, + }; + } - if (!line.Slice(0, ParentStart.Length).SequenceEqual(ParentStart)) - { - return false; - } + private static GitObjectId ReadTree(ReadOnlySpan line) + { + // Format: tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579\n + // 47 bytes: + // tree: 5 bytes + // space: 1 byte + // hash: 40 bytes + // \n: 1 byte + Debug.Assert(line.Slice(0, TreeStart.Length).SequenceEqual(TreeStart)); + Debug.Assert(line[TreeLineLength - 1] == (byte)'\n'); + + return GitObjectId.ParseHex(line.Slice(TreeStart.Length, 40)); + } - if (line[ParentLineLength - 1] != (byte)'\n') - { - return false; - } + private static bool TryReadParent(ReadOnlySpan line, out GitObjectId parent) + { + // Format: "parent ef079ebcca375f6fd54aa0cb9f35e3ecc2bb66e7\n" + parent = GitObjectId.Empty; - parent = GitObjectId.ParseHex(line.Slice(ParentStart.Length, 40)); - return true; + if (!line.Slice(0, ParentStart.Length).SequenceEqual(ParentStart)) + { + return false; } - private static bool TryReadAuthor(ReadOnlySpan line, out GitSignature signature) + if (line[ParentLineLength - 1] != (byte)'\n') { - signature = default; + return false; + } - if (!line.Slice(0, AuthorStart.Length).SequenceEqual(AuthorStart)) - { - return false; - } + parent = GitObjectId.ParseHex(line.Slice(ParentStart.Length, 40)); + return true; + } - line = line.Slice(AuthorStart.Length); + private static bool TryReadAuthor(ReadOnlySpan line, out GitSignature signature) + { + signature = default; - int emailStart = line.IndexOf((byte)'<'); - int emailEnd = line.IndexOf((byte)'>'); - var lineEnd = line.IndexOf((byte)'\n'); + if (!line.Slice(0, AuthorStart.Length).SequenceEqual(AuthorStart)) + { + return false; + } - var name = line.Slice(0, emailStart - 1); - var email = line.Slice(emailStart + 1, emailEnd - emailStart - 1); - var time = line.Slice(emailEnd + 2, lineEnd - emailEnd - 2); + line = line.Slice(AuthorStart.Length); - signature.Name = GitRepository.GetString(name); - signature.Email = GitRepository.GetString(email); + int emailStart = line.IndexOf((byte)'<'); + int emailEnd = line.IndexOf((byte)'>'); + int lineEnd = line.IndexOf((byte)'\n'); - var offsetStart = time.IndexOf((byte)' '); - var ticks = long.Parse(GitRepository.GetString(time.Slice(0, offsetStart))); - signature.Date = DateTimeOffset.FromUnixTimeSeconds(ticks); + ReadOnlySpan name = line.Slice(0, emailStart - 1); + ReadOnlySpan email = line.Slice(emailStart + 1, emailEnd - emailStart - 1); + ReadOnlySpan time = line.Slice(emailEnd + 2, lineEnd - emailEnd - 2); - return true; - } + signature.Name = GitRepository.GetString(name); + signature.Email = GitRepository.GetString(email); + + int offsetStart = time.IndexOf((byte)' '); + long ticks = long.Parse(GitRepository.GetString(time.Slice(0, offsetStart))); + signature.Date = DateTimeOffset.FromUnixTimeSeconds(ticks); + + return true; } } diff --git a/src/NerdBank.GitVersioning/ManagedGit/GitObjectId.cs b/src/NerdBank.GitVersioning/ManagedGit/GitObjectId.cs index 3e62f158..9dc03ef0 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/GitObjectId.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/GitObjectId.cs @@ -1,246 +1,245 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable -using System; using System.Buffers.Binary; using System.Diagnostics; using System.Runtime.InteropServices; -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +/// +/// A identifies an object stored in the Git repository. The +/// of an object is the SHA-1 hash of the contents of that +/// object. +/// +/// . +public unsafe struct GitObjectId : IEquatable { + private const string HexDigits = "0123456789abcdef"; + private const int NativeSize = 20; + private static readonly byte[] HexBytes = new byte[] { (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f' }; + private static readonly byte[] ReverseHexDigits = BuildReverseHexDigits(); + private fixed byte value[NativeSize]; + private string? sha; + /// - /// A identifies an object stored in the Git repository. The - /// of an object is the SHA-1 hash of the contents of that - /// object. + /// Gets a which represents an empty . /// - /// . - public unsafe struct GitObjectId : IEquatable + public static GitObjectId Empty => default(GitObjectId); + + /// + /// Gets the 20 byte ID of this object as a span from the field. + /// + private Span Value { - private const string hexDigits = "0123456789abcdef"; - private readonly static byte[] hexBytes = new byte[] { (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f' }; - private const int NativeSize = 20; - private fixed byte value[NativeSize]; - private string? sha; - - /// - /// Gets the 20 byte ID of this object as a span from the field. - /// - private Span Value + get { - get + fixed (byte* value = this.value) { - fixed (byte* value = this.value) - { - return new Span(value, NativeSize); - } + return new Span(value, NativeSize); } } + } - private static readonly byte[] ReverseHexDigits = BuildReverseHexDigits(); - - /// - /// Gets a which represents an empty . - /// - public static GitObjectId Empty => default(GitObjectId); - - /// - /// Parses a which contains the - /// as a sequence of byte values. - /// - /// - /// The as a sequence of byte values. Must be exactly 20 bytes in length. - /// - /// - /// A . - /// - public static GitObjectId Parse(ReadOnlySpan value) - { - Debug.Assert(value.Length == 20); + public static bool operator ==(GitObjectId left, GitObjectId right) => Equals(left, right); - GitObjectId objectId = new GitObjectId(); - value.CopyTo(objectId.Value); - return objectId; - } + public static bool operator !=(GitObjectId left, GitObjectId right) => !Equals(left, right); - /// - /// Parses a which contains the hexadecimal representation of a - /// . - /// - /// - /// A which contains the hexadecimal representation of the - /// . - /// - /// - /// A . - /// - public static GitObjectId Parse(string value) - { - Debug.Assert(value.Length == 40); + /// + /// Parses a which contains the + /// as a sequence of byte values. + /// + /// + /// The as a sequence of byte values. Must be exactly 20 bytes in length. + /// + /// + /// A . + /// + public static GitObjectId Parse(ReadOnlySpan value) + { + Debug.Assert(value.Length == 20); - GitObjectId objectId = new GitObjectId(); - Span bytes = objectId.Value; + var objectId = default(GitObjectId); + value.CopyTo(objectId.Value); + return objectId; + } - for (int i = 0; i < value.Length; i++) - { - int c1 = ReverseHexDigits[value[i++] - '0'] << 4; - int c2 = ReverseHexDigits[value[i] - '0']; + /// + /// Parses a which contains the hexadecimal representation of a + /// . + /// + /// + /// A which contains the hexadecimal representation of the + /// . + /// + /// + /// A . + /// + public static GitObjectId Parse(string value) + { + Debug.Assert(value.Length == 40); - bytes[i >> 1] = (byte)(c1 + c2); - } + var objectId = default(GitObjectId); + Span bytes = objectId.Value; + + for (int i = 0; i < value.Length; i++) + { + int c1 = ReverseHexDigits[value[i++] - '0'] << 4; + int c2 = ReverseHexDigits[value[i] - '0']; - objectId.sha = value.ToLower(); - return objectId; + bytes[i >> 1] = (byte)(c1 + c2); } - /// - /// Parses a which contains the hexadecimal representation of a - /// . - /// - /// - /// A which contains the hexadecimal representation of the - /// encoded in ASCII. - /// - /// - /// A . - /// - public static GitObjectId ParseHex(ReadOnlySpan value) - { - Debug.Assert(value.Length == 40); + objectId.sha = value.ToLower(); + return objectId; + } - GitObjectId objectId = new GitObjectId(); - Span bytes = objectId.Value; + /// + /// Parses a which contains the hexadecimal representation of a + /// . + /// + /// + /// A which contains the hexadecimal representation of the + /// encoded in ASCII. + /// + /// + /// A . + /// + public static GitObjectId ParseHex(ReadOnlySpan value) + { + Debug.Assert(value.Length == 40); - for (int i = 0; i < value.Length; i++) - { - int c1 = ReverseHexDigits[value[i++] - '0'] << 4; - int c2 = ReverseHexDigits[value[i] - '0']; + var objectId = default(GitObjectId); + Span bytes = objectId.Value; - bytes[i >> 1] = (byte)(c1 + c2); - } + for (int i = 0; i < value.Length; i++) + { + int c1 = ReverseHexDigits[value[i++] - '0'] << 4; + int c2 = ReverseHexDigits[value[i] - '0']; - return objectId; + bytes[i >> 1] = (byte)(c1 + c2); } - private static byte[] BuildReverseHexDigits() + return objectId; + } + + /// + public override bool Equals(object? obj) + { + if (obj is GitObjectId) { - var bytes = new byte['f' - '0' + 1]; + return this.Equals((GitObjectId)obj); + } - for (int i = 0; i < 10; i++) - { - bytes[i] = (byte)i; - } + return false; + } - for (int i = 10; i < 16; i++) - { - bytes[i + 'a' - '0' - 0x0a] = (byte)i; - bytes[i + 'A' - '0' - 0x0a] = (byte)i; - } + /// + public bool Equals(GitObjectId other) => this.Value.SequenceEqual(other.Value); - return bytes; - } + /// + public override int GetHashCode() => BinaryPrimitives.ReadInt32LittleEndian(this.Value.Slice(0, 4)); - /// - public override bool Equals(object? obj) - { - if (obj is GitObjectId) - { - return this.Equals((GitObjectId)obj); - } + /// + /// Gets a which represents the first two bytes of this . + /// + /// + /// A which represents the first two bytes of this . + /// + public ushort AsUInt16() => BinaryPrimitives.ReadUInt16BigEndian(this.Value.Slice(0, 2)); - return false; + /// + /// Returns the SHA1 hash of this object. + /// + /// + public override string ToString() + { + if (this.sha is null) + { + this.sha = this.CreateString(0, 20); } - /// - public bool Equals(GitObjectId other) => this.Value.SequenceEqual(other.Value); + return this.sha; + } - /// - public static bool operator ==(GitObjectId left, GitObjectId right) => Equals(left, right); + /// + /// Encodes a portion of this as hex. + /// + /// + /// The index of the first byte of this to start copying. + /// + /// + /// The number of bytes of this to copy. + /// + /// The buffer that receives the hex characters. It must be at least twice as long as . + /// + /// This method is used to populate file paths as byte* objects which are passed to UTF-16-based + /// Windows APIs. + /// + public void CopyAsHex(int start, int length, Span chars) + { + Span bytes = MemoryMarshal.Cast(chars); - /// - public static bool operator !=(GitObjectId left, GitObjectId right) => !Equals(left, right); + // Inspired by http://stackoverflow.com/questions/623104/c-byte-to-hex-string/3974535#3974535 + int lengthInNibbles = length * 2; + + for (int i = 0; i < (lengthInNibbles & -2); i++) + { + int index0 = +i >> 1; + byte b = (byte)(this.value[start + index0] >> 4); + bytes[(2 * i) + 1] = 0; + bytes[2 * i++] = HexBytes[b]; + + b = (byte)(this.value[start + index0] & 0x0F); + bytes[(2 * i) + 1] = 0; + bytes[2 * i] = HexBytes[b]; + } + } - /// - public override int GetHashCode() => BinaryPrimitives.ReadInt32LittleEndian(this.Value.Slice(0, 4)); + /// + /// Copies the byte representation of this to a . + /// + /// + /// The memory to which to copy this . + /// + public void CopyTo(Span value) => this.Value.CopyTo(value); - /// - /// Gets a which represents the first two bytes of this . - /// - /// - /// A which represents the first two bytes of this . - /// - public ushort AsUInt16() => BinaryPrimitives.ReadUInt16BigEndian(this.Value.Slice(0, 2)); + private static byte[] BuildReverseHexDigits() + { + byte[]? bytes = new byte['f' - '0' + 1]; - /// - /// Returns the SHA1 hash of this object. - /// - public override string ToString() + for (int i = 0; i < 10; i++) { - if (this.sha is null) - { - this.sha = this.CreateString(0, 20); - } - - return this.sha; + bytes[i] = (byte)i; } - private string CreateString(int start, int length) + for (int i = 10; i < 16; i++) { - // Inspired byte http://stackoverflow.com/questions/623104/c-byte-to-hex-string/3974535#3974535 - int lengthInNibbles = length * 2; - var c = new char[lengthInNibbles]; - - for (int i = 0; i < (lengthInNibbles & -2); i++) - { - int index0 = +i >> 1; - var b = ((byte)(this.value[start + index0] >> 4)); - c[i++] = hexDigits[b]; + bytes[i + 'a' - '0' - 0x0a] = (byte)i; + bytes[i + 'A' - '0' - 0x0a] = (byte)i; + } - b = ((byte)(this.value[start + index0] & 0x0F)); - c[i] = hexDigits[b]; - } + return bytes; + } - return new string(c); - } + private string CreateString(int start, int length) + { + // Inspired byte http://stackoverflow.com/questions/623104/c-byte-to-hex-string/3974535#3974535 + int lengthInNibbles = length * 2; + char[]? c = new char[lengthInNibbles]; - /// - /// Encodes a portion of this as hex. - /// - /// - /// The index of the first byte of this to start copying. - /// - /// - /// The number of bytes of this to copy. - /// - /// The buffer that receives the hex characters. It must be at least twice as long as . - /// - /// This method is used to populate file paths as byte* objects which are passed to UTF-16-based - /// Windows APIs. - /// - public void CopyAsHex(int start, int length, Span chars) + for (int i = 0; i < (lengthInNibbles & -2); i++) { - Span bytes = MemoryMarshal.Cast(chars); - - // Inspired by http://stackoverflow.com/questions/623104/c-byte-to-hex-string/3974535#3974535 - int lengthInNibbles = length * 2; + int index0 = +i >> 1; + byte b = (byte)(this.value[start + index0] >> 4); + c[i++] = HexDigits[b]; - for (int i = 0; i < (lengthInNibbles & -2); i++) - { - int index0 = +i >> 1; - var b = ((byte)(this.value[start + index0] >> 4)); - bytes[2 * i + 1] = 0; - bytes[2 * i++] = hexBytes[b]; - - b = ((byte)(this.value[start + index0] & 0x0F)); - bytes[2 * i + 1] = 0; - bytes[2 * i] = hexBytes[b]; - } + b = (byte)(this.value[start + index0] & 0x0F); + c[i] = HexDigits[b]; } - /// - /// Copies the byte representation of this to a . - /// - /// - /// The memory to which to copy this . - /// - public void CopyTo(Span value) => this.Value.CopyTo(value); + return new string(c); } } diff --git a/src/NerdBank.GitVersioning/ManagedGit/GitObjectStream.cs b/src/NerdBank.GitVersioning/ManagedGit/GitObjectStream.cs index da8c5201..7b4749d0 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/GitObjectStream.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/GitObjectStream.cs @@ -1,74 +1,73 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; -using System.IO; +#nullable enable -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +/// +/// A which reads data stored in the Git object store. The data is stored +/// as a gz-compressed stream, and is prefixed with the object type and data length. +/// +public class GitObjectStream : ZLibStream { /// - /// A which reads data stored in the Git object store. The data is stored - /// as a gz-compressed stream, and is prefixed with the object type and data length. + /// Initializes a new instance of the class. /// - public class GitObjectStream : ZLibStream - { - /// - /// Initializes a new instance of the class. - /// - /// - /// The from which to read data. - /// - /// - /// The expected object type of the git object. - /// + /// + /// The from which to read data. + /// + /// + /// The expected object type of the git object. + /// #pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. ObjectType is assigned in ReadObjectTypeAndLength. - public GitObjectStream(Stream stream, string objectType) + public GitObjectStream(Stream stream, string objectType) #pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. - : base(stream, -1) - { - this.ReadObjectTypeAndLength(objectType); - } - - /// - /// Gets the object type of this Git object. - /// - public string ObjectType { get; private set; } - - /// - public override bool CanRead => true; + : base(stream, -1) + { + this.ReadObjectTypeAndLength(objectType); + } - /// - public override bool CanSeek => true; + /// + /// Gets the object type of this Git object. + /// + public string ObjectType { get; private set; } - /// - public override bool CanWrite => false; + /// + public override bool CanRead => true; - private void ReadObjectTypeAndLength(string objectType) - { - Span buffer = stackalloc byte[128]; - this.Read(buffer.Slice(0, objectType.Length + 1)); + /// + public override bool CanSeek => true; - var actualObjectType = GitRepository.GetString(buffer.Slice(0, objectType.Length)); - this.ObjectType = actualObjectType; + /// + public override bool CanWrite => false; - int headerLength = 0; - long length = 0; + private void ReadObjectTypeAndLength(string objectType) + { + Span buffer = stackalloc byte[128]; + this.Read(buffer.Slice(0, objectType.Length + 1)); - while (headerLength < buffer.Length) - { - this.Read(buffer.Slice(headerLength, 1)); + string? actualObjectType = GitRepository.GetString(buffer.Slice(0, objectType.Length)); + this.ObjectType = actualObjectType; - if (buffer[headerLength] == 0) - { - break; - } + int headerLength = 0; + long length = 0; - // Direct conversion from ASCII to int - length = (10 * length) + (buffer[headerLength] - (byte)'0'); + while (headerLength < buffer.Length) + { + this.Read(buffer.Slice(headerLength, 1)); - headerLength += 1; + if (buffer[headerLength] == 0) + { + break; } - this.Initialize(length); + // Direct conversion from ASCII to int + length = (10 * length) + (buffer[headerLength] - (byte)'0'); + + headerLength += 1; } + + this.Initialize(length); } } diff --git a/src/NerdBank.GitVersioning/ManagedGit/GitPack.cs b/src/NerdBank.GitVersioning/ManagedGit/GitPack.cs index 995659e6..8b093428 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/GitPack.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/GitPack.cs @@ -1,299 +1,297 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable -using System; -using System.Collections.Generic; -using System.IO; using System.IO.MemoryMappedFiles; -using System.Linq; using System.Text; -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +/// +/// Supports retrieving objects from a Git pack file. +/// +public class GitPack : IDisposable { + private readonly Func packStream; + private readonly Lazy indexStream; + private readonly GitPackCache cache; + private readonly MemoryMappedFile? packFile = null; + private readonly MemoryMappedViewAccessor? accessor = null; + + // Maps GitObjectIds to offets in the git pack. + private readonly Dictionary offsets = new Dictionary(); + + // A histogram which tracks the objects which have been retrieved from this GitPack. The key is the offset + // of the object. Used to get some insights in usage patterns. +#if DEBUG + private readonly Dictionary histogram = new Dictionary(); +#endif + + private readonly Lazy indexReader; + + // Operating on git packfiles can potentially open a lot of streams which point to the pack file. For example, + // deltafied objects can have base objects which are in turn delafied. Opening and closing these streams has + // become a performance bottleneck. This is mitigated by pooling streams (i.e. reusing the streams after they + // are closed by the caller). + private readonly Queue pooledStreams = new Queue(); + /// - /// Supports retrieving objects from a Git pack file. + /// Initializes a new instance of the class. /// - public class GitPack : IDisposable + /// + /// A delegate which fetches objects from the Git object store. + /// + /// + /// The full path to the index file. + /// + /// + /// The full path to the pack file. + /// + /// + /// A which is used to cache objects which operate + /// on the pack file. + /// + public GitPack(GetObjectFromRepositoryDelegate getObjectFromRepositoryDelegate, string indexPath, string packPath, GitPackCache? cache = null) + : this(getObjectFromRepositoryDelegate, new Lazy(() => File.OpenRead(indexPath)), () => File.OpenRead(packPath), cache) { - /// - /// A delegate for methods which fetch objects from the Git object store. - /// - /// - /// The Git object ID of the object to fetch. - /// - /// - /// The object type of the object to fetch. - /// - /// - /// A which represents the requested object. - /// - public delegate Stream? GetObjectFromRepositoryDelegate(GitObjectId sha, string objectType); - - private readonly Func packStream; - private readonly Lazy indexStream; - private readonly GitPackCache cache; - private MemoryMappedFile? packFile = null; - private MemoryMappedViewAccessor? accessor = null; - - // Maps GitObjectIds to offets in the git pack. - private readonly Dictionary offsets = new Dictionary(); - - // A histogram which tracks the objects which have been retrieved from this GitPack. The key is the offset - // of the object. Used to get some insights in usage patterns. -#if DEBUG - private readonly Dictionary histogram = new Dictionary(); -#endif + } - private Lazy indexReader; - - // Operating on git packfiles can potentially open a lot of streams which point to the pack file. For example, - // deltafied objects can have base objects which are in turn delafied. Opening and closing these streams has - // become a performance bottleneck. This is mitigated by pooling streams (i.e. reusing the streams after they - // are closed by the caller). - private readonly Queue pooledStreams = new Queue(); - - /// - /// Initializes a new instance of the class. - /// - /// - /// A delegate which fetches objects from the Git object store. - /// - /// - /// The full path to the index file. - /// - /// - /// The full path to the pack file. - /// - /// - /// A which is used to cache objects which operate - /// on the pack file. - /// - public GitPack(GetObjectFromRepositoryDelegate getObjectFromRepositoryDelegate, string indexPath, string packPath, GitPackCache? cache = null) - : this(getObjectFromRepositoryDelegate, new Lazy(() => File.OpenRead(indexPath)), () => File.OpenRead(packPath), cache) + /// + /// Initializes a new instance of the class. + /// + /// + /// A delegate which fetches objects from the Git object store. + /// + /// + /// A function which creates a new which provides read-only + /// access to the index file. + /// + /// + /// A function which creates a new which provides read-only + /// access to the pack file. + /// + /// + /// A which is used to cache objects which operate + /// on the pack file. + /// + public GitPack(GetObjectFromRepositoryDelegate getObjectFromRepositoryDelegate, Lazy indexStream, Func packStream, GitPackCache? cache = null) + { + this.GetObjectFromRepository = getObjectFromRepositoryDelegate ?? throw new ArgumentNullException(nameof(getObjectFromRepositoryDelegate)); + this.indexReader = new Lazy(this.OpenIndex); + this.packStream = packStream ?? throw new ArgumentException(nameof(packStream)); + this.indexStream = indexStream ?? throw new ArgumentNullException(nameof(indexStream)); + this.cache = cache ?? new GitPackMemoryCache(); + + if (IntPtr.Size > 4) { + this.packFile = MemoryMappedFile.CreateFromFile(this.packStream(), mapName: null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, leaveOpen: false); + this.accessor = this.packFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read); } + } - /// - /// Initializes a new instance of the class. - /// - /// - /// A delegate which fetches objects from the Git object store. - /// - /// - /// A function which creates a new which provides read-only - /// access to the index file. - /// - /// - /// A function which creates a new which provides read-only - /// access to the pack file. - /// - /// - /// A which is used to cache objects which operate - /// on the pack file. - /// - public GitPack(GetObjectFromRepositoryDelegate getObjectFromRepositoryDelegate, Lazy indexStream, Func packStream, GitPackCache? cache = null) + /// + /// A delegate for methods which fetch objects from the Git object store. + /// + /// + /// The Git object ID of the object to fetch. + /// + /// + /// The object type of the object to fetch. + /// + /// + /// A which represents the requested object. + /// + public delegate Stream? GetObjectFromRepositoryDelegate(GitObjectId sha, string objectType); + + /// + /// Gets a delegate which fetches objects from the Git object store. + /// + public GetObjectFromRepositoryDelegate GetObjectFromRepository { get; private set; } + + /// + /// Finds a git object using a partial object ID. + /// + /// + /// A partial object ID. + /// + /// + /// + /// If found, a full object ID which matches the partial object ID. + /// Otherwise, . + /// + public GitObjectId? Lookup(Span objectId, bool endsWithHalfByte = false) + { + (long? _, GitObjectId? actualObjectId) = this.indexReader.Value.GetOffset(objectId, endsWithHalfByte); + return actualObjectId; + } + + /// + /// Attempts to retrieve a Git object from this Git pack. + /// + /// + /// The Git object Id of the object to retrieve. + /// + /// + /// The object type of the object to retrieve. + /// + /// + /// If found, receives a which represents the object. + /// + /// + /// if the object was found; otherwise, . + /// + public bool TryGetObject(GitObjectId objectId, string objectType, out Stream? value) + { + long? offset = this.GetOffset(objectId); + + if (offset is null) { - this.GetObjectFromRepository = getObjectFromRepositoryDelegate ?? throw new ArgumentNullException(nameof(getObjectFromRepositoryDelegate)); - this.indexReader = new Lazy(this.OpenIndex); - this.packStream = packStream ?? throw new ArgumentException(nameof(packStream)); - this.indexStream = indexStream ?? throw new ArgumentNullException(nameof(indexStream)); - this.cache = cache ?? new GitPackMemoryCache(); - - if (IntPtr.Size > 4) - { - this.packFile = MemoryMappedFile.CreateFromFile(this.packStream(), mapName: null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, leaveOpen: false); - this.accessor = this.packFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read); - } + value = null; + return false; } + else + { + value = this.GetObject(offset.Value, objectType); + return true; + } + } - /// - /// Gets a delegate which fetches objects from the Git object store. - /// - public GetObjectFromRepositoryDelegate GetObjectFromRepository { get; private set; } - - /// - /// Finds a git object using a partial object ID. - /// - /// - /// A partial object ID. - /// - /// - /// - /// If found, a full object ID which matches the partial object ID. - /// Otherwise, . - /// - public GitObjectId? Lookup(Span objectId, bool endsWithHalfByte = false) + /// + /// Gets a Git object at a specific offset. + /// + /// + /// The offset of the Git object, relative to the pack file. + /// + /// + /// The object type of the object to retrieve. + /// + /// + /// A which represents the object. + /// + public Stream GetObject(long offset, string objectType) + { +#if DEBUG + if (!this.histogram.TryAdd(offset, 1)) { - (var _, var actualObjectId) = this.indexReader.Value.GetOffset(objectId, endsWithHalfByte); - return actualObjectId; + this.histogram[offset] += 1; } +#endif - /// - /// Attempts to retrieve a Git object from this Git pack. - /// - /// - /// The Git object Id of the object to retrieve. - /// - /// - /// The object type of the object to retrieve. - /// - /// - /// If found, receives a which represents the object. - /// - /// - /// if the object was found; otherwise, . - /// - public bool TryGetObject(GitObjectId objectId, string objectType, out Stream? value) + if (this.cache.TryOpen(offset, out Stream? stream)) { - var offset = this.GetOffset(objectId); - - if (offset is null) - { - value = null; - return false; - } - else - { - value = this.GetObject(offset.Value, objectType); - return true; - } + return stream!; } - /// - /// Gets a Git object at a specific offset. - /// - /// - /// The offset of the Git object, relative to the pack file. - /// - /// - /// The object type of the object to retrieve. - /// - /// - /// A which represents the object. - /// - public Stream GetObject(long offset, string objectType) + GitPackObjectType packObjectType; + + switch (objectType) { -#if DEBUG - if (!this.histogram.TryAdd(offset, 1)) - { - this.histogram[offset] += 1; - } -#endif + case "commit": + packObjectType = GitPackObjectType.OBJ_COMMIT; + break; + + case "tree": + packObjectType = GitPackObjectType.OBJ_TREE; + break; - if (this.cache.TryOpen(offset, out Stream? stream)) - { - return stream!; - } - - GitPackObjectType packObjectType; - - switch (objectType) - { - case "commit": - packObjectType = GitPackObjectType.OBJ_COMMIT; - break; - - case "tree": - packObjectType = GitPackObjectType.OBJ_TREE; - break; - - case "blob": - packObjectType = GitPackObjectType.OBJ_BLOB; - break; - - default: - throw new GitException($"The object type '{objectType}' is not supported by the {nameof(GitPack)} class."); - } - - var packStream = this.GetPackStream(); - Stream objectStream; - - try - { - objectStream = GitPackReader.GetObject(this, packStream, offset, objectType, packObjectType); - } - catch - { - packStream.Dispose(); - throw; - } - - return this.cache.Add(offset, objectStream); + case "blob": + packObjectType = GitPackObjectType.OBJ_BLOB; + break; + + default: + throw new GitException($"The object type '{objectType}' is not supported by the {nameof(GitPack)} class."); } - /// - /// Writes cache statistics to a . - /// - /// - /// A to which the cache statistics are written. - /// - public void GetCacheStatistics(StringBuilder builder) + Stream? packStream = this.GetPackStream(); + Stream objectStream; + + try { - builder.AppendLine($"Git Pack:"); + objectStream = GitPackReader.GetObject(this, packStream, offset, objectType, packObjectType); + } + catch + { + packStream.Dispose(); + throw; + } + + return this.cache.Add(offset, objectStream); + } + + /// + /// Writes cache statistics to a . + /// + /// + /// A to which the cache statistics are written. + /// + public void GetCacheStatistics(StringBuilder builder) + { + builder.AppendLine($"Git Pack:"); #if DEBUG - int histogramCount = 25; - builder.AppendLine($"Top {histogramCount} / {this.histogram.Count} items:"); + int histogramCount = 25; + builder.AppendLine($"Top {histogramCount} / {this.histogram.Count} items:"); - foreach (var item in this.histogram.OrderByDescending(v => v.Value).Take(25)) - { - builder.AppendLine($" {item.Key}: {item.Value}"); - } + foreach (KeyValuePair item in this.histogram.OrderByDescending(v => v.Value).Take(25)) + { + builder.AppendLine($" {item.Key}: {item.Value}"); + } - builder.AppendLine(); + builder.AppendLine(); #endif - this.cache.GetCacheStatistics(builder); - } + this.cache.GetCacheStatistics(builder); + } - /// - public void Dispose() + /// + public void Dispose() + { + if (this.indexReader.IsValueCreated) { - if (this.indexReader.IsValueCreated) - { - this.indexReader.Value.Dispose(); - } - - this.accessor?.Dispose(); - this.packFile?.Dispose(); - this.cache.Dispose(); + this.indexReader.Value.Dispose(); } - private long? GetOffset(GitObjectId objectId) - { - if (this.offsets.TryGetValue(objectId, out long cachedOffset)) - { - return cachedOffset; - } + this.accessor?.Dispose(); + this.packFile?.Dispose(); + this.cache.Dispose(); + } - var indexReader = this.indexReader.Value; - var offset = indexReader.GetOffset(objectId); + private long? GetOffset(GitObjectId objectId) + { + if (this.offsets.TryGetValue(objectId, out long cachedOffset)) + { + return cachedOffset; + } - if (offset is not null) - { - this.offsets.Add(objectId, offset.Value); - } + GitPackIndexReader? indexReader = this.indexReader.Value; + long? offset = indexReader.GetOffset(objectId); - return offset; + if (offset is not null) + { + this.offsets.Add(objectId, offset.Value); } - private Stream GetPackStream() + return offset; + } + + private Stream GetPackStream() + { + // On 64-bit processes, we can use Memory Mapped Streams (the address space + // will be large enough to map the entire packfile). On 32-bit processes, + // we directly access the underlying stream. + if (IntPtr.Size > 4) { - // On 64-bit processes, we can use Memory Mapped Streams (the address space - // will be large enough to map the entire packfile). On 32-bit processes, - // we directly access the underlying stream. - if (IntPtr.Size > 4) - { - return new MemoryMappedStream(this.accessor); - } - else - { - return this.packStream(); - } + return new MemoryMappedStream(this.accessor); } - - private GitPackIndexReader OpenIndex() + else { - return new GitPackIndexMappedReader(this.indexStream.Value); + return this.packStream(); } } + + private GitPackIndexReader OpenIndex() + { + return new GitPackIndexMappedReader(this.indexStream.Value); + } } diff --git a/src/NerdBank.GitVersioning/ManagedGit/GitPackCache.cs b/src/NerdBank.GitVersioning/ManagedGit/GitPackCache.cs index b817b357..50e054ab 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/GitPackCache.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/GitPackCache.cs @@ -1,71 +1,71 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable -using System; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Text; -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +/// +/// Represents a cache in which objects retrieved from a +/// are cached. Caching these objects can be of interest, because retrieving +/// data from a can be potentially expensive: the data is +/// compressed and can be deltified. +/// +public abstract class GitPackCache : IDisposable { /// - /// Represents a cache in which objects retrieved from a - /// are cached. Caching these objects can be of interest, because retrieving - /// data from a can be potentially expensive: the data is - /// compressed and can be deltified. + /// Attempts to retrieve a Git object from cache. /// - public abstract class GitPackCache : IDisposable - { - /// - /// Attempts to retrieve a Git object from cache. - /// - /// - /// The offset of the Git object in the Git pack. - /// - /// - /// A which will be set to the cached Git object. - /// - /// - /// if the object was found in cache; otherwise, - /// . - /// - public abstract bool TryOpen(long offset, [NotNullWhen(true)] out Stream? stream); + /// + /// The offset of the Git object in the Git pack. + /// + /// + /// A which will be set to the cached Git object. + /// + /// + /// if the object was found in cache; otherwise, + /// . + /// + public abstract bool TryOpen(long offset, [NotNullWhen(true)] out Stream? stream); - /// - /// Gets statistics about the cache usage. - /// - /// - /// A to which to write the statistics. - /// - public abstract void GetCacheStatistics(StringBuilder builder); + /// + /// Gets statistics about the cache usage. + /// + /// + /// A to which to write the statistics. + /// + public abstract void GetCacheStatistics(StringBuilder builder); - /// - /// Adds a Git object to this cache. - /// - /// - /// The offset of the Git object in the Git pack. - /// - /// - /// A which represents the object to add. This stream - /// will be copied to the cache. - /// - /// - /// A which represents the cached entry. - /// - public abstract Stream Add(long offset, Stream stream); + /// + /// Adds a Git object to this cache. + /// + /// + /// The offset of the Git object in the Git pack. + /// + /// + /// A which represents the object to add. This stream + /// will be copied to the cache. + /// + /// + /// A which represents the cached entry. + /// + public abstract Stream Add(long offset, Stream stream); - /// - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } - /// - /// Disposes of native and managed resources associated by this object. - /// - /// to dispose managed and native resources; to only dispose of native resources. - protected virtual void Dispose(bool disposing) - { - } + /// + /// Disposes of native and managed resources associated by this object. + /// + /// to dispose managed and native resources; to only dispose of native resources. + protected virtual void Dispose(bool disposing) + { } } diff --git a/src/NerdBank.GitVersioning/ManagedGit/GitPackDeltafiedStream.cs b/src/NerdBank.GitVersioning/ManagedGit/GitPackDeltafiedStream.cs index 3dae2a4d..45c9db13 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/GitPackDeltafiedStream.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/GitPackDeltafiedStream.cs @@ -1,203 +1,203 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable -using System; using System.Buffers; using System.Diagnostics; -using System.IO; -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +/// +/// Reads data from a deltafied object. +/// +/// +public class GitPackDeltafiedStream : Stream { + private readonly long length; + + private readonly Stream baseStream; + private readonly Stream deltaStream; + + private long position; + private DeltaInstruction? current; + private int offset; + /// - /// Reads data from a deltafied object. + /// Initializes a new instance of the class. /// - /// - public class GitPackDeltafiedStream : Stream + /// + /// The base stream to which the deltas are applied. + /// + /// + /// A which contains a sequence of s. + /// + public GitPackDeltafiedStream(Stream baseStream, Stream deltaStream) { - private readonly long length; - private long position; - - private readonly Stream baseStream; - private readonly Stream deltaStream; - - private DeltaInstruction? current; - private int offset; - - /// - /// Initializes a new instance of the class. - /// - /// - /// The base stream to which the deltas are applied. - /// - /// - /// A which contains a sequence of s. - /// - public GitPackDeltafiedStream(Stream baseStream, Stream deltaStream) - { - this.baseStream = baseStream ?? throw new ArgumentNullException(nameof(baseStream)); - this.deltaStream = deltaStream ?? throw new ArgumentNullException(nameof(deltaStream)); + this.baseStream = baseStream ?? throw new ArgumentNullException(nameof(baseStream)); + this.deltaStream = deltaStream ?? throw new ArgumentNullException(nameof(deltaStream)); - int baseObjectlength = deltaStream.ReadMbsInt(); - this.length = deltaStream.ReadMbsInt(); - } + int baseObjectlength = deltaStream.ReadMbsInt(); + this.length = deltaStream.ReadMbsInt(); + } - /// - /// Gets the base stream to which the deltas are applied. - /// - public Stream BaseStream => this.baseStream; + /// + /// Gets the base stream to which the deltas are applied. + /// + public Stream BaseStream => this.baseStream; - /// - public override bool CanRead => true; + /// + public override bool CanRead => true; - /// - public override bool CanSeek => false; + /// + public override bool CanSeek => false; - /// - public override bool CanWrite => false; + /// + public override bool CanWrite => false; - /// - public override long Length => this.length; + /// + public override long Length => this.length; - /// - public override long Position - { - get => this.position; - set => throw new NotImplementedException(); - } + /// + public override long Position + { + get => this.position; + set => throw new NotImplementedException(); + } #if NETSTANDARD2_0 - /// - /// Reads a sequence of bytes from the current and advances the position - /// within the stream by the number of bytes read. - /// - /// - /// A region of memory. When this method returns, the contents of this region are replaced by the bytes - /// read from the current source. - /// - /// - /// The total number of bytes read into the buffer. This can be less than the number of bytes allocated - /// in the buffer if that many bytes are not currently available, or zero (0) if the end of the stream - /// has been reached. - /// - public int Read(Span span) + /// + /// Reads a sequence of bytes from the current and advances the position + /// within the stream by the number of bytes read. + /// + /// + /// A region of memory. When this method returns, the contents of this region are replaced by the bytes + /// read from the current source. + /// + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes allocated + /// in the buffer if that many bytes are not currently available, or zero (0) if the end of the stream + /// has been reached. + /// + public int Read(Span span) #else - /// - public override int Read(Span span) + /// + public override int Read(Span span) #endif - { - int read = 0; - int canRead; - int didRead; - - while (read < span.Length && this.TryGetInstruction(out DeltaInstruction instruction)) - { - var source = instruction.InstructionType == DeltaInstructionType.Copy ? this.baseStream : this.deltaStream; - - Debug.Assert(instruction.Size > this.offset); - Debug.Assert(source.Position + instruction.Size - this.offset <= source.Length); - canRead = Math.Min(span.Length - read, instruction.Size - this.offset); - didRead = source.Read(span.Slice(read, canRead)); - - Debug.Assert(didRead != 0); - read += didRead; - this.offset += didRead; - } - - this.position += read; - Debug.Assert(read <= span.Length); - return read; - } + { + int read = 0; + int canRead; + int didRead; - /// - public override int Read(byte[] buffer, int offset, int count) + while (read < span.Length && this.TryGetInstruction(out DeltaInstruction instruction)) { - return this.Read(buffer.AsSpan(offset, count)); - } + Stream? source = instruction.InstructionType == DeltaInstructionType.Copy ? this.baseStream : this.deltaStream; - /// - public override void Flush() - { - throw new NotImplementedException(); + Debug.Assert(instruction.Size > this.offset); + Debug.Assert(source.Position + instruction.Size - this.offset <= source.Length); + canRead = Math.Min(span.Length - read, instruction.Size - this.offset); + didRead = source.Read(span.Slice(read, canRead)); + + Debug.Assert(didRead != 0); + read += didRead; + this.offset += didRead; } - /// - public override long Seek(long offset, SeekOrigin origin) + this.position += read; + Debug.Assert(read <= span.Length); + return read; + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + return this.Read(buffer.AsSpan(offset, count)); + } + + /// + public override void Flush() + { + throw new NotImplementedException(); + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin && offset == this.position) { - if (origin == SeekOrigin.Begin && offset == this.position) - { - return this.position; - } - - if (origin == SeekOrigin.Current && offset == 0) - { - return this.position; - } - - if (origin == SeekOrigin.Begin && offset > this.position) - { - // We can optimise this by skipping over instructions rather than executing them - this.ReadExactly(checked((int)(offset - this.position))); - return this.position; - } - else - { - throw new NotImplementedException(); - } + return this.position; } - /// - public override void SetLength(long value) + if (origin == SeekOrigin.Current && offset == 0) { - throw new NotImplementedException(); + return this.position; } - /// - public override void Write(byte[] buffer, int offset, int count) + if (origin == SeekOrigin.Begin && offset > this.position) { - throw new NotImplementedException(); + // We can optimise this by skipping over instructions rather than executing them + this.ReadExactly(checked((int)(offset - this.position))); + return this.position; } - - /// - protected override void Dispose(bool disposing) + else { - this.deltaStream.Dispose(); - this.baseStream.Dispose(); + throw new NotImplementedException(); } + } - private bool TryGetInstruction(out DeltaInstruction instruction) - { - if (this.current is not null && this.offset < this.current.Value.Size) - { - instruction = this.current.Value; - return true; - } + /// + public override void SetLength(long value) + { + throw new NotImplementedException(); + } - this.current = DeltaStreamReader.Read(this.deltaStream); + /// + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } - if (this.current is null) - { - instruction = default; - return false; - } + /// + protected override void Dispose(bool disposing) + { + this.deltaStream.Dispose(); + this.baseStream.Dispose(); + } + private bool TryGetInstruction(out DeltaInstruction instruction) + { + if (this.current is not null && this.offset < this.current.Value.Size) + { instruction = this.current.Value; + return true; + } - switch (instruction.InstructionType) - { - case DeltaInstructionType.Copy: - this.baseStream.Seek(instruction.Offset, SeekOrigin.Begin); - Debug.Assert(this.baseStream.Position == instruction.Offset); - this.offset = 0; - break; + this.current = DeltaStreamReader.Read(this.deltaStream); - case DeltaInstructionType.Insert: - this.offset = 0; - break; + if (this.current is null) + { + instruction = default; + return false; + } - default: - throw new GitException(); - } + instruction = this.current.Value; - return true; + switch (instruction.InstructionType) + { + case DeltaInstructionType.Copy: + this.baseStream.Seek(instruction.Offset, SeekOrigin.Begin); + Debug.Assert(this.baseStream.Position == instruction.Offset); + this.offset = 0; + break; + + case DeltaInstructionType.Insert: + this.offset = 0; + break; + + default: + throw new GitException(); } + + return true; } } diff --git a/src/NerdBank.GitVersioning/ManagedGit/GitPackIndexMappedReader.cs b/src/NerdBank.GitVersioning/ManagedGit/GitPackIndexMappedReader.cs index b3690166..4f71d023 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/GitPackIndexMappedReader.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/GitPackIndexMappedReader.cs @@ -1,166 +1,168 @@ -using System; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + using System.Buffers.Binary; using System.Diagnostics; -using System.IO; using System.IO.MemoryMappedFiles; -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +/// +/// A which uses a memory-mapped file to read from the index. +/// +/// +public unsafe class GitPackIndexMappedReader : GitPackIndexReader { + private readonly MemoryMappedFile file; + private readonly MemoryMappedViewAccessor accessor; + + // The fanout table consists of + // 256 4-byte network byte order integers. + // The N-th entry of this table records the number of objects in the corresponding pack, + // the first byte of whose object name is less than or equal to N. + private readonly int[] fanoutTable = new int[257]; + private byte* ptr; + + private bool initialized; + /// - /// A which uses a memory-mapped file to read from the index. + /// Initializes a new instance of the class. /// - /// - public unsafe class GitPackIndexMappedReader : GitPackIndexReader + /// + /// A which points to the index file. + /// + public GitPackIndexMappedReader(FileStream stream) { - private readonly MemoryMappedFile file; - private readonly MemoryMappedViewAccessor accessor; - - // The fanout table consists of - // 256 4-byte network byte order integers. - // The N-th entry of this table records the number of objects in the corresponding pack, - // the first byte of whose object name is less than or equal to N. - private readonly int[] fanoutTable = new int[257]; - - private byte* ptr; - private bool initialized; - - /// - /// Initializes a new instance of the class. - /// - /// - /// A which points to the index file. - /// - public GitPackIndexMappedReader(FileStream stream) + if (stream is null) { - if (stream is null) - { - throw new ArgumentNullException(nameof(stream)); - } - - this.file = MemoryMappedFile.CreateFromFile(stream, mapName: null, capacity: 0, MemoryMappedFileAccess.Read, HandleInheritability.None, leaveOpen: false); - this.accessor = this.file.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read); - this.accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref this.ptr); + throw new ArgumentNullException(nameof(stream)); } - private ReadOnlySpan Value - { - get - { - return new ReadOnlySpan(this.ptr, (int)this.accessor.Capacity); - } - } + this.file = MemoryMappedFile.CreateFromFile(stream, mapName: null, capacity: 0, MemoryMappedFileAccess.Read, HandleInheritability.None, leaveOpen: false); + this.accessor = this.file.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read); + this.accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref this.ptr); + } - /// - public override (long?, GitObjectId?) GetOffset(Span objectName, bool endsWithHalfByte = false) - { - this.Initialize(); + /// + public override (long? Offset, GitObjectId? ObjectId) GetOffset(Span objectName, bool endsWithHalfByte = false) + { + this.Initialize(); - var packStart = this.fanoutTable[objectName[0]]; - var packEnd = this.fanoutTable[objectName[0] + 1]; - var objectCount = this.fanoutTable[256]; + int packStart = this.fanoutTable[objectName[0]]; + int packEnd = this.fanoutTable[objectName[0] + 1]; + int objectCount = this.fanoutTable[256]; - // The fanout table is followed by a table of sorted 20-byte SHA-1 object names. - // These are packed together without offset values to reduce the cache footprint of the binary search for a specific object name. + // The fanout table is followed by a table of sorted 20-byte SHA-1 object names. + // These are packed together without offset values to reduce the cache footprint of the binary search for a specific object name. - // The object names start at: 4 (header) + 4 (version) + 256 * 4 (fanout table) + 20 * (packStart) - // and end at 4 (header) + 4 (version) + 256 * 4 (fanout table) + 20 * (packEnd) + // The object names start at: 4 (header) + 4 (version) + 256 * 4 (fanout table) + 20 * (packStart) + // and end at 4 (header) + 4 (version) + 256 * 4 (fanout table) + 20 * (packEnd) + int i = 0; + int order = 0; - var i = 0; - var order = 0; + int tableSize = 20 * (packEnd - packStart + 1); + ReadOnlySpan table = this.GetSpan((ulong)(4 + 4 + (256 * 4) + (20 * packStart)), tableSize); - var tableSize = 20 * (packEnd - packStart + 1); - var table = this.Value.Slice(4 + 4 + 256 * 4 + 20 * packStart, tableSize); + int originalPackStart = packStart; - int originalPackStart = packStart; + packEnd -= originalPackStart; + packStart = 0; - packEnd -= originalPackStart; - packStart = 0; + Span buffer = stackalloc byte[20]; + while (packStart <= packEnd) + { + i = (packStart + packEnd) / 2; - while (packStart <= packEnd) + ReadOnlySpan comparand = table.Slice(20 * i, objectName.Length); + if (endsWithHalfByte) { - i = (packStart + packEnd) / 2; - - ReadOnlySpan comparand = table.Slice(20 * i, objectName.Length); - if (endsWithHalfByte) - { - // Copy out the value to be checked so we can zero out the last four bits, - // so that it matches the last 4 bits of the objectName that isn't supposed to be compared. - Span buffer = stackalloc byte[20]; - comparand.CopyTo(buffer); - buffer[objectName.Length - 1] &= 0xf0; - order = buffer.Slice(0, objectName.Length).SequenceCompareTo(objectName); - } - else - { - order = comparand.SequenceCompareTo(objectName); - } - - if (order < 0) - { - packStart = i + 1; - } - else if (order > 0) - { - packEnd = i - 1; - } - else - { - break; - } + // Copy out the value to be checked so we can zero out the last four bits, + // so that it matches the last 4 bits of the objectName that isn't supposed to be compared. + comparand.CopyTo(buffer); + buffer[objectName.Length - 1] &= 0xf0; + order = buffer.Slice(0, objectName.Length).SequenceCompareTo(objectName); } - - if (order != 0) + else { - return (null, null); + order = comparand.SequenceCompareTo(objectName); } - // Get the offset value. It's located at: - // 4 (header) + 4 (version) + 256 * 4 (fanout table) + 20 * objectCount (SHA1 object name table) + 4 * objectCount (CRC32) + 4 * i (offset values) - int offsetTableStart = 4 + 4 + 256 * 4 + 20 * objectCount + 4 * objectCount; - var offsetBuffer = this.Value.Slice(offsetTableStart + 4 * (i + originalPackStart), 4); - var offset = BinaryPrimitives.ReadUInt32BigEndian(offsetBuffer); - - if (offsetBuffer[0] < 128) + if (order < 0) { - return (offset, GitObjectId.Parse(table.Slice(20 * i, 20))); + packStart = i + 1; + } + else if (order > 0) + { + packEnd = i - 1; } else { - // If the first bit of the offset address is set, the offset is stored as a 64-bit value in the table of 8-byte offset entries, - // which follows the table of 4-byte offset entries: "large offsets are encoded as an index into the next table with the msbit set." - offset = offset & 0x7FFFFFFF; - - offsetBuffer = this.Value.Slice(offsetTableStart + 4 * objectCount + 8 * (int)offset, 8); - var offset64 = BinaryPrimitives.ReadInt64BigEndian(offsetBuffer); - return (offset64, GitObjectId.Parse(table.Slice(20 * i, 20))); + break; } } - /// - public override void Dispose() + if (order != 0) { - this.accessor.Dispose(); - this.file.Dispose(); + return (null, null); } - private void Initialize() + // Get the offset value. It's located at: + // 4 (header) + 4 (version) + 256 * 4 (fanout table) + 20 * objectCount (SHA1 object name table) + 4 * objectCount (CRC32) + 4 * i (offset values) + int offsetTableStart = 4 + 4 + (256 * 4) + (20 * objectCount) + (4 * objectCount); + ReadOnlySpan offsetBuffer = this.GetSpan((ulong)(offsetTableStart + (4 * (i + originalPackStart))), 4); + uint offset = BinaryPrimitives.ReadUInt32BigEndian(offsetBuffer); + + if (offsetBuffer[0] < 128) { - if (!this.initialized) - { - var value = this.Value; + return (offset, GitObjectId.Parse(table.Slice(20 * i, 20))); + } + else + { + // If the first bit of the offset address is set, the offset is stored as a 64-bit value in the table of 8-byte offset entries, + // which follows the table of 4-byte offset entries: "large offsets are encoded as an index into the next table with the msbit set." + offset = offset & 0x7FFFFFFF; + + offsetBuffer = this.GetSpan((ulong)(offsetTableStart + (4 * objectCount) + (8 * (int)offset)), 8); + long offset64 = BinaryPrimitives.ReadInt64BigEndian(offsetBuffer); + return (offset64, GitObjectId.Parse(table.Slice(20 * i, 20))); + } + } + + /// + public override void Dispose() + { + if (this.ptr is not null) + { + this.accessor.SafeMemoryMappedViewHandle.ReleasePointer(); + this.ptr = null; + } + + this.accessor.Dispose(); + this.file.Dispose(); + } - var header = value.Slice(0, 4); - var version = BinaryPrimitives.ReadInt32BigEndian(value.Slice(4, 4)); - Debug.Assert(header.SequenceEqual(Header)); - Debug.Assert(version == 2); + private ReadOnlySpan GetSpan(ulong offset, int length) => new ReadOnlySpan(this.ptr + offset, length); - for (int i = 1; i <= 256; i++) - { - this.fanoutTable[i] = BinaryPrimitives.ReadInt32BigEndian(value.Slice(4 + 4 * i, 4)); - } + private void Initialize() + { + if (!this.initialized) + { + const int fanoutTableLength = 256; + ReadOnlySpan value = this.GetSpan(0, 4 + (4 * fanoutTableLength) + 4); + + ReadOnlySpan header = value.Slice(0, 4); + int version = BinaryPrimitives.ReadInt32BigEndian(value.Slice(4, 4)); + Debug.Assert(header.SequenceEqual(Header)); + Debug.Assert(version == 2); - this.initialized = true; + for (int i = 1; i <= fanoutTableLength; i++) + { + this.fanoutTable[i] = BinaryPrimitives.ReadInt32BigEndian(value.Slice(4 + (4 * i), 4)); } + + this.initialized = true; } } } diff --git a/src/NerdBank.GitVersioning/ManagedGit/GitPackIndexReader.cs b/src/NerdBank.GitVersioning/ManagedGit/GitPackIndexReader.cs index d11be037..2d9bde35 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/GitPackIndexReader.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/GitPackIndexReader.cs @@ -1,50 +1,50 @@ -using System; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +/// +/// Base class for classes which support reading data stored in a Git Pack file. +/// +/// +public abstract class GitPackIndexReader : IDisposable { /// - /// Base class for classes which support reading data stored in a Git Pack file. + /// The header of the index file. /// - /// - public abstract class GitPackIndexReader : IDisposable - { - /// - /// The header of the index file. - /// - protected static readonly byte[] Header = new byte[] { 0xff, 0x74, 0x4f, 0x63 }; + protected static readonly byte[] Header = new byte[] { 0xff, 0x74, 0x4f, 0x63 }; - /// - /// Gets the offset of a Git object in the index file. - /// - /// - /// The Git object Id of the Git object for which to get the offset. - /// - /// - /// If found, the offset of the Git object in the index file; otherwise, - /// . - /// - public long? GetOffset(GitObjectId objectId) - { - Span name = stackalloc byte[20]; - objectId.CopyTo(name); - (var offset, var _) = this.GetOffset(name); - return offset; - } + /// + /// Gets the offset of a Git object in the index file. + /// + /// + /// The Git object Id of the Git object for which to get the offset. + /// + /// + /// If found, the offset of the Git object in the index file; otherwise, + /// . + /// + public long? GetOffset(GitObjectId objectId) + { + Span name = stackalloc byte[20]; + objectId.CopyTo(name); + (long? offset, GitObjectId? _) = this.GetOffset(name); + return offset; + } - /// - /// Gets the offset of a Git object in the index file. - /// - /// - /// A partial or full Git object id, in its binary representation. - /// - /// if ends with a byte whose last 4 bits are all zeros and not intended for inclusion in the search; otherwise. - /// - /// If found, the offset of the Git object in the index file; otherwise, - /// . - /// - public abstract (long?, GitObjectId?) GetOffset(Span objectId, bool endsWithHalfByte = false); + /// + /// Gets the offset of a Git object in the index file. + /// + /// + /// A partial or full Git object id, in its binary representation. + /// + /// if ends with a byte whose last 4 bits are all zeros and not intended for inclusion in the search; otherwise. + /// + /// If found, the offset of the Git object in the index file; otherwise, + /// . + /// + public abstract (long? Offset, GitObjectId? ObjectId) GetOffset(Span objectId, bool endsWithHalfByte = false); - /// - public abstract void Dispose(); - } -} \ No newline at end of file + /// + public abstract void Dispose(); +} diff --git a/src/NerdBank.GitVersioning/ManagedGit/GitPackMemoryCache.cs b/src/NerdBank.GitVersioning/ManagedGit/GitPackMemoryCache.cs index c88abe19..c7f8c6eb 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/GitPackMemoryCache.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/GitPackMemoryCache.cs @@ -1,77 +1,76 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; using System.Text; -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +/// +/// +/// The implements the abstract class. +/// When a is added to the , it is wrapped in a +/// . This stream allows for just-in-time, random, read-only +/// access to the underlying data (which may deltafied and/or compressed). +/// +/// +/// Whenever data is read from a , the call is forwarded to the +/// underlying and cached in a . If the same data is read +/// twice, it is read from the , rather than the underlying . +/// +/// +/// and return +/// objects which may operate on the same underlying , but independently maintain +/// their state. +/// +/// +public class GitPackMemoryCache : GitPackCache { - /// - /// - /// The implements the abstract class. - /// When a is added to the , it is wrapped in a - /// . This stream allows for just-in-time, random, read-only - /// access to the underlying data (which may deltafied and/or compressed). - /// - /// - /// Whenever data is read from a , the call is forwarded to the - /// underlying and cached in a . If the same data is read - /// twice, it is read from the , rather than the underlying . - /// - /// - /// and return - /// objects which may operate on the same underlying , but independently maintain - /// their state. - /// - /// - public class GitPackMemoryCache : GitPackCache + private readonly Dictionary cache = new Dictionary(); + + /// + public override Stream Add(long offset, Stream stream) { - private readonly Dictionary cache = new Dictionary(); + var cacheStream = new GitPackMemoryCacheStream(stream); + this.cache.Add(offset, cacheStream); + return new GitPackMemoryCacheViewStream(cacheStream); + } - /// - public override Stream Add(long offset, Stream stream) + /// + public override bool TryOpen(long offset, [NotNullWhen(true)] out Stream? stream) + { + if (this.cache.TryGetValue(offset, out GitPackMemoryCacheStream? cacheStream)) { - var cacheStream = new GitPackMemoryCacheStream(stream); - this.cache.Add(offset, cacheStream); - return new GitPackMemoryCacheViewStream(cacheStream); + stream = new GitPackMemoryCacheViewStream(cacheStream!); + return true; } - /// - public override bool TryOpen(long offset, [NotNullWhen(true)] out Stream? stream) - { - if (this.cache.TryGetValue(offset, out GitPackMemoryCacheStream? cacheStream)) - { - stream = new GitPackMemoryCacheViewStream(cacheStream!); - return true; - } - - stream = null; - return false; - } + stream = null; + return false; + } - /// - public override void GetCacheStatistics(StringBuilder builder) - { - builder.AppendLine($"{this.cache.Count} items in cache"); - } + /// + public override void GetCacheStatistics(StringBuilder builder) + { + builder.AppendLine($"{this.cache.Count} items in cache"); + } - /// - protected override void Dispose(bool disposing) + /// + protected override void Dispose(bool disposing) + { + if (disposing) { - if (disposing) + while (this.cache.Count > 0) { - while (this.cache.Count > 0) - { - var key = this.cache.Keys.First(); - var stream = this.cache[key]; - stream.Dispose(); - this.cache.Remove(key); - } + long key = this.cache.Keys.First(); + GitPackMemoryCacheStream? stream = this.cache[key]; + stream.Dispose(); + this.cache.Remove(key); } - - base.Dispose(disposing); } + + base.Dispose(disposing); } } diff --git a/src/NerdBank.GitVersioning/ManagedGit/GitPackMemoryCacheStream.cs b/src/NerdBank.GitVersioning/ManagedGit/GitPackMemoryCacheStream.cs index 4b0a9191..79dc845c 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/GitPackMemoryCacheStream.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/GitPackMemoryCacheStream.cs @@ -1,117 +1,128 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable -using System; using System.Buffers; -using System.IO; -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +internal class GitPackMemoryCacheStream : Stream { - internal class GitPackMemoryCacheStream : Stream - { - private Stream stream; - private readonly MemoryStream cacheStream = new MemoryStream(); - private long length; + private readonly Stream stream; + private readonly MemoryStream cacheStream = new MemoryStream(); + private readonly long length; - public GitPackMemoryCacheStream(Stream stream) - { - this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); - this.length = this.stream.Length; - } + public GitPackMemoryCacheStream(Stream stream) + { + this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); + this.length = this.stream.Length; + } - public override bool CanRead => true; + /// + public override bool CanRead => true; - public override bool CanSeek => true; + /// + public override bool CanSeek => true; - public override bool CanWrite => false; + /// + public override bool CanWrite => false; - public override long Length => this.length; + /// + public override long Length => this.length; - public override long Position - { - get => this.cacheStream.Position; - set => throw new NotSupportedException(); - } + /// + public override long Position + { + get => this.cacheStream.Position; + set => throw new NotSupportedException(); + } - public override void Flush() - { - throw new NotSupportedException(); - } + /// + public override void Flush() + { + throw new NotSupportedException(); + } #if NETSTANDARD2_0 - public int Read(Span buffer) + public int Read(Span buffer) #else - /// - public override int Read(Span buffer) + /// + public override int Read(Span buffer) #endif + { + if (this.cacheStream.Length < this.length + && this.cacheStream.Position + buffer.Length > this.cacheStream.Length) { - if (this.cacheStream.Length < this.length - && this.cacheStream.Position + buffer.Length > this.cacheStream.Length) - { - var currentPosition = this.cacheStream.Position; - var toRead = (int)(buffer.Length - this.cacheStream.Length + this.cacheStream.Position); - int actualRead = this.stream.Read(buffer.Slice(0, toRead)); - this.cacheStream.Seek(0, SeekOrigin.End); - this.cacheStream.Write(buffer.Slice(0, actualRead)); - this.cacheStream.Seek(currentPosition, SeekOrigin.Begin); - this.DisposeStreamIfRead(); - } - - return this.cacheStream.Read(buffer); + long currentPosition = this.cacheStream.Position; + int toRead = (int)(buffer.Length - this.cacheStream.Length + this.cacheStream.Position); + int actualRead = this.stream.Read(buffer.Slice(0, toRead)); + this.cacheStream.Seek(0, SeekOrigin.End); + this.cacheStream.Write(buffer.Slice(0, actualRead)); + this.cacheStream.Seek(currentPosition, SeekOrigin.Begin); + this.DisposeStreamIfRead(); } - public override int Read(byte[] buffer, int offset, int count) - { - return this.Read(buffer.AsSpan(offset, count)); - } + return this.cacheStream.Read(buffer); + } - public override long Seek(long offset, SeekOrigin origin) - { - if (origin != SeekOrigin.Begin) - { - throw new NotSupportedException(); - } - - if (offset > this.cacheStream.Length) - { - this.cacheStream.Seek(0, SeekOrigin.End); - int toRead = (int)(offset - this.cacheStream.Length); - this.stream.ReadExactly(toRead, this.cacheStream); - this.DisposeStreamIfRead(); - return this.cacheStream.Position; - } - else - { - return this.cacheStream.Seek(offset, origin); - } - } + /// + public override int Read(byte[] buffer, int offset, int count) + { + return this.Read(buffer.AsSpan(offset, count)); + } - public override void SetLength(long value) + /// + public override long Seek(long offset, SeekOrigin origin) + { + if (origin != SeekOrigin.Begin) { throw new NotSupportedException(); } - public override void Write(byte[] buffer, int offset, int count) + if (offset > this.cacheStream.Length) { - throw new NotSupportedException(); + this.cacheStream.Seek(0, SeekOrigin.End); + int toRead = (int)(offset - this.cacheStream.Length); + this.stream.ReadExactly(toRead, this.cacheStream); + this.DisposeStreamIfRead(); + return this.cacheStream.Position; } - - protected override void Dispose(bool disposing) + else { - if (disposing) - { - this.stream.Dispose(); - this.cacheStream.Dispose(); - } + return this.cacheStream.Seek(offset, origin); + } + } + + /// + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } - base.Dispose(disposing); + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.stream.Dispose(); + this.cacheStream.Dispose(); } - private void DisposeStreamIfRead() + base.Dispose(disposing); + } + + private void DisposeStreamIfRead() + { + if (this.cacheStream.Length == this.stream.Length) { - if (this.cacheStream.Length == this.stream.Length) - { - this.stream.Dispose(); - } + this.stream.Dispose(); } } } diff --git a/src/NerdBank.GitVersioning/ManagedGit/GitPackMemoryCacheViewStream.cs b/src/NerdBank.GitVersioning/ManagedGit/GitPackMemoryCacheViewStream.cs index e91b67c1..80f47b33 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/GitPackMemoryCacheViewStream.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/GitPackMemoryCacheViewStream.cs @@ -1,77 +1,85 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +internal class GitPackMemoryCacheViewStream : Stream { - internal class GitPackMemoryCacheViewStream : Stream - { - private readonly GitPackMemoryCacheStream baseStream; + private readonly GitPackMemoryCacheStream baseStream; - public GitPackMemoryCacheViewStream(GitPackMemoryCacheStream baseStream) - { - this.baseStream = baseStream ?? throw new ArgumentNullException(nameof(baseStream)); - } + private long position; - public override bool CanRead => true; + public GitPackMemoryCacheViewStream(GitPackMemoryCacheStream baseStream) + { + this.baseStream = baseStream ?? throw new ArgumentNullException(nameof(baseStream)); + } - public override bool CanSeek => true; + /// + public override bool CanRead => true; - public override bool CanWrite => false; + /// + public override bool CanSeek => true; - public override long Length => this.baseStream.Length; + /// + public override bool CanWrite => false; - private long position; + /// + public override long Length => this.baseStream.Length; - public override long Position - { - get => this.position; - set => throw new NotSupportedException(); - } + /// + public override long Position + { + get => this.position; + set => throw new NotSupportedException(); + } - public override void Flush() => throw new NotImplementedException(); + /// + public override void Flush() => throw new NotImplementedException(); - public override int Read(byte[] buffer, int offset, int count) - { - return this.Read(buffer.AsSpan(offset, count)); - } + /// + public override int Read(byte[] buffer, int offset, int count) + { + return this.Read(buffer.AsSpan(offset, count)); + } #if NETSTANDARD2_0 - public int Read(Span buffer) + public int Read(Span buffer) #else - /// - public override int Read(Span buffer) + /// + public override int Read(Span buffer) #endif - { - int read = 0; + { + int read = 0; - lock (this.baseStream) + lock (this.baseStream) + { + if (this.baseStream.Position != this.position) { - if (this.baseStream.Position != this.position) - { - this.baseStream.Seek(this.position, SeekOrigin.Begin); - } - - read = this.baseStream.Read(buffer); + this.baseStream.Seek(this.position, SeekOrigin.Begin); } - this.position += read; - return read; + read = this.baseStream.Read(buffer); } - public override long Seek(long offset, SeekOrigin origin) - { - if (origin != SeekOrigin.Begin) - { - throw new NotSupportedException(); - } + this.position += read; + return read; + } - this.position = Math.Min(offset, this.Length); - return this.position; + /// + public override long Seek(long offset, SeekOrigin origin) + { + if (origin != SeekOrigin.Begin) + { + throw new NotSupportedException(); } - public override void SetLength(long value) => throw new NotSupportedException(); - public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + this.position = Math.Min(offset, this.Length); + return this.position; } + + /// + public override void SetLength(long value) => throw new NotSupportedException(); + + /// + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); } diff --git a/src/NerdBank.GitVersioning/ManagedGit/GitPackNullCache.cs b/src/NerdBank.GitVersioning/ManagedGit/GitPackNullCache.cs index 0050123d..f96e8ba9 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/GitPackNullCache.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/GitPackNullCache.cs @@ -1,37 +1,38 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Text; -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +/// +/// A no-op implementation of the class. +/// +public class GitPackNullCache : GitPackCache { /// - /// A no-op implementation of the class. + /// Gets the default instance of the class. /// - public class GitPackNullCache : GitPackCache - { - /// - /// Gets the default instance of the class. - /// - public static GitPackNullCache Instance { get; } = new GitPackNullCache(); + public static GitPackNullCache Instance { get; } = new GitPackNullCache(); - /// - public override Stream Add(long offset, Stream stream) - { - return stream; - } + /// + public override Stream Add(long offset, Stream stream) + { + return stream; + } - /// - public override bool TryOpen(long offset, [NotNullWhen(true)] out Stream? stream) - { - stream = null; - return false; - } + /// + public override bool TryOpen(long offset, [NotNullWhen(true)] out Stream? stream) + { + stream = null; + return false; + } - /// - public override void GetCacheStatistics(StringBuilder builder) - { - } + /// + public override void GetCacheStatistics(StringBuilder builder) + { } } diff --git a/src/NerdBank.GitVersioning/ManagedGit/GitPackObjectType.cs b/src/NerdBank.GitVersioning/ManagedGit/GitPackObjectType.cs index e0ff3ef2..5275c9d5 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/GitPackObjectType.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/GitPackObjectType.cs @@ -1,15 +1,44 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Nerdbank.GitVersioning.ManagedGit +#nullable enable + +namespace Nerdbank.GitVersioning.ManagedGit; + +internal enum GitPackObjectType { - internal enum GitPackObjectType - { - Invalid = 0, - OBJ_COMMIT = 1, - OBJ_TREE = 2, - OBJ_BLOB = 3, - OBJ_TAG = 4, - OBJ_OFS_DELTA = 6, - OBJ_REF_DELTA = 7, - } + /// + /// Invalid. + /// + Invalid = 0, + + /// + /// A commit. + /// + OBJ_COMMIT = 1, + + /// + /// A tree. + /// + OBJ_TREE = 2, + + /// + /// A blob. + /// + OBJ_BLOB = 3, + + /// + /// A tag. + /// + OBJ_TAG = 4, + + /// + /// An OFS_DELTA. + /// + OBJ_OFS_DELTA = 6, + + /// + /// A REF_DELTA. + /// + OBJ_REF_DELTA = 7, } diff --git a/src/NerdBank.GitVersioning/ManagedGit/GitPackPooledStream.cs b/src/NerdBank.GitVersioning/ManagedGit/GitPackPooledStream.cs index 74cde41e..53ca6769 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/GitPackPooledStream.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/GitPackPooledStream.cs @@ -1,104 +1,103 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.IO; -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +/// +/// A pooled , which wraps around a +/// which will be returned to a pool +/// instead of actually being closed when is called. +/// +public class GitPackPooledStream : Stream { + private readonly Stream stream; + private readonly Queue pool; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The which is being pooled. + /// + /// + /// A to which the stream will be returned. + /// + public GitPackPooledStream(Stream stream, Queue pool) + { + this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); + this.pool = pool ?? throw new ArgumentNullException(nameof(pool)); + } + /// - /// A pooled , which wraps around a - /// which will be returned to a pool - /// instead of actually being closed when is called. + /// Gets the underlying for this . /// - public class GitPackPooledStream : Stream + public Stream BaseStream => this.stream; + + /// + public override bool CanRead => this.stream.CanRead; + + /// + public override bool CanSeek => this.stream.CanSeek; + + /// + public override bool CanWrite => this.stream.CanWrite; + + /// + public override long Length => this.stream.Length; + + /// + public override long Position + { + get => this.stream.Position; + set => this.stream.Position = value; + } + + /// + public override void Flush() { - private readonly Stream stream; - private readonly Queue pool; - - /// - /// Initializes a new instance of the class. - /// - /// - /// The which is being pooled. - /// - /// - /// A to which the stream will be returned. - /// - public GitPackPooledStream(Stream stream, Queue pool) - { - this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); - this.pool = pool ?? throw new ArgumentNullException(nameof(pool)); - } - - /// - /// Gets the underlying for this . - /// - public Stream BaseStream => this.stream; - - /// - public override bool CanRead => this.stream.CanRead; - - /// - public override bool CanSeek => this.stream.CanSeek; - - /// - public override bool CanWrite => this.stream.CanWrite; - - /// - public override long Length => this.stream.Length; - - /// - public override long Position - { - get => this.stream.Position; - set => this.stream.Position = value; - } - - /// - public override void Flush() - { - this.stream.Flush(); - } + this.stream.Flush(); + } #if !NETSTANDARD2_0 - /// - public override int Read(Span buffer) - { - return this.stream.Read(buffer); - } + /// + public override int Read(Span buffer) + { + return this.stream.Read(buffer); + } #endif - /// - public override int Read(byte[] buffer, int offset, int count) - { - return this.stream.Read(buffer, offset, count); - } - - /// - public override long Seek(long offset, SeekOrigin origin) - { - return this.stream.Seek(offset, origin); - } - - /// - public override void SetLength(long value) - { - this.stream.SetLength(value); - } - - /// - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - - /// - protected override void Dispose(bool disposing) - { - this.pool.Enqueue(this); - Debug.WriteLine("Returning stream to pool"); - } + /// + public override int Read(byte[] buffer, int offset, int count) + { + return this.stream.Read(buffer, offset, count); + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + return this.stream.Seek(offset, origin); + } + + /// + public override void SetLength(long value) + { + this.stream.SetLength(value); + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + /// + protected override void Dispose(bool disposing) + { + this.pool.Enqueue(this); + Debug.WriteLine("Returning stream to pool"); } } diff --git a/src/NerdBank.GitVersioning/ManagedGit/GitPackReader.cs b/src/NerdBank.GitVersioning/ManagedGit/GitPackReader.cs index 354060d4..12ca7865 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/GitPackReader.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/GitPackReader.cs @@ -1,117 +1,118 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable -using System; using System.Buffers.Binary; using System.Diagnostics; -using System.IO; -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +internal static class GitPackReader { - internal static class GitPackReader - { - private static readonly byte[] Signature = GitRepository.Encoding.GetBytes("PACK"); + private static readonly byte[] Signature = GitRepository.Encoding.GetBytes("PACK"); - public static Stream GetObject(GitPack pack, Stream stream, long offset, string objectType, GitPackObjectType packObjectType) + public static Stream GetObject(GitPack pack, Stream stream, long offset, string objectType, GitPackObjectType packObjectType) + { + if (pack is null) { - if (pack is null) - { - throw new ArgumentNullException(nameof(pack)); - } + throw new ArgumentNullException(nameof(pack)); + } - if (stream is null) - { - throw new ArgumentNullException(nameof(stream)); - } + if (stream is null) + { + throw new ArgumentNullException(nameof(stream)); + } - // Read the signature + // Read the signature #if DEBUG - stream.Seek(0, SeekOrigin.Begin); - Span buffer = stackalloc byte[12]; - stream.ReadAll(buffer); + stream.Seek(0, SeekOrigin.Begin); + Span buffer = stackalloc byte[12]; + stream.ReadAll(buffer); - Debug.Assert(buffer.Slice(0, 4).SequenceEqual(Signature)); + Debug.Assert(buffer.Slice(0, 4).SequenceEqual(Signature)); - var versionNumber = BinaryPrimitives.ReadInt32BigEndian(buffer.Slice(4, 4)); - Debug.Assert(versionNumber == 2); + int versionNumber = BinaryPrimitives.ReadInt32BigEndian(buffer.Slice(4, 4)); + Debug.Assert(versionNumber == 2); - var numberOfObjects = BinaryPrimitives.ReadInt32BigEndian(buffer.Slice(8, 4)); + int numberOfObjects = BinaryPrimitives.ReadInt32BigEndian(buffer.Slice(8, 4)); #endif - stream.Seek(offset, SeekOrigin.Begin); + stream.Seek(offset, SeekOrigin.Begin); - var (type, decompressedSize) = ReadObjectHeader(stream); + (GitPackObjectType type, long decompressedSize) = ReadObjectHeader(stream); - if (type == GitPackObjectType.OBJ_OFS_DELTA) - { - var baseObjectRelativeOffset = ReadVariableLengthInteger(stream); - long baseObjectOffset = offset - baseObjectRelativeOffset; - - var deltaStream = new ZLibStream(stream, decompressedSize); - var baseObjectStream = pack.GetObject(baseObjectOffset, objectType); - - return new GitPackDeltafiedStream(baseObjectStream, deltaStream); - } - else if (type == GitPackObjectType.OBJ_REF_DELTA) - { - Span baseObjectId = stackalloc byte[20]; - stream.ReadAll(baseObjectId); + if (type == GitPackObjectType.OBJ_OFS_DELTA) + { + long baseObjectRelativeOffset = ReadVariableLengthInteger(stream); + long baseObjectOffset = offset - baseObjectRelativeOffset; - Stream baseObject = pack.GetObjectFromRepository(GitObjectId.Parse(baseObjectId), objectType)!; - var seekableBaseObject = new GitPackMemoryCacheStream(baseObject); + var deltaStream = new ZLibStream(stream, decompressedSize); + Stream? baseObjectStream = pack.GetObject(baseObjectOffset, objectType); - var deltaStream = new ZLibStream(stream, decompressedSize); + return new GitPackDeltafiedStream(baseObjectStream, deltaStream); + } + else if (type == GitPackObjectType.OBJ_REF_DELTA) + { + Span baseObjectId = stackalloc byte[20]; + stream.ReadAll(baseObjectId); - return new GitPackDeltafiedStream(seekableBaseObject, deltaStream); - } + Stream baseObject = pack.GetObjectFromRepository(GitObjectId.Parse(baseObjectId), objectType)!; + var seekableBaseObject = new GitPackMemoryCacheStream(baseObject); - // Tips for handling deltas: https://github.com/choffmeister/gitnet/blob/4d907623d5ce2d79a8875aee82e718c12a8aad0b/src/GitNet/GitPack.cs - if (type != packObjectType) - { - throw new GitException($"An object of type {objectType} could not be located at offset {offset}.") { ErrorCode = GitException.ErrorCodes.ObjectNotFound }; - } + var deltaStream = new ZLibStream(stream, decompressedSize); - return new ZLibStream(stream, decompressedSize); + return new GitPackDeltafiedStream(seekableBaseObject, deltaStream); } - private static (GitPackObjectType, long) ReadObjectHeader(Stream stream) + // Tips for handling deltas: https://github.com/choffmeister/gitnet/blob/4d907623d5ce2d79a8875aee82e718c12a8aad0b/src/GitNet/GitPack.cs + if (type != packObjectType) { - Span value = stackalloc byte[1]; - stream.Read(value); - - var type = (GitPackObjectType)((value[0] & 0b0111_0000) >> 4); - long length = value[0] & 0b_1111; + throw new GitException($"An object of type {objectType} could not be located at offset {offset}.") { ErrorCode = GitException.ErrorCodes.ObjectNotFound }; + } - if ((value[0] & 0b1000_0000) == 0) - { - return (type, length); - } + return new ZLibStream(stream, decompressedSize); + } - int shift = 4; + private static (GitPackObjectType ObjectType, long Length) ReadObjectHeader(Stream stream) + { + Span value = stackalloc byte[1]; + stream.Read(value); - do - { - stream.Read(value); - length = length | ((value[0] & (long)0b0111_1111) << shift); - shift += 7; - } while ((value[0] & 0b1000_0000) != 0); + var type = (GitPackObjectType)((value[0] & 0b0111_0000) >> 4); + long length = value[0] & 0b_1111; + if ((value[0] & 0b1000_0000) == 0) + { return (type, length); } - private static long ReadVariableLengthInteger(Stream stream) + int shift = 4; + + do + { + stream.Read(value); + length = length | ((value[0] & 0b0111_1111L) << shift); + shift += 7; + } + while ((value[0] & 0b1000_0000) != 0); + + return (type, length); + } + + private static long ReadVariableLengthInteger(Stream stream) + { + long offset = -1; + int b; + + do { - long offset = -1; - int b; - - do - { - offset++; - b = stream.ReadByte(); - offset = (offset << 7) + (b & 127); - } - while ((b & (byte)128) != 0); - - return offset; + offset++; + b = stream.ReadByte(); + offset = (offset << 7) + (b & 127); } + while ((b & 128) != 0); + + return offset; } } diff --git a/src/NerdBank.GitVersioning/ManagedGit/GitReferenceReader.cs b/src/NerdBank.GitVersioning/ManagedGit/GitReferenceReader.cs index e7ce4ae6..942e8ff2 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/GitReferenceReader.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/GitReferenceReader.cs @@ -1,39 +1,38 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; -using System.IO; +#nullable enable -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +internal class GitReferenceReader { - internal class GitReferenceReader + private static readonly byte[] RefPrefix = GitRepository.Encoding.GetBytes("ref: "); + + public static object ReadReference(Stream stream) { - private readonly static byte[] RefPrefix = GitRepository.Encoding.GetBytes("ref: "); + Span reference = stackalloc byte[(int)stream.Length]; + stream.ReadAll(reference); - public static object ReadReference(Stream stream) - { - Span reference = stackalloc byte[(int)stream.Length]; - stream.ReadAll(reference); + return ReadReference(reference); + } - return ReadReference(reference); + public static object ReadReference(Span value) + { + if (value.Length == 41 && !value.StartsWith(RefPrefix)) + { + // Skip the trailing \n + return GitObjectId.ParseHex(value.Slice(0, 40)); } - - public static object ReadReference(Span value) + else { - if (value.Length == 41 && !value.StartsWith(RefPrefix)) + if (!value.StartsWith(RefPrefix)) { - // Skip the trailing \n - return GitObjectId.ParseHex(value.Slice(0, 40)); + throw new GitException(); } - else - { - if (!value.StartsWith(RefPrefix)) - { - throw new GitException(); - } - // Skip the terminating \n character - return GitRepository.GetString(value.Slice(RefPrefix.Length, value.Length - RefPrefix.Length - 1)); - } + // Skip the terminating \n character + return GitRepository.GetString(value.Slice(RefPrefix.Length, value.Length - RefPrefix.Length - 1)); } } } diff --git a/src/NerdBank.GitVersioning/ManagedGit/GitRepository.cs b/src/NerdBank.GitVersioning/ManagedGit/GitRepository.cs index c079b951..183f04ef 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/GitRepository.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/GitRepository.cs @@ -1,774 +1,770 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.IO; -using System.Linq; using System.Runtime.InteropServices; using System.Text; using Validation; -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +/// +/// Provides access to a Git repository. +/// +public class GitRepository : IDisposable { + private const string HeadFileName = "HEAD"; + private const string GitDirectoryName = ".git"; + private readonly Lazy> packs; + /// - /// Provides access to a Git repository. + /// UTF-16 encoded string. /// - public class GitRepository : IDisposable - { - private const string HeadFileName = "HEAD"; - private const string GitDirectoryName = ".git"; - private readonly Lazy> packs; - - /// - /// UTF-16 encoded string. - /// - private readonly char[] objectPathBuffer; + private readonly char[] objectPathBuffer; - private readonly List alternates = new List(); + private readonly List alternates = new List(); #if DEBUG - private Dictionary histogram = new Dictionary(); + private readonly Dictionary histogram = new Dictionary(); #endif - /// - /// Creates a new instance of the class. - /// - /// - /// - /// A which represents the git repository, or - /// if no git repository was found. - /// - public static GitRepository? Create(string? workingDirectory) - { - if (!GitContext.TryFindGitPaths(workingDirectory, out string? gitDirectory, out string? workingTreeDirectory, out string? workingTreeRelativePath)) - { - return null; + /// + /// Initializes a new instance of the class. + /// + /// + /// The current working directory. This can be a subdirectory of the Git repository. + /// + /// + /// The directory in which the git HEAD file is stored. This is the .git directory unless the working directory is a worktree. + /// + /// + /// The common Git directory, which is parent to the objects, refs, and other directories. + /// + /// + /// The object directory in which Git objects are stored. + /// + public GitRepository(string workingDirectory, string gitDirectory, string commonDirectory, string objectDirectory) + { + this.WorkingDirectory = workingDirectory ?? throw new ArgumentNullException(nameof(workingDirectory)); + this.GitDirectory = gitDirectory ?? throw new ArgumentNullException(nameof(gitDirectory)); + this.CommonDirectory = commonDirectory ?? throw new ArgumentNullException(nameof(commonDirectory)); + this.ObjectDirectory = objectDirectory ?? throw new ArgumentNullException(nameof(objectDirectory)); + + // Normalize paths + this.WorkingDirectory = TrimEndingDirectorySeparator(Path.GetFullPath(this.WorkingDirectory)); + this.GitDirectory = TrimEndingDirectorySeparator(Path.GetFullPath(this.GitDirectory)); + this.CommonDirectory = TrimEndingDirectorySeparator(Path.GetFullPath(this.CommonDirectory)); + this.ObjectDirectory = TrimEndingDirectorySeparator(Path.GetFullPath(this.ObjectDirectory)); + + if (FileHelpers.TryOpen( + Path.Combine(this.ObjectDirectory, "info", "alternates"), + out FileStream? alternateStream)) + { + // There's not a lot of documentation on git alternates; but this StackOverflow question + // https://stackoverflow.com/questions/36123655/what-is-the-git-alternates-mechanism + // provides a good starting point. + Span alternates = stackalloc byte[4096]; + int length = alternateStream!.Read(alternates); + alternates = alternates.Slice(0, length); + + foreach (string? alternate in ParseAlternates(alternates)) + { + this.alternates.Add( + GitRepository.Create( + workingDirectory, + gitDirectory, + commonDirectory, + objectDirectory: Path.GetFullPath(Path.Combine(this.ObjectDirectory, alternate)))); } + } - string commonDirectory = gitDirectory; - string commonDirFile = Path.Combine(gitDirectory, "commondir"); + int pathLengthInChars = this.ObjectDirectory.Length + + 1 // '/' + + 2 // 'xy' is first byte as 2 hex characters. + + 1 // '/' + + 38 // 19 bytes * 2 hex chars each + + 1; // Trailing null character + this.objectPathBuffer = new char[pathLengthInChars]; + this.ObjectDirectory.CopyTo(0, this.objectPathBuffer, 0, this.ObjectDirectory.Length); - if (File.Exists(commonDirFile)) - { - var commonDirectoryRelativePath = File.ReadAllText(commonDirFile).Trim('\n'); - commonDirectory = Path.Combine(gitDirectory, commonDirectoryRelativePath); - } + this.objectPathBuffer[this.ObjectDirectory.Length] = '/'; + this.objectPathBuffer[this.ObjectDirectory.Length + 3] = '/'; + this.objectPathBuffer[pathLengthInChars - 1] = '\0'; // Make sure to initialize with zeros - string objectDirectory = Path.Combine(commonDirectory, "objects"); - - return new GitRepository(workingDirectory!, gitDirectory, commonDirectory, objectDirectory); - } - - /// - /// Creates a new instance of the class. - /// - /// - /// - /// - /// - public static GitRepository Create(string workingDirectory, string gitDirectory, string commonDirectory, string objectDirectory) - { - return new GitRepository(workingDirectory, gitDirectory, commonDirectory, objectDirectory); - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// The current working directory. This can be a subdirectory of the Git repository. - /// - /// - /// The directory in which the git HEAD file is stored. This is the .git directory unless the working directory is a worktree. - /// - /// - /// The common Git directory, which is parent to the objects, refs, and other directories. - /// - /// - /// The object directory in which Git objects are stored. - /// - public GitRepository(string workingDirectory, string gitDirectory, string commonDirectory, string objectDirectory) - { - this.WorkingDirectory = workingDirectory ?? throw new ArgumentNullException(nameof(workingDirectory)); - this.GitDirectory = gitDirectory ?? throw new ArgumentNullException(nameof(gitDirectory)); - this.CommonDirectory = commonDirectory ?? throw new ArgumentNullException(nameof(commonDirectory)); - this.ObjectDirectory = objectDirectory ?? throw new ArgumentNullException(nameof(objectDirectory)); - - // Normalize paths - this.WorkingDirectory = TrimEndingDirectorySeparator(Path.GetFullPath(this.WorkingDirectory)); - this.GitDirectory = TrimEndingDirectorySeparator(Path.GetFullPath(this.GitDirectory)); - this.CommonDirectory = TrimEndingDirectorySeparator(Path.GetFullPath(this.CommonDirectory)); - this.ObjectDirectory = TrimEndingDirectorySeparator(Path.GetFullPath(this.ObjectDirectory)); - - if (FileHelpers.TryOpen( - Path.Combine(this.ObjectDirectory, "info", "alternates"), - out var alternateStream)) - { - // There's not a lot of documentation on git alternates; but this StackOverflow question - // https://stackoverflow.com/questions/36123655/what-is-the-git-alternates-mechanism - // provides a good starting point. - Span alternates = stackalloc byte[4096]; - var length = alternateStream!.Read(alternates); - alternates = alternates.Slice(0, length); - - foreach (var alternate in ParseAlternates(alternates)) - { - this.alternates.Add( - GitRepository.Create( - workingDirectory, - gitDirectory, - commonDirectory, - objectDirectory: Path.GetFullPath(Path.Combine(this.ObjectDirectory, alternate)))); - } - } + this.packs = new Lazy>(this.LoadPacks); + } + // TODO: read from Git settings - int pathLengthInChars = this.ObjectDirectory.Length - + 1 // '/' - + 2 // 'xy' is first byte as 2 hex characters. - + 1 // '/' - + 38 // 19 bytes * 2 hex chars each - + 1; // Trailing null character - this.objectPathBuffer = new char[pathLengthInChars]; - this.ObjectDirectory.CopyTo(0, this.objectPathBuffer, 0, this.ObjectDirectory.Length); - - this.objectPathBuffer[this.ObjectDirectory.Length] = '/'; - this.objectPathBuffer[this.ObjectDirectory.Length + 3] = '/'; - this.objectPathBuffer[pathLengthInChars - 1] = '\0'; // Make sure to initialize with zeros - - this.packs = new Lazy>(this.LoadPacks); - } - - // TODO: read from Git settings - /// - /// Gets a value indicating whether this Git repository is case-insensitive. - /// - public bool IgnoreCase { get; private set; } = false; - - /// - /// Gets the path to the current working directory. - /// - public string WorkingDirectory { get; private set; } - - /// - /// Gets the path to the Git directory, in which at minimum HEAD is stored. - /// Use for all other metadata (e.g. references, configuration). - /// - public string GitDirectory { get; private set; } - - /// - /// Gets the path to the common directory, in which shared Git data (e.g. objects) are stored. - /// - public string CommonDirectory { get; private set; } - - /// - /// Gets the path to the Git object directory. It is a subdirectory of . - /// - public string ObjectDirectory { get; private set; } - - /// - /// Gets the encoding used by this Git repository. - /// - public static Encoding Encoding => Encoding.UTF8; - - /// - /// Shortens the object id - /// - /// - /// The object Id to shorten. - /// - /// - /// The minimum string length. - /// - /// - /// The short object id. - /// - public string ShortenObjectId(GitObjectId objectId, int minimum) - { - var sha = objectId.ToString(); - - for (int length = minimum; length < sha.Length; length += 2) - { - var objectish = sha.Substring(0, length); + /// + /// Gets the encoding used by this Git repository. + /// + public static Encoding Encoding => Encoding.UTF8; - if (this.Lookup(objectish) is not null) - { - return objectish; - } - } + /// + /// Gets a value indicating whether this Git repository is case-insensitive. + /// + public bool IgnoreCase { get; private set; } = false; - return sha; - } + /// + /// Gets the path to the current working directory. + /// + public string WorkingDirectory { get; private set; } + + /// + /// Gets the path to the Git directory, in which at minimum HEAD is stored. + /// Use for all other metadata (e.g. references, configuration). + /// + public string GitDirectory { get; private set; } + + /// + /// Gets the path to the common directory, in which shared Git data (e.g. objects) are stored. + /// + public string CommonDirectory { get; private set; } + + /// + /// Gets the path to the Git object directory. It is a subdirectory of . + /// + public string ObjectDirectory { get; private set; } - /// - /// Returns the current HEAD as a reference (if available) or a Git object id. - /// - /// - /// The current HEAD as a reference (if available) or a Git object id. - /// - public object GetHeadAsReferenceOrSha() + /// + /// Creates a new instance of the class. + /// + /// + /// + /// A which represents the git repository, or + /// if no git repository was found. + /// + public static GitRepository? Create(string? workingDirectory) + { + if (!GitContext.TryFindGitPaths(workingDirectory, out string? gitDirectory, out string? workingTreeDirectory, out string? workingTreeRelativePath)) { - using (var stream = File.OpenRead(Path.Combine(this.GitDirectory, HeadFileName))) - { - return GitReferenceReader.ReadReference(stream); - } + return null; } - /// - /// Gets the object ID of the current HEAD. - /// - /// - /// The object ID of the current HEAD. - /// - public GitObjectId GetHeadCommitSha() + string commonDirectory = gitDirectory; + string commonDirFile = Path.Combine(gitDirectory, "commondir"); + + if (File.Exists(commonDirFile)) { - return this.Lookup("HEAD") ?? GitObjectId.Empty; + string? commonDirectoryRelativePath = File.ReadAllText(commonDirFile).Trim('\n'); + commonDirectory = Path.Combine(gitDirectory, commonDirectoryRelativePath); } - /// - /// Gets the current HEAD commit, if available. - /// - /// - /// A value indicating whether to populate the field. - /// - /// - /// The current HEAD commit, or if not available. - /// - public GitCommit? GetHeadCommit(bool readAuthor = false) - { - var headCommitId = this.GetHeadCommitSha(); + string objectDirectory = Path.Combine(commonDirectory, "objects"); - if (headCommitId == GitObjectId.Empty) - { - return null; - } + return new GitRepository(workingDirectory!, gitDirectory, commonDirectory, objectDirectory); + } - return this.GetCommit(headCommitId, readAuthor); - } + /// + /// Creates a new instance of the class. + /// + /// + /// + /// + /// + /// A newly created instance. + public static GitRepository Create(string workingDirectory, string gitDirectory, string commonDirectory, string objectDirectory) + { + return new GitRepository(workingDirectory, gitDirectory, commonDirectory, objectDirectory); + } - /// - /// Gets a commit by its Git object Id. - /// - /// - /// The Git object Id of the commit. - /// - /// - /// A value indicating whether to populate the field. - /// - /// - /// The requested commit. - /// - public GitCommit GetCommit(GitObjectId sha, bool readAuthor = false) + /// + /// Decodes a sequence of bytes from the specified byte array into a . + /// + /// + /// The span containing the sequence of UTF-8 bytes to decode. + /// + /// + /// A that contains the results of decoding the specified sequence of bytes. + /// + public static unsafe string GetString(ReadOnlySpan bytes) + { + if (bytes.Length == 0) { - using (Stream? stream = this.GetObjectBySha(sha, "commit")) - { - if (stream is null) - { - throw new GitException($"The commit {sha} was not found in this repository.") { ErrorCode = GitException.ErrorCodes.ObjectNotFound }; - } - - return GitCommitReader.Read(stream, sha, readAuthor); - } + return string.Empty; } - /// - /// Parses any committish to an object id. - /// - /// Any "objectish" string (e.g. commit ID (partial or full), branch name, tag name, or "HEAD"). - /// The object ID referenced by if found; otherwise . - public GitObjectId? Lookup(string objectish) + fixed (byte* pBytes = bytes) { - bool skipObjectIdLookup = false; + return Encoding.GetString(pBytes, bytes.Length); + } + } - if (objectish == "HEAD") - { - var reference = this.GetHeadAsReferenceOrSha(); - if (reference is GitObjectId headObjectId) - { - return headObjectId; - } + /// + /// Parses the contents of the alternates file, and returns a list of (relative) paths to the alternate object directories. + /// + /// + /// The contents of the alternates files. + /// + /// + /// A list of (relative) paths to the alternate object directories. + /// + public static List ParseAlternates(ReadOnlySpan alternates) + => ParseAlternates(alternates, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 2 : 0); - objectish = (string)reference; - } + /// + /// Parses the contents of the alternates file, and returns a list of (relative) paths to the alternate object directories. + /// + /// + /// The contents of the alternates files. + /// + /// + /// The number of bytes to skip in the span when looking for a delimiter. + /// + /// + /// A list of (relative) paths to the alternate object directories. + /// + public static List ParseAlternates(ReadOnlySpan alternates, int skipCount) + { + var values = new List(); - var possibleLooseFileMatches = new List(); - if (objectish.StartsWith("refs/", StringComparison.Ordinal)) - { - // Match on loose ref files by their canonical name. - possibleLooseFileMatches.Add(Path.Combine(this.CommonDirectory, objectish)); - skipObjectIdLookup = true; - } - else - { - // Look for simple names for branch or tag. - possibleLooseFileMatches.Add(Path.Combine(this.CommonDirectory, "refs", "heads", objectish)); - possibleLooseFileMatches.Add(Path.Combine(this.CommonDirectory, "refs", "tags", objectish)); - possibleLooseFileMatches.Add(Path.Combine(this.CommonDirectory, "refs", "remotes", objectish)); - } + int index; + int length; - if (possibleLooseFileMatches.FirstOrDefault(File.Exists) is string existingPath) - { - return GitObjectId.Parse(File.ReadAllText(existingPath).TrimEnd()); - } + // The alternates path is colon (:)-separated. On Windows, there may be full paths, such as + // C:/Users/username/source/repos/nbgv/.git, which also contain a colon. Because the colon + // can only appear at the second position, we skip the first two characters (e.g. C:) on Windows. + while (alternates.Length > skipCount) + { + index = alternates.Slice(skipCount).IndexOfAny((byte)':', (byte)'\n'); + length = index > 0 ? skipCount + index : alternates.Length; - // Match in packed-refs file. - string packedRefPath = Path.Combine(this.CommonDirectory, "packed-refs"); - if (File.Exists(packedRefPath)) - { - using var refReader = File.OpenText(packedRefPath); - string? line; - while ((line = refReader.ReadLine()) is object) - { - if (line.StartsWith("#", StringComparison.Ordinal)) - { - continue; - } + values.Add(GetString(alternates.Slice(0, length))); + alternates = index > 0 ? alternates.Slice(length + 1) : Span.Empty; + } - string refName = line.Substring(41); - if (string.Equals(refName, objectish, StringComparison.Ordinal)) - { - return GitObjectId.Parse(line.Substring(0, 40)); - } - else if (!objectish.StartsWith("refs/", StringComparison.Ordinal)) - { - // Not a canonical ref, so try heads and tags - if (string.Equals(refName, "refs/heads/" + objectish, StringComparison.Ordinal)) - { - return GitObjectId.Parse(line.Substring(0, 40)); - } - else if (string.Equals(refName, "refs/tags/" + objectish, StringComparison.Ordinal)) - { - return GitObjectId.Parse(line.Substring(0, 40)); - } - else if (string.Equals(refName, "refs/remotes/" + objectish, StringComparison.Ordinal)) - { - return GitObjectId.Parse(line.Substring(0, 40)); - } - } - } - } + return values; + } - if (skipObjectIdLookup) - { - return null; - } + /// + /// Shortens the object id. + /// + /// + /// The object Id to shorten. + /// + /// + /// The minimum string length. + /// + /// + /// The short object id. + /// + public string ShortenObjectId(GitObjectId objectId, int minimum) + { + string? sha = objectId.ToString(); + + for (int length = minimum; length < sha.Length; length += 2) + { + string? objectish = sha.Substring(0, length); - if (objectish.Length == 40) + if (this.Lookup(objectish) is not null) { - return GitObjectId.Parse(objectish); + return objectish; } + } - var possibleObjectIds = new List(); - if (objectish.Length > 2 && objectish.Length < 40) - { - // Search for _any_ object whose id starts with objectish in the object database - var directory = Path.Combine(this.ObjectDirectory, objectish.Substring(0, 2)); + return sha; + } - if (Directory.Exists(directory)) - { - var files = Directory.GetFiles(directory, $"{objectish.Substring(2)}*"); + /// + /// Returns the current HEAD as a reference (if available) or a Git object id. + /// + /// + /// The current HEAD as a reference (if available) or a Git object id. + /// + public object GetHeadAsReferenceOrSha() + { + using FileStream? stream = File.OpenRead(Path.Combine(this.GitDirectory, HeadFileName)); + return GitReferenceReader.ReadReference(stream); + } - foreach (var file in files) - { - var objectId = $"{objectish.Substring(0, 2)}{Path.GetFileName(file)}"; - possibleObjectIds.Add(GitObjectId.Parse(objectId)); - } - } + /// + /// Gets the object ID of the current HEAD. + /// + /// + /// The object ID of the current HEAD. + /// + public GitObjectId GetHeadCommitSha() + { + return this.Lookup("HEAD") ?? GitObjectId.Empty; + } - // Search for _any_ object whose id starts with objectish in the packfile - bool endsWithHalfByte = objectish.Length % 2 == 1; - if (endsWithHalfByte) - { - // Add one more character so hex can be converted to bytes. - // The bit length to be compared will not consider the last four bits. - objectish += "0"; - } + /// + /// Gets the current HEAD commit, if available. + /// + /// + /// A value indicating whether to populate the field. + /// + /// + /// The current HEAD commit, or if not available. + /// + public GitCommit? GetHeadCommit(bool readAuthor = false) + { + GitObjectId headCommitId = this.GetHeadCommitSha(); - if (objectish.Length <= 40 && objectish.Length % 2 == 0) - { - Span decodedHex = stackalloc byte[objectish.Length / 2]; - if (TryConvertHexStringToByteArray(objectish, decodedHex)) - { - foreach (var pack in this.packs.Value.Span) - { - var objectId = pack.Lookup(decodedHex, endsWithHalfByte); + if (headCommitId == GitObjectId.Empty) + { + return null; + } - // It's possible for the same object to be present in both the object database and the pack files, - // or in multiple pack files. - if (objectId is not null && !possibleObjectIds.Contains(objectId.Value)) - { - if (possibleObjectIds.Count > 0) - { - // If objectish already resolved to at least one object which is different from the current - // object id, objectish is not well-defined; so stop resolving and return null instead. - return null; - } - else - { - possibleObjectIds.Add(objectId.Value); - } - } - } - } - } - } + return this.GetCommit(headCommitId, readAuthor); + } - if (possibleObjectIds.Count == 1) + /// + /// Gets a commit by its Git object Id. + /// + /// + /// The Git object Id of the commit. + /// + /// + /// A value indicating whether to populate the field. + /// + /// + /// The requested commit. + /// + public GitCommit GetCommit(GitObjectId sha, bool readAuthor = false) + { + using Stream? stream = this.GetObjectBySha(sha, "commit"); + if (stream is null) + { + throw new GitException($"The commit {sha} was not found in this repository.") { ErrorCode = GitException.ErrorCodes.ObjectNotFound }; + } + + return GitCommitReader.Read(stream, sha, readAuthor); + } + + /// + /// Parses any committish to an object id. + /// + /// Any "objectish" string (e.g. commit ID (partial or full), branch name, tag name, or "HEAD"). + /// The object ID referenced by if found; otherwise . + public GitObjectId? Lookup(string objectish) + { + bool skipObjectIdLookup = false; + + if (objectish == "HEAD") + { + object? reference = this.GetHeadAsReferenceOrSha(); + if (reference is GitObjectId headObjectId) { - return possibleObjectIds[0]; + return headObjectId; } - return null; + objectish = (string)reference; } - /// - /// Gets a tree object by its Git object Id. - /// - /// - /// The Git object Id of the tree. - /// - /// - /// The requested tree. - /// - public GitTree GetTree(GitObjectId sha) + var possibleLooseFileMatches = new List(); + if (objectish.StartsWith("refs/", StringComparison.Ordinal)) { - using (Stream? stream = this.GetObjectBySha(sha, "tree")) - { - if (stream is null) - { - throw new GitException($"The tree {sha} was not found in this repository.") { ErrorCode = GitException.ErrorCodes.ObjectNotFound }; - } + // Match on loose ref files by their canonical name. + possibleLooseFileMatches.Add(Path.Combine(this.CommonDirectory, objectish)); + skipObjectIdLookup = true; + } + else + { + // Look for simple names for branch or tag. + possibleLooseFileMatches.Add(Path.Combine(this.CommonDirectory, "refs", "heads", objectish)); + possibleLooseFileMatches.Add(Path.Combine(this.CommonDirectory, "refs", "tags", objectish)); + possibleLooseFileMatches.Add(Path.Combine(this.CommonDirectory, "refs", "remotes", objectish)); + } - return GitTreeReader.Read(stream, sha); - } + if (possibleLooseFileMatches.FirstOrDefault(File.Exists) is string existingPath) + { + return GitObjectId.Parse(File.ReadAllText(existingPath).TrimEnd()); } - /// - /// Gets an entry in a git tree. - /// - /// - /// The Git object Id of the Git tree. - /// - /// - /// The name of the node in the Git tree. - /// - /// - /// The object Id of the requested entry. Returns if the entry - /// could not be found. - /// - public GitObjectId GetTreeEntry(GitObjectId treeId, ReadOnlySpan nodeName) - { - using (Stream? treeStream = this.GetObjectBySha(treeId, "tree")) + // Match in packed-refs file. + string packedRefPath = Path.Combine(this.CommonDirectory, "packed-refs"); + if (File.Exists(packedRefPath)) + { + using StreamReader? refReader = File.OpenText(packedRefPath); + string? line; + while ((line = refReader.ReadLine()) is object) { - if (treeStream is null) + if (line.StartsWith("#", StringComparison.Ordinal)) { - throw new GitException($"The tree {treeId} was not found in this repository.") { ErrorCode = GitException.ErrorCodes.ObjectNotFound }; + continue; } - return GitTreeStreamingReader.FindNode(treeStream, nodeName); + string refName = line.Substring(41); + if (string.Equals(refName, objectish, StringComparison.Ordinal)) + { + return GitObjectId.Parse(line.Substring(0, 40)); + } + else if (!objectish.StartsWith("refs/", StringComparison.Ordinal)) + { + // Not a canonical ref, so try heads and tags + if (string.Equals(refName, "refs/heads/" + objectish, StringComparison.Ordinal)) + { + return GitObjectId.Parse(line.Substring(0, 40)); + } + else if (string.Equals(refName, "refs/tags/" + objectish, StringComparison.Ordinal)) + { + return GitObjectId.Parse(line.Substring(0, 40)); + } + else if (string.Equals(refName, "refs/remotes/" + objectish, StringComparison.Ordinal)) + { + return GitObjectId.Parse(line.Substring(0, 40)); + } + } } } - /// - /// Gets a Git object by its Git object Id. - /// - /// - /// The Git object id of the object to retrieve. - /// - /// - /// The type of object to retrieve. - /// - /// - /// A which represents the requested object. - /// - /// - /// The requested object could not be found. - /// - /// - /// As a special case, a value will be returned for - /// . - /// - public Stream? GetObjectBySha(GitObjectId sha, string objectType) - { - if (sha == GitObjectId.Empty) - { - return null; - } + if (skipObjectIdLookup) + { + return null; + } - if (this.TryGetObjectBySha(sha, objectType, out Stream? value)) - { - return value; - } - else - { - throw new GitException($"An {objectType} object with SHA {sha} could not be found.") { ErrorCode = GitException.ErrorCodes.ObjectNotFound }; - } + if (objectish.Length == 40) + { + return GitObjectId.Parse(objectish); } - /// - /// Gets a Git object by its Git object Id. - /// - /// - /// The Git object id of the object to retrieve. - /// - /// - /// The type of object to retrieve. - /// - /// - /// An output parameter which retrieves the requested Git object. - /// - /// - /// if the object could be found; otherwise, - /// . - /// - public bool TryGetObjectBySha(GitObjectId sha, string objectType, out Stream? value) + var possibleObjectIds = new List(); + if (objectish.Length > 2 && objectish.Length < 40) { -#if DEBUG - if (!this.histogram.TryAdd(sha, 1)) - { - this.histogram[sha] += 1; - } -#endif + // Search for _any_ object whose id starts with objectish in the object database + string? directory = Path.Combine(this.ObjectDirectory, objectish.Substring(0, 2)); - foreach (var pack in this.packs.Value.Span) + if (Directory.Exists(directory)) { - if (pack.TryGetObject(sha, objectType, out value)) + string[]? files = Directory.GetFiles(directory, $"{objectish.Substring(2)}*"); + + foreach (string? file in files) { - return true; + string? objectId = $"{objectish.Substring(0, 2)}{Path.GetFileName(file)}"; + possibleObjectIds.Add(GitObjectId.Parse(objectId)); } } - if (this.TryGetObjectByPath(sha, objectType, out value)) + // Search for _any_ object whose id starts with objectish in the packfile + bool endsWithHalfByte = objectish.Length % 2 == 1; + if (endsWithHalfByte) { - return true; + // Add one more character so hex can be converted to bytes. + // The bit length to be compared will not consider the last four bits. + objectish += "0"; } - foreach (var alternate in this.alternates) + if (objectish.Length <= 40 && objectish.Length % 2 == 0) { - if (alternate.TryGetObjectBySha(sha, objectType, out value)) + Span decodedHex = stackalloc byte[objectish.Length / 2]; + if (TryConvertHexStringToByteArray(objectish, decodedHex)) { - return true; + foreach (GitPack? pack in this.packs.Value.Span) + { + GitObjectId? objectId = pack.Lookup(decodedHex, endsWithHalfByte); + + // It's possible for the same object to be present in both the object database and the pack files, + // or in multiple pack files. + if (objectId is not null && !possibleObjectIds.Contains(objectId.Value)) + { + if (possibleObjectIds.Count > 0) + { + // If objectish already resolved to at least one object which is different from the current + // object id, objectish is not well-defined; so stop resolving and return null instead. + return null; + } + else + { + possibleObjectIds.Add(objectId.Value); + } + } + } } } - - value = null; - return false; } - /// - /// Gets cache usage statistics. - /// - /// - /// A which represents the cache usage statistics. - /// - public string GetCacheStatistics() + if (possibleObjectIds.Count == 1) { - StringBuilder builder = new StringBuilder(); + return possibleObjectIds[0]; + } -#if DEBUG - int histogramCount = 25; + return null; + } - builder.AppendLine("Overall repository:"); - builder.AppendLine($"Top {histogramCount} / {this.histogram.Count} items:"); + /// + /// Gets a tree object by its Git object Id. + /// + /// + /// The Git object Id of the tree. + /// + /// + /// The requested tree. + /// + public GitTree GetTree(GitObjectId sha) + { + using Stream? stream = this.GetObjectBySha(sha, "tree"); + if (stream is null) + { + throw new GitException($"The tree {sha} was not found in this repository.") { ErrorCode = GitException.ErrorCodes.ObjectNotFound }; + } - foreach (var item in this.histogram.OrderByDescending(v => v.Value).Take(25)) - { - builder.AppendLine($" {item.Key}: {item.Value}"); - } + return GitTreeReader.Read(stream, sha); + } - builder.AppendLine(); -#endif + /// + /// Gets an entry in a git tree. + /// + /// + /// The Git object Id of the Git tree. + /// + /// + /// The name of the node in the Git tree. + /// + /// + /// The object Id of the requested entry. Returns if the entry + /// could not be found. + /// + public GitObjectId GetTreeEntry(GitObjectId treeId, ReadOnlySpan nodeName) + { + using Stream? treeStream = this.GetObjectBySha(treeId, "tree"); + if (treeStream is null) + { + throw new GitException($"The tree {treeId} was not found in this repository.") { ErrorCode = GitException.ErrorCodes.ObjectNotFound }; + } - foreach (var pack in this.packs.Value.Span) - { - pack.GetCacheStatistics(builder); - } + return GitTreeStreamingReader.FindNode(treeStream, nodeName); + } - return builder.ToString(); + /// + /// Gets a Git object by its Git object Id. + /// + /// + /// The Git object id of the object to retrieve. + /// + /// + /// The type of object to retrieve. + /// + /// + /// A which represents the requested object. + /// + /// + /// The requested object could not be found. + /// + /// + /// As a special case, a value will be returned for + /// . + /// + public Stream? GetObjectBySha(GitObjectId sha, string objectType) + { + if (sha == GitObjectId.Empty) + { + return null; } - /// - public override string ToString() + if (this.TryGetObjectBySha(sha, objectType, out Stream? value)) { - return $"Git Repository: {this.WorkingDirectory}"; + return value; } - - /// - public void Dispose() + else { - if (this.packs.IsValueCreated) - { - foreach (var pack in this.packs.Value.Span) - { - pack.Dispose(); - } - } + throw new GitException($"An {objectType} object with SHA {sha} could not be found.") { ErrorCode = GitException.ErrorCodes.ObjectNotFound }; } + } - private bool TryGetObjectByPath(GitObjectId sha, string objectType, [NotNullWhen(true)] out Stream? value) + /// + /// Gets a Git object by its Git object Id. + /// + /// + /// The Git object id of the object to retrieve. + /// + /// + /// The type of object to retrieve. + /// + /// + /// An output parameter which retrieves the requested Git object. + /// + /// + /// if the object could be found; otherwise, + /// . + /// + public bool TryGetObjectBySha(GitObjectId sha, string objectType, out Stream? value) + { +#if DEBUG + if (!this.histogram.TryAdd(sha, 1)) { - sha.CopyAsHex(0, 1, this.objectPathBuffer.AsSpan(this.ObjectDirectory.Length + 1, 2)); - sha.CopyAsHex(1, 19, this.objectPathBuffer.AsSpan(this.ObjectDirectory.Length + 1 + 2 + 1)); - - if (!FileHelpers.TryOpen(this.objectPathBuffer, out var compressedFile)) - { - value = null; - return false; - } - - var objectStream = new GitObjectStream(compressedFile!, objectType); + this.histogram[sha] += 1; + } +#endif - if (string.CompareOrdinal(objectStream.ObjectType, objectType) != 0) + foreach (GitPack? pack in this.packs.Value.Span) + { + if (pack.TryGetObject(sha, objectType, out value)) { - throw new GitException($"Got a {objectStream.ObjectType} instead of a {objectType} when opening object {sha}"); + return true; } + } - value = objectStream; + if (this.TryGetObjectByPath(sha, objectType, out value)) + { return true; } - private ReadOnlyMemory LoadPacks() + foreach (GitRepository? alternate in this.alternates) { - var packDirectory = Path.Combine(this.ObjectDirectory, "pack/"); - - if (!Directory.Exists(packDirectory)) + if (alternate.TryGetObjectBySha(sha, objectType, out value)) { - return Array.Empty(); + return true; } + } - var indexFiles = Directory.GetFiles(packDirectory, "*.idx"); - var packs = new GitPack[indexFiles.Length]; - int addCount = 0; + value = null; + return false; + } - for (int i = 0; i < indexFiles.Length; i++) - { - var name = Path.GetFileNameWithoutExtension(indexFiles[i]); - var indexPath = Path.Combine(this.ObjectDirectory, "pack", $"{name}.idx"); - var packPath = Path.Combine(this.ObjectDirectory, "pack", $"{name}.pack"); + /// + /// Gets cache usage statistics. + /// + /// + /// A which represents the cache usage statistics. + /// + public string GetCacheStatistics() + { + var builder = new StringBuilder(); - // Only proceed if both the packfile and index file exist. - if (File.Exists(packPath)) - { - packs[addCount++] = new GitPack(this.GetObjectBySha, indexPath, packPath); - } - } +#if DEBUG + int histogramCount = 25; + + builder.AppendLine("Overall repository:"); + builder.AppendLine($"Top {histogramCount} / {this.histogram.Count} items:"); - return packs.AsMemory(0, addCount); + foreach (KeyValuePair item in this.histogram.OrderByDescending(v => v.Value).Take(25)) + { + builder.AppendLine($" {item.Key}: {item.Value}"); } - private static string TrimEndingDirectorySeparator(string path) + builder.AppendLine(); +#endif + + foreach (GitPack? pack in this.packs.Value.Span) { -#if NETSTANDARD2_0 - if (string.IsNullOrEmpty(path) || path.Length == 1) - { - return path; - } + pack.GetCacheStatistics(builder); + } - var last = path[path.Length - 1]; + return builder.ToString(); + } + + /// + public override string ToString() + { + return $"Git Repository: {this.WorkingDirectory}"; + } - if (last == Path.DirectorySeparatorChar || last == Path.AltDirectorySeparatorChar) + /// + public void Dispose() + { + if (this.packs.IsValueCreated) + { + foreach (GitPack? pack in this.packs.Value.Span) { - return path.Substring(0, path.Length - 1); + pack.Dispose(); } + } + } + private static string TrimEndingDirectorySeparator(string path) + { +#if NETSTANDARD2_0 + if (string.IsNullOrEmpty(path) || path.Length == 1) + { return path; + } + + char last = path[path.Length - 1]; + + if (last == Path.DirectorySeparatorChar || last == Path.AltDirectorySeparatorChar) + { + return path.Substring(0, path.Length - 1); + } + + return path; #else - return Path.TrimEndingDirectorySeparator(path); + return Path.TrimEndingDirectorySeparator(path); #endif + } + + private static bool TryConvertHexStringToByteArray(string hexString, Span data) + { + // https://stackoverflow.com/questions/321370/how-can-i-convert-a-hex-string-to-a-byte-array + if (hexString.Length % 2 != 0) + { + data = null; + return false; } - private static bool TryConvertHexStringToByteArray(string hexString, Span data) + Requires.Argument(data.Length == hexString.Length / 2, nameof(data), "Length must be exactly half that of " + nameof(hexString) + "."); + for (int index = 0; index < data.Length; index++) { - // https://stackoverflow.com/questions/321370/how-can-i-convert-a-hex-string-to-a-byte-array - if (hexString.Length % 2 != 0) +#if !NETSTANDARD2_0 + ReadOnlySpan byteValue = hexString.AsSpan(index * 2, 2); + if (!byte.TryParse(byteValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out data[index])) { - data = null; return false; } - - Requires.Argument(data.Length == hexString.Length / 2, nameof(data), "Length must be exactly half that of " + nameof(hexString) + "."); - for (int index = 0; index < data.Length; index++) - { -#if !NETSTANDARD2_0 - ReadOnlySpan byteValue = hexString.AsSpan(index * 2, 2); - if (!byte.TryParse(byteValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out data[index])) - { - return false; - } #else - string byteValue = hexString.Substring(index * 2, 2); - if (!byte.TryParse(byteValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out data[index])) - { - return false; - } -#endif + string byteValue = hexString.Substring(index * 2, 2); + if (!byte.TryParse(byteValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out data[index])) + { + return false; } +#endif + } - return true; + return true; + } + + private bool TryGetObjectByPath(GitObjectId sha, string objectType, [NotNullWhen(true)] out Stream? value) + { + sha.CopyAsHex(0, 1, this.objectPathBuffer.AsSpan(this.ObjectDirectory.Length + 1, 2)); + sha.CopyAsHex(1, 19, this.objectPathBuffer.AsSpan(this.ObjectDirectory.Length + 1 + 2 + 1)); + + if (!FileHelpers.TryOpen(this.objectPathBuffer, out FileStream? compressedFile)) + { + value = null; + return false; } - /// - /// Decodes a sequence of bytes from the specified byte array into a . - /// - /// - /// The span containing the sequence of UTF-8 bytes to decode. - /// - /// - /// A that contains the results of decoding the specified sequence of bytes. - /// - public static unsafe string GetString(ReadOnlySpan bytes) + var objectStream = new GitObjectStream(compressedFile!, objectType); + + if (string.CompareOrdinal(objectStream.ObjectType, objectType) != 0) { - fixed (byte* pBytes = bytes) - { - return Encoding.GetString(pBytes, bytes.Length); - } + throw new GitException($"Got a {objectStream.ObjectType} instead of a {objectType} when opening object {sha}"); } - /// - /// Parses the contents of the alternates file, and returns a list of (relative) paths to the alternate object directories. - /// - /// - /// The contents of the alternates files. - /// - /// - /// A list of (relative) paths to the alternate object directories. - /// - public static List ParseAlternates(ReadOnlySpan alternates) - => ParseAlternates(alternates, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 2 : 0); - - /// - /// Parses the contents of the alternates file, and returns a list of (relative) paths to the alternate object directories. - /// - /// - /// The contents of the alternates files. - /// - /// - /// The number of bytes to skip in the span when looking for a delimiter. - /// - /// - /// A list of (relative) paths to the alternate object directories. - /// - public static List ParseAlternates(ReadOnlySpan alternates, int skipCount) - { - List values = new List(); - - int index; - int length; - - // The alternates path is colon (:)-separated. On Windows, there may be full paths, such as - // C:/Users/username/source/repos/nbgv/.git, which also contain a colon. Because the colon - // can only appear at the second position, we skip the first two characters (e.g. C:) on Windows. - while (alternates.Length > skipCount) - { - index = alternates.Slice(skipCount).IndexOfAny((byte)':', (byte)'\n'); - length = index > 0 ? skipCount + index : alternates.Length; + value = objectStream; + return true; + } - values.Add(GetString(alternates.Slice(0, length))); - alternates = index > 0 ? alternates.Slice(length + 1) : Span.Empty; - } + private ReadOnlyMemory LoadPacks() + { + string? packDirectory = Path.Combine(this.ObjectDirectory, "pack/"); + + if (!Directory.Exists(packDirectory)) + { + return Array.Empty(); + } + + string[]? indexFiles = Directory.GetFiles(packDirectory, "*.idx"); + var packs = new GitPack[indexFiles.Length]; + int addCount = 0; - return values; + for (int i = 0; i < indexFiles.Length; i++) + { + string? name = Path.GetFileNameWithoutExtension(indexFiles[i]); + string? indexPath = Path.Combine(this.ObjectDirectory, "pack", $"{name}.idx"); + string? packPath = Path.Combine(this.ObjectDirectory, "pack", $"{name}.pack"); + + // Only proceed if both the packfile and index file exist. + if (File.Exists(packPath)) + { + packs[addCount++] = new GitPack(this.GetObjectBySha, indexPath, packPath); + } } + + return packs.AsMemory(0, addCount); } } diff --git a/src/NerdBank.GitVersioning/ManagedGit/GitSignature.cs b/src/NerdBank.GitVersioning/ManagedGit/GitSignature.cs index 53d56759..e3d65410 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/GitSignature.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/GitSignature.cs @@ -1,27 +1,27 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; +#nullable enable -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +/// +/// Represents the signature of a Git committer or author. +/// +public struct GitSignature { /// - /// Represents the signature of a Git committer or author. + /// Gets or sets the name of the committer or author. /// - public struct GitSignature - { - /// - /// Gets or sets the name of the committer or author. - /// - public string Name { get; set; } + public string Name { get; set; } - /// - /// Gets or sets the e-mail address of the commiter or author. - /// - public string Email { get; set; } + /// + /// Gets or sets the e-mail address of the commiter or author. + /// + public string Email { get; set; } - /// - /// Gets or sets the date and time at which the commit was made. - /// - public DateTimeOffset Date { get; set; } - } + /// + /// Gets or sets the date and time at which the commit was made. + /// + public DateTimeOffset Date { get; set; } } diff --git a/src/NerdBank.GitVersioning/ManagedGit/GitTree.cs b/src/NerdBank.GitVersioning/ManagedGit/GitTree.cs index 6635d630..adeb7968 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/GitTree.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/GitTree.cs @@ -1,33 +1,33 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System.Collections.Generic; +#nullable enable -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +/// +/// Represents a git tree. +/// +public class GitTree { /// - /// Represents a git tree. + /// Gets an empty . /// - public class GitTree - { - /// - /// Gets an empty . - /// - public static GitTree Empty { get; } = new GitTree(); + public static GitTree Empty { get; } = new GitTree(); - /// - /// The Git object Id of this . - /// - public GitObjectId Sha { get; set; } + /// + /// Gets or sets the Git object Id of this . + /// + public GitObjectId Sha { get; set; } - /// - /// Gets a dictionary which contains all entries in the current tree, accessible by name. - /// - public Dictionary Children { get; } = new Dictionary(); + /// + /// Gets a dictionary which contains all entries in the current tree, accessible by name. + /// + public Dictionary Children { get; } = new Dictionary(); - /// - public override string ToString() - { - return $"Git tree: {this.Sha}"; - } + /// + public override string ToString() + { + return $"Git tree: {this.Sha}"; } } diff --git a/src/NerdBank.GitVersioning/ManagedGit/GitTreeEntry.cs b/src/NerdBank.GitVersioning/ManagedGit/GitTreeEntry.cs index a213b268..57590dc7 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/GitTreeEntry.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/GitTreeEntry.cs @@ -1,50 +1,52 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Nerdbank.GitVersioning.ManagedGit +#nullable enable + +namespace Nerdbank.GitVersioning.ManagedGit; + +/// +/// Represents an individual entry in the Git tree. +/// +public class GitTreeEntry { /// - /// Represents an individual entry in the Git tree. + /// Initializes a new instance of the class. /// - public class GitTreeEntry + /// + /// The name of the entry. + /// + /// + /// A vaolue indicating whether the current entry is a file. + /// + /// + /// The Git object Id of the blob or tree of the current entry. + /// + public GitTreeEntry(string name, bool isFile, GitObjectId sha) { - /// - /// Initializes a new instance of the class. - /// - /// - /// The name of the entry. - /// - /// - /// A vaolue indicating whether the current entry is a file. - /// - /// - /// The Git object Id of the blob or tree of the current entry. - /// - public GitTreeEntry(string name, bool isFile, GitObjectId sha) - { - this.Name = name; - this.IsFile = isFile; - this.Sha = sha; - } + this.Name = name; + this.IsFile = isFile; + this.Sha = sha; + } - /// - /// Gets the name of the entry. - /// - public string Name { get; } + /// + /// Gets the name of the entry. + /// + public string Name { get; } - /// - /// Gets a value indicating whether the current entry is a file. - /// - public bool IsFile { get; } + /// + /// Gets a value indicating whether the current entry is a file. + /// + public bool IsFile { get; } - /// - /// Gets the Git object Id of the blob or tree of the current entry. - /// - public GitObjectId Sha { get; } + /// + /// Gets the Git object Id of the blob or tree of the current entry. + /// + public GitObjectId Sha { get; } - /// - public override string ToString() - { - return this.Name; - } + /// + public override string ToString() + { + return this.Name; } } diff --git a/src/NerdBank.GitVersioning/ManagedGit/GitTreeReader.cs b/src/NerdBank.GitVersioning/ManagedGit/GitTreeReader.cs index 7baf8451..d230a2e4 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/GitTreeReader.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/GitTreeReader.cs @@ -1,57 +1,57 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable -using System; using System.Buffers; -using System.IO; -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +internal static class GitTreeReader { - internal static class GitTreeReader + public static GitTree Read(Stream stream, GitObjectId objectId) { - public static GitTree Read(Stream stream, GitObjectId objectId) - { - byte[] buffer = ArrayPool.Shared.Rent((int)stream.Length); + byte[] buffer = ArrayPool.Shared.Rent((int)stream.Length); #if DEBUG - Array.Clear(buffer, 0, buffer.Length); + Array.Clear(buffer, 0, buffer.Length); #endif - GitTree value = new GitTree() - { - Sha = objectId, - }; + var value = new GitTree() + { + Sha = objectId, + }; - try - { - Span contents = buffer.AsSpan(0, (int)stream.Length); - stream.ReadAll(contents); + try + { + Span contents = buffer.AsSpan(0, (int)stream.Length); + stream.ReadAll(contents); - while (contents.Length > 0) - { - // Format: [mode] [file/ folder name]\0[SHA - 1 of referencing blob or tree] - // Mode is either 6-bytes long (directory) or 7-bytes long (file). - // If the entry is a file, the first byte is '1' - var fileNameEnds = contents.IndexOf((byte)0); - bool isFile = contents[0] == (byte)'1'; - var modeLength = isFile ? 7 : 6; + while (contents.Length > 0) + { + // Format: [mode] [file/ folder name]\0[SHA - 1 of referencing blob or tree] + // Mode is either 6-bytes long (directory) or 7-bytes long (file). + // If the entry is a file, the first byte is '1' + int fileNameEnds = contents.IndexOf((byte)0); + bool isFile = contents[0] == (byte)'1'; + int modeLength = isFile ? 7 : 6; - var currentName = contents.Slice(modeLength, fileNameEnds - modeLength); - var currentObjectId = GitObjectId.Parse(contents.Slice(fileNameEnds + 1, 20)); + Span currentName = contents.Slice(modeLength, fileNameEnds - modeLength); + var currentObjectId = GitObjectId.Parse(contents.Slice(fileNameEnds + 1, 20)); - var name = GitRepository.GetString(currentName); + string? name = GitRepository.GetString(currentName); - value.Children.Add( - name, - new GitTreeEntry(name, isFile, currentObjectId)); + value.Children.Add( + name, + new GitTreeEntry(name, isFile, currentObjectId)); - contents = contents.Slice(fileNameEnds + 1 + 20); - } - } - finally - { - ArrayPool.Shared.Return(buffer); + contents = contents.Slice(fileNameEnds + 1 + 20); } - - return value; } + finally + { + ArrayPool.Shared.Return(buffer); + } + + return value; } } diff --git a/src/NerdBank.GitVersioning/ManagedGit/GitTreeStreamingReader.cs b/src/NerdBank.GitVersioning/ManagedGit/GitTreeStreamingReader.cs index fe0db93a..6799b7a0 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/GitTreeStreamingReader.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/GitTreeStreamingReader.cs @@ -1,62 +1,62 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable -using System; using System.Buffers; -using System.IO; -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +/// +/// Reads git tree objects. +/// +public class GitTreeStreamingReader { /// - /// Reads git tree objects. + /// Finds a specific node in a git tree. /// - public class GitTreeStreamingReader + /// + /// A which represents the git tree. + /// + /// + /// The name of the node to find, in it UTF-8 representation. + /// + /// + /// The of the requested node. + /// + public static GitObjectId FindNode(Stream stream, ReadOnlySpan name) { - /// - /// Finds a specific node in a git tree. - /// - /// - /// A which represents the git tree. - /// - /// - /// The name of the node to find, in it UTF-8 representation. - /// - /// - /// The of the requested node. - /// - public static GitObjectId FindNode(Stream stream, ReadOnlySpan name) - { - byte[] buffer = ArrayPool.Shared.Rent((int)stream.Length); - Span contents = new Span(buffer, 0, (int)stream.Length); + byte[] buffer = ArrayPool.Shared.Rent((int)stream.Length); + var contents = new Span(buffer, 0, (int)stream.Length); + + stream.ReadAll(contents); + + GitObjectId value = GitObjectId.Empty; - stream.ReadAll(contents); + while (contents.Length > 0) + { + // Format: [mode] [file/ folder name]\0[SHA - 1 of referencing blob or tree] + // Mode is either 6-bytes long (directory) or 7-bytes long (file). + // If the entry is a file, the first byte is '1' + int fileNameEnds = contents.IndexOf((byte)0); + bool isFile = contents[0] == (byte)'1'; + int modeLength = isFile ? 7 : 6; - GitObjectId value = GitObjectId.Empty; + Span currentName = contents.Slice(modeLength, fileNameEnds - modeLength); - while (contents.Length > 0) + if (currentName.SequenceEqual(name)) + { + value = GitObjectId.Parse(contents.Slice(fileNameEnds + 1, 20)); + break; + } + else { - // Format: [mode] [file/ folder name]\0[SHA - 1 of referencing blob or tree] - // Mode is either 6-bytes long (directory) or 7-bytes long (file). - // If the entry is a file, the first byte is '1' - var fileNameEnds = contents.IndexOf((byte)0); - bool isFile = contents[0] == (byte)'1'; - var modeLength = isFile ? 7 : 6; - - var currentName = contents.Slice(modeLength, fileNameEnds - modeLength); - - if (currentName.SequenceEqual(name)) - { - value = GitObjectId.Parse(contents.Slice(fileNameEnds + 1, 20)); - break; - } - else - { - contents = contents.Slice(fileNameEnds + 1 + 20); - } + contents = contents.Slice(fileNameEnds + 1 + 20); } + } - ArrayPool.Shared.Return(buffer); + ArrayPool.Shared.Return(buffer); - return value; - } + return value; } } diff --git a/src/NerdBank.GitVersioning/ManagedGit/MemoryMappedStream.cs b/src/NerdBank.GitVersioning/ManagedGit/MemoryMappedStream.cs index 8128dbf4..cecef5ed 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/MemoryMappedStream.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/MemoryMappedStream.cs @@ -1,156 +1,156 @@ -using System; -using System.IO; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + using System.IO.MemoryMappedFiles; -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +/// +/// Provides read-only, seekable access to a . +/// +public unsafe class MemoryMappedStream : Stream { + private readonly MemoryMappedViewAccessor accessor; + private readonly long length; + private readonly byte* ptr; + private long position; + private bool disposed; + /// - /// Provides read-only, seekable access to a . + /// Initializes a new instance of the class. /// - public unsafe class MemoryMappedStream : Stream + /// + /// The accessor to the memory mapped stream. + /// + public MemoryMappedStream(MemoryMappedViewAccessor accessor) { - private readonly MemoryMappedViewAccessor accessor; - private readonly long length; - private long position; - private byte* ptr; - private bool disposed; - - /// - /// Initializes a new instance of the class. - /// - /// - /// The accessor to the memory mapped stream. - /// - public MemoryMappedStream(MemoryMappedViewAccessor accessor) - { - this.accessor = accessor ?? throw new ArgumentNullException(nameof(accessor)); - this.accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref this.ptr); - this.length = this.accessor.Capacity; - } + this.accessor = accessor ?? throw new ArgumentNullException(nameof(accessor)); + this.accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref this.ptr); + this.length = this.accessor.Capacity; + } - /// - public override bool CanRead => true; + /// + public override bool CanRead => true; - /// - public override bool CanSeek => true; + /// + public override bool CanSeek => true; - /// - public override bool CanWrite => false; + /// + public override bool CanWrite => false; - /// - public override long Length => this.length; + /// + public override long Length => this.length; - /// - public override long Position + /// + public override long Position + { + get => this.position; + set { - get => this.position; - set - { - this.position = (int)value; - } + this.position = (int)value; } + } - /// - public override void Flush() - { - } + /// + public override void Flush() + { + } - /// - public override int Read(byte[] buffer, int offset, int count) + /// + public override int Read(byte[] buffer, int offset, int count) + { + if (this.disposed) { - if (this.disposed) - { - throw new ObjectDisposedException(nameof(MemoryMappedStream)); - } + throw new ObjectDisposedException(nameof(MemoryMappedStream)); + } - int read = (int)Math.Min(count, this.length - this.position); + int read = (int)Math.Min(count, this.length - this.position); - new Span(this.ptr + this.position, read) - .CopyTo(buffer.AsSpan(offset, count)); + new Span(this.ptr + this.position, read) + .CopyTo(buffer.AsSpan(offset, count)); - this.position += read; - return read; - } + this.position += read; + return read; + } #if !NETSTANDARD2_0 - /// - public override int Read(Span buffer) + /// + public override int Read(Span buffer) + { + if (this.disposed) { - if (this.disposed) - { - throw new ObjectDisposedException(nameof(MemoryMappedStream)); - } - - int read = (int)Math.Min(buffer.Length, this.length - this.position); - - new Span(this.ptr + this.position, read) - .CopyTo(buffer); - - this.position += read; - return read; + throw new ObjectDisposedException(nameof(MemoryMappedStream)); } + + int read = (int)Math.Min(buffer.Length, this.length - this.position); + + new Span(this.ptr + this.position, read) + .CopyTo(buffer); + + this.position += read; + return read; + } #endif - /// - public override long Seek(long offset, SeekOrigin origin) + /// + public override long Seek(long offset, SeekOrigin origin) + { + if (this.disposed) { - if (this.disposed) - { - throw new ObjectDisposedException(nameof(MemoryMappedStream)); - } - - long newPosition = this.position; - - switch (origin) - { - case SeekOrigin.Begin: - newPosition = offset; - break; - - case SeekOrigin.Current: - newPosition += offset; - break; - - case SeekOrigin.End: - throw new NotSupportedException(); - } - - if (newPosition > this.length) - { - newPosition = this.length; - } - - if (newPosition < 0) - { - throw new IOException("Attempted to seek before the start or beyond the end of the stream."); - } - - this.position = newPosition; - return this.position; + throw new ObjectDisposedException(nameof(MemoryMappedStream)); } - /// - public override void SetLength(long value) + long newPosition = this.position; + + switch (origin) { - throw new NotSupportedException(); + case SeekOrigin.Begin: + newPosition = offset; + break; + + case SeekOrigin.Current: + newPosition += offset; + break; + + case SeekOrigin.End: + throw new NotSupportedException(); } - /// - public override void Write(byte[] buffer, int offset, int count) + if (newPosition > this.length) { - throw new NotSupportedException(); + newPosition = this.length; } - /// - protected override void Dispose(bool disposing) + if (newPosition < 0) { - if (disposing) - { - this.accessor.SafeMemoryMappedViewHandle.ReleasePointer(); - this.disposed = true; - } + throw new IOException("Attempted to seek before the start or beyond the end of the stream."); + } + + this.position = newPosition; + return this.position; + } - base.Dispose(disposing); + /// + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.accessor.SafeMemoryMappedViewHandle.ReleasePointer(); + this.disposed = true; } + + base.Dispose(disposing); } } diff --git a/src/NerdBank.GitVersioning/ManagedGit/StreamExtensions.cs b/src/NerdBank.GitVersioning/ManagedGit/StreamExtensions.cs index df84ebdd..87ca6a1f 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/StreamExtensions.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/StreamExtensions.cs @@ -1,181 +1,181 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable -using System; using System.Buffers; -using System.IO; -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +/// +/// Provides extension methods for the class. +/// +public static class StreamExtensions { /// - /// Provides extension methods for the class. + /// Reads data from a to fill a given buffer. /// - public static class StreamExtensions + /// + /// The from which to read data. + /// + /// + /// A buffer into which to store the data read. + /// + /// Thrown when the stream runs out of data before could be filled. + public static void ReadAll(this Stream stream, Span buffer) { - /// - /// Reads data from a to fill a given buffer. - /// - /// - /// The from which to read data. - /// - /// - /// A buffer into which to store the data read. - /// - /// Thrown when the stream runs out of data before could be filled. - public static void ReadAll(this Stream stream, Span buffer) + if (buffer.Length == 0) { - if (buffer.Length == 0) - { - return; - } - - int totalBytesRead = 0; - while (totalBytesRead < buffer.Length) - { - int bytesRead = stream.Read(buffer.Slice(totalBytesRead)); - if (bytesRead == 0) - { - throw new EndOfStreamException(); - } - - totalBytesRead += bytesRead; - } + return; } - /// - /// Reads an variable-length integer off a . - /// - /// - /// The stream off which to read the variable-length integer. - /// - /// - /// The requested value. - /// - /// Thrown when the stream runs out of data before the integer could be read. - public static int ReadMbsInt(this Stream stream) + int totalBytesRead = 0; + while (totalBytesRead < buffer.Length) { - int value = 0; - int currentBit = 0; - int read; - - while (true) + int bytesRead = stream.Read(buffer.Slice(totalBytesRead)); + if (bytesRead == 0) { - read = stream.ReadByte(); - if (read == -1) - { - throw new EndOfStreamException(); - } - - value |= (read & 0b_0111_1111) << currentBit; - currentBit += 7; - - if (read < 128) - { - break; - } + throw new EndOfStreamException(); } - return value; + totalBytesRead += bytesRead; } + } -#if NETSTANDARD2_0 - /// - /// Reads a sequence of bytes from the current stream and advances the position within the stream by - /// the number of bytes read. - /// - /// - /// The from which to read the data. - /// - /// - /// A region of memory. When this method returns, the contents of this region are replaced by the bytes - /// read from the current source. - /// - /// - /// The total number of bytes read into the buffer. This can be less than the number of bytes allocated - /// in the buffer if that many bytes are not currently available, or zero (0) if the end of the stream - /// has been reached. - /// - public static int Read(this Stream stream, Span span) - { - byte[]? buffer = null; + /// + /// Reads an variable-length integer off a . + /// + /// + /// The stream off which to read the variable-length integer. + /// + /// + /// The requested value. + /// + /// Thrown when the stream runs out of data before the integer could be read. + public static int ReadMbsInt(this Stream stream) + { + int value = 0; + int currentBit = 0; + int read; - try + while (true) + { + read = stream.ReadByte(); + if (read == -1) { - buffer = ArrayPool.Shared.Rent(span.Length); - int read = stream.Read(buffer, 0, span.Length); - - buffer.AsSpan(0, read).CopyTo(span); - return read; + throw new EndOfStreamException(); } - finally + + value |= (read & 0b_0111_1111) << currentBit; + currentBit += 7; + + if (read < 128) { - ArrayPool.Shared.Return(buffer); + break; } } - /// - /// Writes a sequence of bytes to the current stream and advances the current position within this stream - /// by the number of bytes written. - /// - /// - /// The to which to write the data. - /// - /// - /// A region of memory. This method copies the contents of this region to the current stream. - /// - public static void Write(this Stream stream, Span span) - { - byte[]? buffer = null; + return value; + } - try - { - buffer = ArrayPool.Shared.Rent(span.Length); - span.CopyTo(buffer.AsSpan(0, span.Length)); +#if NETSTANDARD2_0 + /// + /// Reads a sequence of bytes from the current stream and advances the position within the stream by + /// the number of bytes read. + /// + /// + /// The from which to read the data. + /// + /// + /// A region of memory. When this method returns, the contents of this region are replaced by the bytes + /// read from the current source. + /// + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes allocated + /// in the buffer if that many bytes are not currently available, or zero (0) if the end of the stream + /// has been reached. + /// + public static int Read(this Stream stream, Span span) + { + byte[]? buffer = null; - stream.Write(buffer, 0, span.Length); - } - finally - { - ArrayPool.Shared.Return(buffer); - } + try + { + buffer = ArrayPool.Shared.Rent(span.Length); + int read = stream.Read(buffer, 0, span.Length); + + buffer.AsSpan(0, read).CopyTo(span); + return read; } + finally + { + ArrayPool.Shared.Return(buffer); + } + } - internal static bool TryAdd(this System.Collections.Generic.IDictionary dictionary, TKey key, TValue value) + /// + /// Writes a sequence of bytes to the current stream and advances the current position within this stream + /// by the number of bytes written. + /// + /// + /// The to which to write the data. + /// + /// + /// A region of memory. This method copies the contents of this region to the current stream. + /// + public static void Write(this Stream stream, Span span) + { + byte[]? buffer = null; + + try { - if (dictionary.ContainsKey(key)) - { - return false; - } + buffer = ArrayPool.Shared.Rent(span.Length); + span.CopyTo(buffer.AsSpan(0, span.Length)); + + stream.Write(buffer, 0, span.Length); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } - dictionary.Add(key, value); - return true; + internal static bool TryAdd(this System.Collections.Generic.IDictionary dictionary, TKey key, TValue value) + { + if (dictionary.ContainsKey(key)) + { + return false; } + + dictionary.Add(key, value); + return true; + } #endif - /// - /// Reads the specified number of bytes from a stream, or until the end of the stream. - /// - /// The stream to read from. - /// The number of bytes to be read. - /// The stream to copy the read bytes to, if required. - /// The number of bytes actually read. This will be less than only if the end of is reached. - internal static int ReadExactly(this Stream readFrom, int length, Stream? copyTo = null) + /// + /// Reads the specified number of bytes from a stream, or until the end of the stream. + /// + /// The stream to read from. + /// The number of bytes to be read. + /// The stream to copy the read bytes to, if required. + /// The number of bytes actually read. This will be less than only if the end of is reached. + internal static int ReadExactly(this Stream readFrom, int length, Stream? copyTo = null) + { + int bytesRemaining = length; + byte[] buffer = ArrayPool.Shared.Rent(Math.Min(50 * 1024, bytesRemaining)); + while (bytesRemaining > 0) { - int bytesRemaining = length; - byte[] buffer = ArrayPool.Shared.Rent(Math.Min(50 * 1024, bytesRemaining)); - while (bytesRemaining > 0) + int read = readFrom.Read(buffer, 0, Math.Min(buffer.Length, bytesRemaining)); + if (read == 0) { - int read = readFrom.Read(buffer, 0, Math.Min(buffer.Length, bytesRemaining)); - if (read == 0) - { - break; - } - - copyTo?.Write(buffer, 0, read); - bytesRemaining -= read; + break; } - ArrayPool.Shared.Return(buffer); - return length - bytesRemaining; + copyTo?.Write(buffer, 0, read); + bytesRemaining -= read; } + + ArrayPool.Shared.Return(buffer); + return length - bytesRemaining; } } diff --git a/src/NerdBank.GitVersioning/ManagedGit/ZLibStream.cs b/src/NerdBank.GitVersioning/ManagedGit/ZLibStream.cs index d09e0b34..56438025 100644 --- a/src/NerdBank.GitVersioning/ManagedGit/ZLibStream.cs +++ b/src/NerdBank.GitVersioning/ManagedGit/ZLibStream.cs @@ -1,199 +1,196 @@ - +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + #nullable enable -using System; using System.Buffers; -using System.IO; using System.IO.Compression; -using System.Threading; -using System.Threading.Tasks; -namespace Nerdbank.GitVersioning.ManagedGit +namespace Nerdbank.GitVersioning.ManagedGit; + +/// +/// A which reads zlib-compressed data. +/// +/// +/// +/// This stream parses but ignores the two-byte zlib header at the start of the compressed +/// stream. +/// +/// +/// This stream keeps track of the current position and, if provided via the constructor, +/// the length. +/// +/// +/// This class wraps a rather than inheriting from it, because +/// detects whether Read(Span{byte}) is being overriden +/// and behaves differently when it is. +/// +/// +/// .NET 5.0 ships with a built-in ZLibStream; which may render (parts of) this implementation +/// obsolete. +/// +/// +/// +public class ZLibStream : Stream { + private readonly DeflateStream stream; + private long length; + private long position; + /// - /// A which reads zlib-compressed data. + /// Initializes a new instance of the class. /// - /// - /// - /// This stream parses but ignores the two-byte zlib header at the start of the compressed - /// stream. - /// - /// - /// This stream keeps track of the current position and, if provided via the constructor, - /// the length. - /// - /// - /// This class wraps a rather than inheriting from it, because - /// detects whether Read(Span{byte}) is being overriden - /// and behaves differently when it is. - /// - /// - /// .NET 5.0 ships with a built-in ZLibStream; which may render (parts of) this implementation - /// obsolete. - /// - /// - /// - public class ZLibStream : Stream + /// + /// The from which to read data. + /// + /// + /// The size of the uncompressed data. + /// + public ZLibStream(Stream stream, long length = -1) { - private long length; - private long position; - private DeflateStream stream; - - /// - /// Initializes a new instance of the class. - /// - /// - /// The from which to read data. - /// - /// - /// The size of the uncompressed data. - /// - public ZLibStream(Stream stream, long length = -1) - { - this.stream = new DeflateStream(stream, CompressionMode.Decompress, leaveOpen: false); - this.length = length; + this.stream = new DeflateStream(stream, CompressionMode.Decompress, leaveOpen: false); + this.length = length; - Span zlibHeader = stackalloc byte[2]; - stream.ReadAll(zlibHeader); + Span zlibHeader = stackalloc byte[2]; + stream.ReadAll(zlibHeader); - if (zlibHeader[0] != 0x78 || (zlibHeader[1] != 0x01 && zlibHeader[1] != 0x9C && zlibHeader[1] != 0x5E && zlibHeader[1] != 0xDA)) - { - throw new GitException($"Invalid zlib header {zlibHeader[0]:X2} {zlibHeader[1]:X2}"); - } + if (zlibHeader[0] != 0x78 || (zlibHeader[1] != 0x01 && zlibHeader[1] != 0x9C && zlibHeader[1] != 0x5E && zlibHeader[1] != 0xDA)) + { + throw new GitException($"Invalid zlib header {zlibHeader[0]:X2} {zlibHeader[1]:X2}"); } + } - /// - /// Gets the from which the data is being read. - /// - public Stream BaseStream => this.stream; + /// + /// Gets the from which the data is being read. + /// + public Stream BaseStream => this.stream; - /// - public override long Position - { - get => this.position; - set => throw new NotSupportedException(); - } + /// + public override long Position + { + get => this.position; + set => throw new NotSupportedException(); + } - /// - public override long Length => this.length; + /// + public override long Length => this.length; - /// - public override bool CanRead => true; + /// + public override bool CanRead => true; - /// - public override bool CanSeek => true; + /// + public override bool CanSeek => true; - /// - public override bool CanWrite => false; + /// + public override bool CanWrite => false; - /// - public override int Read(byte[] array, int offset, int count) - { - int read = this.stream.Read(array, offset, count); - this.position += read; - return read; - } + /// + public override int Read(byte[] array, int offset, int count) + { + int read = this.stream.Read(array, offset, count); + this.position += read; + return read; + } #if !NETSTANDARD2_0 - /// - public override int Read(Span buffer) - { - int read = this.stream.Read(buffer); - this.position += read; - return read; - } + /// + public override int Read(Span buffer) + { + int read = this.stream.Read(buffer); + this.position += read; + return read; + } #endif - /// - public override async Task ReadAsync(byte[] array, int offset, int count, CancellationToken cancellationToken) - { - int read = await this.stream.ReadAsync(array, offset, count, cancellationToken); - this.position += read; - return read; - } + /// + public override async Task ReadAsync(byte[] array, int offset, int count, CancellationToken cancellationToken) + { + int read = await this.stream.ReadAsync(array, offset, count, cancellationToken); + this.position += read; + return read; + } #if !NETSTANDARD2_0 - /// - public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) - { - int read = await this.stream.ReadAsync(buffer, cancellationToken); - this.position += read; - return read; - } + /// + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + int read = await this.stream.ReadAsync(buffer, cancellationToken); + this.position += read; + return read; + } #endif - /// - public override int ReadByte() - { - int value = this.stream.ReadByte(); - - if (value != -1) - { - this.position += 1; - } - - return value; - } + /// + public override int ReadByte() + { + int value = this.stream.ReadByte(); - /// - public override long Seek(long offset, SeekOrigin origin) + if (value != -1) { - if (origin == SeekOrigin.Begin && offset == this.position) - { - return this.position; - } - - if (origin == SeekOrigin.Current && offset == 0) - { - return this.position; - } - - if (origin == SeekOrigin.Begin && offset > this.position) - { - // We may be able to optimize this by skipping over the compressed data - this.ReadExactly(checked((int)(offset - this.position))); - return this.position; - } - else - { - throw new NotImplementedException(); - } + this.position += 1; } - /// - public override void Flush() - { - throw new NotSupportedException(); - } + return value; + } - /// - public override void SetLength(long value) + /// + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin && offset == this.position) { - throw new NotSupportedException(); + return this.position; } - /// - public override void Write(byte[] buffer, int offset, int count) + if (origin == SeekOrigin.Current && offset == 0) { - throw new NotSupportedException(); + return this.position; } - /// - protected override void Dispose(bool disposing) + if (origin == SeekOrigin.Begin && offset > this.position) { - this.stream.Dispose(); + // We may be able to optimize this by skipping over the compressed data + this.ReadExactly(checked((int)(offset - this.position))); + return this.position; } - - /// - /// Initializes the length and position properties. - /// - /// - /// The length of this class. - /// - protected void Initialize(long length) + else { - this.position = 0; - this.length = length; + throw new NotImplementedException(); } } + + /// + public override void Flush() + { + throw new NotSupportedException(); + } + + /// + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + /// + protected override void Dispose(bool disposing) + { + this.stream.Dispose(); + } + + /// + /// Initializes the length and position properties. + /// + /// + /// The length of this class. + /// + protected void Initialize(long length) + { + this.position = 0; + this.length = length; + } } diff --git a/src/NerdBank.GitVersioning/NativeMethods.txt b/src/NerdBank.GitVersioning/NativeMethods.txt index 885be08e..669c0efc 100644 --- a/src/NerdBank.GitVersioning/NativeMethods.txt +++ b/src/NerdBank.GitVersioning/NativeMethods.txt @@ -1,2 +1,4 @@ CreateFile +FILE_ACCESS_RIGHTS INVALID_HANDLE_VALUE +LoadLibrary diff --git a/src/NerdBank.GitVersioning/NerdBank.GitVersioning.csproj b/src/NerdBank.GitVersioning/NerdBank.GitVersioning.csproj deleted file mode 100644 index dcda6438..00000000 --- a/src/NerdBank.GitVersioning/NerdBank.GitVersioning.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - netstandard2.0;netcoreapp3.1 - true - Full - false - Nerdbank.GitVersioning.Core - true - Nerdbank.GitVersioning - - - - - - - - - - - - - - - - diff --git a/src/NerdBank.GitVersioning/Nerdbank.GitVersioning.csproj b/src/NerdBank.GitVersioning/Nerdbank.GitVersioning.csproj new file mode 100644 index 00000000..54346b17 --- /dev/null +++ b/src/NerdBank.GitVersioning/Nerdbank.GitVersioning.csproj @@ -0,0 +1,24 @@ + + + netstandard2.0;net6.0 + true + Full + false + Nerdbank.GitVersioning.Core + true + Nerdbank.GitVersioning + + + + + + + + + + + + + + + diff --git a/src/NerdBank.GitVersioning/NoGit/NoGitContext.cs b/src/NerdBank.GitVersioning/NoGit/NoGitContext.cs index 4d8324db..b1e15b47 100644 --- a/src/NerdBank.GitVersioning/NoGit/NoGitContext.cs +++ b/src/NerdBank.GitVersioning/NoGit/NoGitContext.cs @@ -1,38 +1,55 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable -using System; using System.Diagnostics; -namespace Nerdbank.GitVersioning +namespace Nerdbank.GitVersioning; + +[DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] +internal class NoGitContext : GitContext { - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] - internal class NoGitContext : GitContext + private const string NotAGitRepoMessage = "Not a git repo"; + + public NoGitContext(string workingTreePath) + : base(workingTreePath, null) { - private const string NotAGitRepoMessage = "Not a git repo"; + this.VersionFile = new NoGitVersionFile(this); + } - public NoGitContext(string workingTreePath) - : base(workingTreePath, null) - { - this.VersionFile = new NoGitVersionFile(this); - } + /// + public override VersionFile VersionFile { get; } - public override VersionFile VersionFile { get; } + /// + public override string? GitCommitId => null; - public override string? GitCommitId => null; + /// + public override bool IsHead => false; - public override bool IsHead => false; + /// + public override DateTimeOffset? GitCommitDate => null; - public override DateTimeOffset? GitCommitDate => null; + /// + public override string? HeadCanonicalName => null; - public override string? HeadCanonicalName => null; + private string DebuggerDisplay => $"\"{this.WorkingTreePath}\" (no-git)"; - private string DebuggerDisplay => $"\"{this.WorkingTreePath}\" (no-git)"; + /// + public override void ApplyTag(string name) => throw new InvalidOperationException(NotAGitRepoMessage); - public override void ApplyTag(string name) => throw new InvalidOperationException(NotAGitRepoMessage); - public override void Stage(string path) => throw new InvalidOperationException(NotAGitRepoMessage); - public override string GetShortUniqueCommitId(int minLength) => throw new InvalidOperationException(NotAGitRepoMessage); - public override bool TrySelectCommit(string committish) => throw new InvalidOperationException(NotAGitRepoMessage); - internal override int CalculateVersionHeight(VersionOptions? committedVersion, VersionOptions? workingVersion) => 0; - internal override Version GetIdAsVersion(VersionOptions? committedVersion, VersionOptions? workingVersion, int versionHeight) => throw new NotImplementedException(); - } + /// + public override void Stage(string path) => throw new InvalidOperationException(NotAGitRepoMessage); + + /// + public override string GetShortUniqueCommitId(int minLength) => throw new InvalidOperationException(NotAGitRepoMessage); + + /// + public override bool TrySelectCommit(string committish) => throw new InvalidOperationException(NotAGitRepoMessage); + + /// + internal override int CalculateVersionHeight(VersionOptions? committedVersion, VersionOptions? workingVersion) => 0; + + /// + internal override Version GetIdAsVersion(VersionOptions? committedVersion, VersionOptions? workingVersion, int versionHeight) => throw new NotImplementedException(); } diff --git a/src/NerdBank.GitVersioning/NoGit/NoGitVersionFile.cs b/src/NerdBank.GitVersioning/NoGit/NoGitVersionFile.cs index 44229691..98aca3a4 100644 --- a/src/NerdBank.GitVersioning/NoGit/NoGitVersionFile.cs +++ b/src/NerdBank.GitVersioning/NoGit/NoGitVersionFile.cs @@ -1,14 +1,17 @@ -namespace Nerdbank.GitVersioning -{ - using Validation; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. - internal class NoGitVersionFile : VersionFile - { - public NoGitVersionFile(GitContext context) - : base(context) - { - } +using Validation; + +namespace Nerdbank.GitVersioning; - protected override VersionOptions GetVersionCore(out string actualDirectory) => throw Assumes.NotReachable(); +internal class NoGitVersionFile : VersionFile +{ + public NoGitVersionFile(GitContext context) + : base(context) + { } + + /// + protected override VersionOptions GetVersionCore(out string actualDirectory) => throw Assumes.NotReachable(); } diff --git a/src/NerdBank.GitVersioning/Properties/AssemblyInfo.cs b/src/NerdBank.GitVersioning/Properties/AssemblyInfo.cs deleted file mode 100644 index 86d2abd0..00000000 --- a/src/NerdBank.GitVersioning/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -[assembly: AssemblyCopyright("Copyright (c) .NET Foundation and Contributors")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: ComVisible(false)] diff --git a/src/NerdBank.GitVersioning/ReleaseManager.cs b/src/NerdBank.GitVersioning/ReleaseManager.cs index 09fc5aa8..9dc19801 100644 --- a/src/NerdBank.GitVersioning/ReleaseManager.cs +++ b/src/NerdBank.GitVersioning/ReleaseManager.cs @@ -1,486 +1,489 @@ -namespace Nerdbank.GitVersioning +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using LibGit2Sharp; +using Nerdbank.GitVersioning.LibGit2; +using Newtonsoft.Json; +using Validation; +using Version = System.Version; + +namespace Nerdbank.GitVersioning; + +/// +/// Methods for creating releases. +/// +/// +/// This class authors git commits, branches, etc. and thus must use libgit2 rather than our internal managed implementation which is read-only. +/// +public class ReleaseManager { - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using LibGit2Sharp; - using Nerdbank.GitVersioning.LibGit2; - using Newtonsoft.Json; - using Validation; - using Version = System.Version; + private readonly TextWriter stdout; + private readonly TextWriter stderr; /// - /// Methods for creating releases + /// Initializes a new instance of the class. /// - /// - /// This class authors git commits, branches, etc. and thus must use libgit2 rather than our internal managed implementation which is read-only. - /// - public class ReleaseManager + /// The to write output to (e.g. ). + /// The to write error messages to (e.g. ). + public ReleaseManager(TextWriter outputWriter = null, TextWriter errorWriter = null) + { + this.stdout = outputWriter ?? TextWriter.Null; + this.stderr = errorWriter ?? TextWriter.Null; + } + + /// + /// Defines the possible errors that can occur when preparing a release. + /// + public enum ReleasePreparationError { /// - /// Defines the possible errors that can occur when preparing a release + /// The project directory is not a git repository. /// - public enum ReleasePreparationError - { - /// - /// The project directory is not a git repository - /// - NoGitRepo, - - /// - /// There are pending changes in the project directory - /// - UncommittedChanges, - - /// - /// The "branchName" setting in "version.json" is invalid - /// - InvalidBranchNameSetting, - - /// - /// version.json/version.txt not found - /// - NoVersionFile, - - /// - /// Updating the version would result in a version lower than the previous version - /// - VersionDecrement, - - /// - /// Branch cannot be set to the specified version because the new version is not higher than the current version - /// - NoVersionIncrement, - - /// - /// Cannot create a branch because it already exists - /// - BranchAlreadyExists, - - /// - /// Cannot create a commit because user name and user email are not configured (either at the repo or global level) - /// - UserNotConfigured, - - /// - /// HEAD is detached. A branch must be checked out first. - /// - DetachedHead, - - /// - /// The versionIncrement setting cannot be applied to the current version. - /// - InvalidVersionIncrementSetting, - } + NoGitRepo, /// - /// Exception indicating an error during preparation of a release + /// There are pending changes in the project directory. /// - public class ReleasePreparationException : Exception - { - /// - /// Gets the error that occurred. - /// - public ReleasePreparationError Error { get; } - - /// - /// Initializes a new instance of - /// - /// The error that occurred. - public ReleasePreparationException(ReleasePreparationError error) => this.Error = error; - - /// - /// Initializes a new instance of - /// - /// The error that occurred. - /// The inner exception. - public ReleasePreparationException(ReleasePreparationError error, Exception innerException) - : base(null, innerException) => this.Error = error; - } + UncommittedChanges, /// - /// Encapsulates information on a release created through . + /// The "branchName" setting in "version.json" is invalid. /// - public class ReleaseInfo - { - /// - /// Gets information on the 'current' branch, i.e. the branch the release was created from. - /// - public ReleaseBranchInfo CurrentBranch { get; } - - /// - /// Gets information on the new branch created by . - /// - /// - /// Information on the newly created branch as instance of or null, if no new branch was created. - /// - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public ReleaseBranchInfo NewBranch { get; } - - /// - /// Initializes a new instance of . - /// - /// Information on the branch the release was created from. - public ReleaseInfo(ReleaseBranchInfo currentBranch) : this(currentBranch, null) - { } - - /// - /// Initializes a new instance of . - /// - /// Information on the branch the release was created from. - /// Information on the newly created branch. - [JsonConstructor] - public ReleaseInfo(ReleaseBranchInfo currentBranch, ReleaseBranchInfo newBranch) - { - Requires.NotNull(currentBranch, nameof(currentBranch)); - // skip null check for newBranch, it is allowed to be null. + InvalidBranchNameSetting, - this.CurrentBranch = currentBranch; - this.NewBranch = newBranch; - } - } + /// + /// version.json/version.txt not found. + /// + NoVersionFile, /// - /// Encapsulates information on a branch created or updated by . + /// Updating the version would result in a version lower than the previous version. /// - public class ReleaseBranchInfo - { - /// - /// The name of the branch, e.g. master. - /// - public string Name { get; } - - /// - /// The id of the branch's tip commit after the update. - /// - public string Commit { get; } - - /// - /// The version configured in the branch's version.json. - /// - public SemanticVersion Version { get; } - - /// - /// Initializes a new instance of . - /// - /// The name of the branch. - /// The id of the branch's tip. - /// The version configured in the branch's version.json. - public ReleaseBranchInfo(string name, string commit, SemanticVersion version) - { - Requires.NotNullOrWhiteSpace(name, nameof(name)); - Requires.NotNullOrWhiteSpace(commit, nameof(commit)); - Requires.NotNull(version, nameof(version)); + VersionDecrement, - this.Name = name; - this.Commit = commit; - this.Version = version; - } - } + /// + /// Branch cannot be set to the specified version because the new version is not higher than the current version. + /// + NoVersionIncrement, /// - /// Enumerates the output formats supported by . + /// Cannot create a branch because it already exists. /// - public enum ReleaseManagerOutputMode - { - /// - /// Use unstructured text output. - /// - Text = 0, - /// - /// Output information about the release as JSON. - /// - Json = 1 - } + BranchAlreadyExists, + /// + /// Cannot create a commit because user name and user email are not configured (either at the repo or global level). + /// + UserNotConfigured, - private readonly TextWriter stdout; - private readonly TextWriter stderr; + /// + /// HEAD is detached. A branch must be checked out first. + /// + DetachedHead, /// - /// Initializes a new instance of . + /// The versionIncrement setting cannot be applied to the current version. /// - /// The to write output to (e.g. ). - /// The to write error messages to (e.g. ). - public ReleaseManager(TextWriter outputWriter = null, TextWriter errorWriter = null) - { - this.stdout = outputWriter ?? TextWriter.Null; - this.stderr = errorWriter ?? TextWriter.Null; - } + InvalidVersionIncrementSetting, + } + /// + /// Enumerates the output formats supported by . + /// + public enum ReleaseManagerOutputMode + { /// - /// Prepares a release for the specified directory by creating a release branch and incrementing the version in the current branch. + /// Use unstructured text output. /// - /// Thrown when the release could not be created. - /// - /// The path to the directory which may (or its ancestors may) define the version file. - /// - /// - /// The prerelease tag to add to the version on the release branch. Pass null to omit/remove the prerelease tag. - /// The leading hyphen may be specified or omitted. - /// - /// - /// The next version to save to the version file on the current branch. Pass null to automatically determine the next - /// version based on the current version and the versionIncrement setting in version.json. - /// Parameter will be ignored if the current branch is a release branch. - /// - /// - /// The increment to apply in order to determine the next version on the current branch. - /// If specified, value will be used instead of the increment specified in version.json. - /// Parameter will be ignored if the current branch is a release branch. - /// - /// - /// The output format to use for writing to stdout. - /// - public void PrepareRelease(string projectDirectory, string releaseUnstableTag = null, Version nextVersion = null, VersionOptions.ReleaseVersionIncrement? versionIncrement = null, ReleaseManagerOutputMode outputMode = default) - { - Requires.NotNull(projectDirectory, nameof(projectDirectory)); + Text = 0, - // open the git repository - var context = this.GetRepository(projectDirectory); - var repository = context.Repository; + /// + /// Output information about the release as JSON. + /// + Json = 1, + } - if (repository.Info.IsHeadDetached) - { - this.stderr.WriteLine("Detached head. Check out a branch first."); - throw new ReleasePreparationException(ReleasePreparationError.DetachedHead); - } + /// + /// Prepares a release for the specified directory by creating a release branch and incrementing the version in the current branch. + /// + /// Thrown when the release could not be created. + /// + /// The path to the directory which may (or its ancestors may) define the version file. + /// + /// + /// The prerelease tag to add to the version on the release branch. Pass to omit/remove the prerelease tag. + /// The leading hyphen may be specified or omitted. + /// + /// + /// The next version to save to the version file on the current branch. Pass to automatically determine the next + /// version based on the current version and the setting in version.json. + /// Parameter will be ignored if the current branch is a release branch. + /// + /// + /// The increment to apply in order to determine the next version on the current branch. + /// If specified, value will be used instead of the increment specified in version.json. + /// Parameter will be ignored if the current branch is a release branch. + /// + /// + /// The output format to use for writing to stdout. + /// + public void PrepareRelease(string projectDirectory, string releaseUnstableTag = null, Version nextVersion = null, VersionOptions.ReleaseVersionIncrement? versionIncrement = null, ReleaseManagerOutputMode outputMode = default) + { + Requires.NotNull(projectDirectory, nameof(projectDirectory)); - // get the current version - var versionOptions = context.VersionFile.GetVersion(); - if (versionOptions is null) - { - this.stderr.WriteLine($"Failed to load version file for directory '{projectDirectory}'."); - throw new ReleasePreparationException(ReleasePreparationError.NoVersionFile); - } + // open the git repository + LibGit2Context context = this.GetRepository(projectDirectory); + Repository repository = context.Repository; - var releaseBranchName = this.GetReleaseBranchName(versionOptions); - var originalBranchName = repository.Head.FriendlyName; - var releaseVersion = string.IsNullOrEmpty(releaseUnstableTag) - ? versionOptions.Version.WithoutPrepreleaseTags() - : versionOptions.Version.SetFirstPrereleaseTag(releaseUnstableTag); + if (repository.Info.IsHeadDetached) + { + this.stderr.WriteLine("Detached head. Check out a branch first."); + throw new ReleasePreparationException(ReleasePreparationError.DetachedHead); + } - // check if the current branch is the release branch - if (string.Equals(originalBranchName, releaseBranchName, StringComparison.OrdinalIgnoreCase)) - { - if (outputMode == ReleaseManagerOutputMode.Text) - { - this.stdout.WriteLine($"{releaseBranchName} branch advanced from {versionOptions.Version} to {releaseVersion}."); - } - else - { - var releaseInfo = new ReleaseInfo(new ReleaseBranchInfo(releaseBranchName, repository.Head.Tip.Id.ToString(), releaseVersion)); - this.WriteToOutput(releaseInfo); - } - this.UpdateVersion(context, versionOptions.Version, releaseVersion); - return; - } + // get the current version + VersionOptions versionOptions = context.VersionFile.GetVersion(); + if (versionOptions is null) + { + this.stderr.WriteLine($"Failed to load version file for directory '{projectDirectory}'."); + throw new ReleasePreparationException(ReleasePreparationError.NoVersionFile); + } - var nextDevVersion = this.GetNextDevVersion(versionOptions, nextVersion, versionIncrement); + string releaseBranchName = this.GetReleaseBranchName(versionOptions); + string originalBranchName = repository.Head.FriendlyName; + SemanticVersion releaseVersion = string.IsNullOrEmpty(releaseUnstableTag) + ? versionOptions.Version.WithoutPrepreleaseTags() + : versionOptions.Version.SetFirstPrereleaseTag(releaseUnstableTag); - // check if the current version on the current branch is different from the next version - // otherwise, both the release branch and the dev branch would have the same version - if (versionOptions.Version.Version == nextDevVersion.Version) + // check if the current branch is the release branch + if (string.Equals(originalBranchName, releaseBranchName, StringComparison.OrdinalIgnoreCase)) + { + if (outputMode == ReleaseManagerOutputMode.Text) { - this.stderr.WriteLine($"Version on '{originalBranchName}' is already set to next version {nextDevVersion.Version}."); - throw new ReleasePreparationException(ReleasePreparationError.NoVersionIncrement); + this.stdout.WriteLine($"{releaseBranchName} branch advanced from {versionOptions.Version} to {releaseVersion}."); } - - // check if the release branch already exists - if (repository.Branches[releaseBranchName] is not null) + else { - this.stderr.WriteLine($"Cannot create branch '{releaseBranchName}' because it already exists."); - throw new ReleasePreparationException(ReleasePreparationError.BranchAlreadyExists); + var releaseInfo = new ReleaseInfo(new ReleaseBranchInfo(releaseBranchName, repository.Head.Tip.Id.ToString(), releaseVersion)); + this.WriteToOutput(releaseInfo); } - // create release branch and update version - var releaseBranch = repository.CreateBranch(releaseBranchName); - global::LibGit2Sharp.Commands.Checkout(repository, releaseBranch); this.UpdateVersion(context, versionOptions.Version, releaseVersion); + return; + } - if (outputMode == ReleaseManagerOutputMode.Text) - { - this.stdout.WriteLine($"{releaseBranchName} branch now tracks v{releaseVersion} stabilization and release."); - } - - // update version on main branch - global::LibGit2Sharp.Commands.Checkout(repository, originalBranchName); - this.UpdateVersion(context, versionOptions.Version, nextDevVersion); + SemanticVersion nextDevVersion = this.GetNextDevVersion(versionOptions, nextVersion, versionIncrement); - if (outputMode == ReleaseManagerOutputMode.Text) - { - this.stdout.WriteLine($"{originalBranchName} branch now tracks v{nextDevVersion} development."); - } + // check if the current version on the current branch is different from the next version + // otherwise, both the release branch and the dev branch would have the same version + if (versionOptions.Version.Version == nextDevVersion.Version) + { + this.stderr.WriteLine($"Version on '{originalBranchName}' is already set to next version {nextDevVersion.Version}."); + throw new ReleasePreparationException(ReleasePreparationError.NoVersionIncrement); + } - // Merge release branch back to main branch - var mergeOptions = new MergeOptions() - { - CommitOnSuccess = true, - MergeFileFavor = MergeFileFavor.Ours, - }; - repository.Merge(releaseBranch, this.GetSignature(repository), mergeOptions); + // check if the release branch already exists + if (repository.Branches[releaseBranchName] is not null) + { + this.stderr.WriteLine($"Cannot create branch '{releaseBranchName}' because it already exists."); + throw new ReleasePreparationException(ReleasePreparationError.BranchAlreadyExists); + } - if (outputMode == ReleaseManagerOutputMode.Json) - { - var originalBranchInfo = new ReleaseBranchInfo(originalBranchName, repository.Head.Tip.Sha, nextDevVersion); - var releaseBranchInfo = new ReleaseBranchInfo(releaseBranchName, repository.Branches[releaseBranchName].Tip.Id.ToString(), releaseVersion); - var releaseInfo = new ReleaseInfo(originalBranchInfo, releaseBranchInfo); + // create release branch and update version + Branch releaseBranch = repository.CreateBranch(releaseBranchName); + global::LibGit2Sharp.Commands.Checkout(repository, releaseBranch); + this.UpdateVersion(context, versionOptions.Version, releaseVersion); - this.WriteToOutput(releaseInfo); - } + if (outputMode == ReleaseManagerOutputMode.Text) + { + this.stdout.WriteLine($"{releaseBranchName} branch now tracks v{releaseVersion} stabilization and release."); } - private string GetReleaseBranchName(VersionOptions versionOptions) + // update version on main branch + global::LibGit2Sharp.Commands.Checkout(repository, originalBranchName); + this.UpdateVersion(context, versionOptions.Version, nextDevVersion); + + if (outputMode == ReleaseManagerOutputMode.Text) { - Requires.NotNull(versionOptions, nameof(versionOptions)); + this.stdout.WriteLine($"{originalBranchName} branch now tracks v{nextDevVersion} development."); + } - var branchNameFormat = versionOptions.ReleaseOrDefault.BranchNameOrDefault; + // Merge release branch back to main branch + var mergeOptions = new MergeOptions() + { + CommitOnSuccess = true, + MergeFileFavor = MergeFileFavor.Ours, + }; + repository.Merge(releaseBranch, this.GetSignature(repository), mergeOptions); - // ensure there is a '{version}' placeholder in the branch name - if (string.IsNullOrEmpty(branchNameFormat) || !branchNameFormat.Contains("{version}")) - { - this.stderr.WriteLine($"Invalid 'branchName' setting '{branchNameFormat}'. Missing version placeholder '{{version}}'."); - throw new ReleasePreparationException(ReleasePreparationError.InvalidBranchNameSetting); - } + if (outputMode == ReleaseManagerOutputMode.Json) + { + var originalBranchInfo = new ReleaseBranchInfo(originalBranchName, repository.Head.Tip.Sha, nextDevVersion); + var releaseBranchInfo = new ReleaseBranchInfo(releaseBranchName, repository.Branches[releaseBranchName].Tip.Id.ToString(), releaseVersion); + var releaseInfo = new ReleaseInfo(originalBranchInfo, releaseBranchInfo); - // replace the "{version}" placeholder with the actual version - return branchNameFormat.Replace("{version}", versionOptions.Version.Version.ToString()); + this.WriteToOutput(releaseInfo); } + } - private void UpdateVersion(LibGit2Context context, SemanticVersion oldVersion, SemanticVersion newVersion) + private static bool IsVersionDecrement(SemanticVersion oldVersion, SemanticVersion newVersion) + { + if (newVersion.Version > oldVersion.Version) { - Requires.NotNull(context, nameof(context)); + return false; + } + else if (newVersion.Version == oldVersion.Version) + { + return string.IsNullOrEmpty(oldVersion.Prerelease) && + !string.IsNullOrEmpty(newVersion.Prerelease); + } + else + { + // newVersion.Version < oldVersion.Version + return true; + } + } - var signature = this.GetSignature(context.Repository); - var versionOptions = context.VersionFile.GetVersion(); + private string GetReleaseBranchName(VersionOptions versionOptions) + { + Requires.NotNull(versionOptions, nameof(versionOptions)); - if (IsVersionDecrement(oldVersion, newVersion)) - { - this.stderr.WriteLine($"Cannot change version from {oldVersion} to {newVersion} because {newVersion} is older than {oldVersion}."); - throw new ReleasePreparationException(ReleasePreparationError.VersionDecrement); - } + string branchNameFormat = versionOptions.ReleaseOrDefault.BranchNameOrDefault; - if (!EqualityComparer.Default.Equals(versionOptions.Version, newVersion)) - { - if (versionOptions.VersionHeightPosition.HasValue && SemanticVersion.WillVersionChangeResetVersionHeight(versionOptions.Version, newVersion, versionOptions.VersionHeightPosition.Value)) - { - // The version will be reset by this change, so remove the version height offset property. - versionOptions.VersionHeightOffset = null; - } - - versionOptions.Version = newVersion; - var filePath = context.VersionFile.SetVersion(context.AbsoluteProjectDirectory, versionOptions, includeSchemaProperty: true); - - global::LibGit2Sharp.Commands.Stage(context.Repository, filePath); - - // Author a commit only if we effectively changed something. - if (!context.Repository.Head.Tip.Tree.Equals(context.Repository.Index.WriteToTree())) - { - context.Repository.Commit($"Set version to '{versionOptions.Version}'", signature, signature, new CommitOptions() { AllowEmptyCommit = false }); - } - } + // ensure there is a '{version}' placeholder in the branch name + if (string.IsNullOrEmpty(branchNameFormat) || !branchNameFormat.Contains("{version}")) + { + this.stderr.WriteLine($"Invalid 'branchName' setting '{branchNameFormat}'. Missing version placeholder '{{version}}'."); + throw new ReleasePreparationException(ReleasePreparationError.InvalidBranchNameSetting); } - private Signature GetSignature(Repository repository) - { - var signature = repository.Config.BuildSignature(DateTimeOffset.Now); - if (signature is null) - { - this.stderr.WriteLine("Cannot create commits in this repo because git user name and email are not configured."); - throw new ReleasePreparationException(ReleasePreparationError.UserNotConfigured); - } + // replace the "{version}" placeholder with the actual version + return branchNameFormat.Replace("{version}", versionOptions.Version.Version.ToString()); + } + + private void UpdateVersion(LibGit2Context context, SemanticVersion oldVersion, SemanticVersion newVersion) + { + Requires.NotNull(context, nameof(context)); + + Signature signature = this.GetSignature(context.Repository); + VersionOptions versionOptions = context.VersionFile.GetVersion(); - return signature; + if (IsVersionDecrement(oldVersion, newVersion)) + { + this.stderr.WriteLine($"Cannot change version from {oldVersion} to {newVersion} because {newVersion} is older than {oldVersion}."); + throw new ReleasePreparationException(ReleasePreparationError.VersionDecrement); } - private LibGit2Context GetRepository(string projectDirectory) + if (!EqualityComparer.Default.Equals(versionOptions.Version, newVersion)) { - // open git repo and use default configuration (in order to commit we need a configured user name and email - // which is most likely configured on a user/system level rather than the repo level. - var context = GitContext.Create(projectDirectory, writable: true); - if (!context.IsRepository) + if (versionOptions.VersionHeightPosition.HasValue && SemanticVersion.WillVersionChangeResetVersionHeight(versionOptions.Version, newVersion, versionOptions.VersionHeightPosition.Value)) { - this.stderr.WriteLine($"No git repository found above directory '{projectDirectory}'."); - throw new ReleasePreparationException(ReleasePreparationError.NoGitRepo); + // The version will be reset by this change, so remove the version height offset property. + versionOptions.VersionHeightOffset = null; } - var libgit2context = (LibGit2Context)context; + versionOptions.Version = newVersion; + string filePath = context.VersionFile.SetVersion(context.AbsoluteProjectDirectory, versionOptions, includeSchemaProperty: true); + global::LibGit2Sharp.Commands.Stage(context.Repository, filePath); - // abort if there are any pending changes - var status = libgit2context.Repository.RetrieveStatus(); - if (status.IsDirty) + // Author a commit only if we effectively changed something. + if (!context.Repository.Head.Tip.Tree.Equals(context.Repository.Index.WriteToTree())) { - var changedFiles = status.OfType().ToList(); - var changesFilesFormatted = string.Join(Environment.NewLine, changedFiles.Select(t => $"- {t.FilePath} changed with {nameof(FileStatus)} {t.State}")); - this.stderr.WriteLine($"Uncommitted changes ({changedFiles.Count}) in directory '{projectDirectory}':"); - this.stderr.WriteLine(changesFilesFormatted); - throw new ReleasePreparationException(ReleasePreparationError.UncommittedChanges); + context.Repository.Commit($"Set version to '{versionOptions.Version}'", signature, signature, new CommitOptions() { AllowEmptyCommit = false }); } + } + } - // check if repo is configured so we can create commits - _ = this.GetSignature(libgit2context.Repository); + private Signature GetSignature(Repository repository) + { + Signature signature = repository.Config.BuildSignature(DateTimeOffset.Now); + if (signature is null) + { + this.stderr.WriteLine("Cannot create commits in this repo because git user name and email are not configured."); + throw new ReleasePreparationException(ReleasePreparationError.UserNotConfigured); + } + + return signature; + } - return libgit2context; + private LibGit2Context GetRepository(string projectDirectory) + { + // open git repo and use default configuration (in order to commit we need a configured user name and email + // which is most likely configured on a user/system level rather than the repo level. + var context = GitContext.Create(projectDirectory, engine: GitContext.Engine.ReadWrite); + if (!context.IsRepository) + { + this.stderr.WriteLine($"No git repository found above directory '{projectDirectory}'."); + throw new ReleasePreparationException(ReleasePreparationError.NoGitRepo); } - private static bool IsVersionDecrement(SemanticVersion oldVersion, SemanticVersion newVersion) + var libgit2context = (LibGit2Context)context; + + // abort if there are any pending changes + RepositoryStatus status = libgit2context.Repository.RetrieveStatus(); + if (status.IsDirty) { - if (newVersion.Version > oldVersion.Version) - { - return false; - } - else if (newVersion.Version == oldVersion.Version) - { - return string.IsNullOrEmpty(oldVersion.Prerelease) && - !string.IsNullOrEmpty(newVersion.Prerelease); - } - else - { - // newVersion.Version < oldVersion.Version - return true; - } + // This filter copies the internal logic used by LibGit2 behind RepositoryStatus.IsDirty to tell if + // a repo is dirty or not + // Could be simplified if https://github.com/libgit2/libgit2sharp/pull/2004 is ever merged + var changedFiles = status.Where(file => file.State != FileStatus.Ignored && file.State != FileStatus.Unaltered).ToList(); + string changesFilesFormatted = string.Join(Environment.NewLine, changedFiles.Select(t => $"- {t.FilePath} changed with {nameof(FileStatus)} {t.State}")); + this.stderr.WriteLine($"No uncommitted changes are allowed, but {changedFiles.Count} are present in directory '{projectDirectory}':"); + this.stderr.WriteLine(changesFilesFormatted); + throw new ReleasePreparationException(ReleasePreparationError.UncommittedChanges); } - private SemanticVersion GetNextDevVersion(VersionOptions versionOptions, Version nextVersionOverride, VersionOptions.ReleaseVersionIncrement? versionIncrementOverride) + // check if repo is configured so we can create commits + _ = this.GetSignature(libgit2context.Repository); + + return libgit2context; + } + + private SemanticVersion GetNextDevVersion(VersionOptions versionOptions, Version nextVersionOverride, VersionOptions.ReleaseVersionIncrement? versionIncrementOverride) + { + SemanticVersion currentVersion = versionOptions.Version; + + SemanticVersion nextDevVersion; + if (nextVersionOverride is not null) + { + nextDevVersion = new SemanticVersion(nextVersionOverride, currentVersion.Prerelease, currentVersion.BuildMetadata); + } + else { - var currentVersion = versionOptions.Version; + // Determine the increment to use: + // Use parameter versionIncrementOverride if it has a value, otherwise use setting from version.json. + VersionOptions.ReleaseVersionIncrement versionIncrement = versionIncrementOverride ?? versionOptions.ReleaseOrDefault.VersionIncrementOrDefault; - SemanticVersion nextDevVersion; - if (nextVersionOverride is not null) - { - nextDevVersion = new SemanticVersion(nextVersionOverride, currentVersion.Prerelease, currentVersion.BuildMetadata); - } - else + // The increment is only valid if the current version has the required precision: + // - increment settings "Major" and "Minor" are always valid. + // - increment setting "Build" is only valid if the version has at lease three segments. + bool isValidIncrement = versionIncrement != VersionOptions.ReleaseVersionIncrement.Build || + versionOptions.Version.Version.Build >= 0; + + if (!isValidIncrement) { - // Determine the increment to use: - // Use parameter versionIncrementOverride if it has a value, otherwise use setting from version.json. - var versionIncrement = versionIncrementOverride ?? versionOptions.ReleaseOrDefault.VersionIncrementOrDefault; - - // The increment is only valid if the current version has the required precision: - // - increment settings "Major" and "Minor" are always valid. - // - increment setting "Build" is only valid if the version has at lease three segments. - var isValidIncrement = versionIncrement != VersionOptions.ReleaseVersionIncrement.Build || - versionOptions.Version.Version.Build >= 0; - - if (!isValidIncrement) - { - this.stderr.WriteLine($"Cannot apply version increment 'build' to version '{versionOptions.Version}' because it only has major and minor segments."); - throw new ReleasePreparationException(ReleasePreparationError.InvalidVersionIncrementSetting); - } - - nextDevVersion = currentVersion.Increment(versionIncrement); + this.stderr.WriteLine($"Cannot apply version increment 'build' to version '{versionOptions.Version}' because it only has major and minor segments."); + throw new ReleasePreparationException(ReleasePreparationError.InvalidVersionIncrementSetting); } - // return next version with prerelease tag specified in version.json - return nextDevVersion.SetFirstPrereleaseTag(versionOptions.ReleaseOrDefault.FirstUnstableTagOrDefault); + nextDevVersion = currentVersion.Increment(versionIncrement); } - private void WriteToOutput(ReleaseInfo releaseInfo) + // return next version with prerelease tag specified in version.json + return nextDevVersion.SetFirstPrereleaseTag(versionOptions.ReleaseOrDefault.FirstUnstableTagOrDefault); + } + + private void WriteToOutput(ReleaseInfo releaseInfo) + { + string json = JsonConvert.SerializeObject(releaseInfo, Formatting.Indented, new SemanticVersionJsonConverter()); + this.stdout.WriteLine(json); + } + + /// + /// Exception indicating an error during preparation of a release. + /// + public class ReleasePreparationException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The error that occurred. + public ReleasePreparationException(ReleasePreparationError error) => this.Error = error; + + /// + /// Initializes a new instance of the class. + /// + /// The error that occurred. + /// The inner exception. + public ReleasePreparationException(ReleasePreparationError error, Exception innerException) + : base(null, innerException) => this.Error = error; + + /// + /// Gets the error that occurred. + /// + public ReleasePreparationError Error { get; } + } + + /// + /// Encapsulates information on a release created through . + /// + public class ReleaseInfo + { + /// + /// Initializes a new instance of the class. + /// + /// Information on the branch the release was created from. + public ReleaseInfo(ReleaseBranchInfo currentBranch) + : this(currentBranch, null) { - var json = JsonConvert.SerializeObject(releaseInfo, Formatting.Indented, new SemanticVersionJsonConverter()); - this.stdout.WriteLine(json); } + + /// + /// Initializes a new instance of the class. + /// + /// Information on the branch the release was created from. + /// Information on the newly created branch. + [JsonConstructor] + public ReleaseInfo(ReleaseBranchInfo currentBranch, ReleaseBranchInfo newBranch) + { + Requires.NotNull(currentBranch, nameof(currentBranch)); + //// skip null check for newBranch, it is allowed to be null. + + this.CurrentBranch = currentBranch; + this.NewBranch = newBranch; + } + + /// + /// Gets information on the 'current' branch, i.e. the branch the release was created from. + /// + public ReleaseBranchInfo CurrentBranch { get; } + + /// + /// Gets information on the new branch created by . + /// + /// + /// Information on the newly created branch as instance of or , if no new branch was created. + /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public ReleaseBranchInfo NewBranch { get; } + } + + /// + /// Encapsulates information on a branch created or updated by . + /// + public class ReleaseBranchInfo + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the branch. + /// The id of the branch's tip. + /// The version configured in the branch's version.json. + public ReleaseBranchInfo(string name, string commit, SemanticVersion version) + { + Requires.NotNullOrWhiteSpace(name, nameof(name)); + Requires.NotNullOrWhiteSpace(commit, nameof(commit)); + Requires.NotNull(version, nameof(version)); + + this.Name = name; + this.Commit = commit; + this.Version = version; + } + + /// + /// Gets the name of the branch, e.g. main. + /// + public string Name { get; } + + /// + /// Gets the id of the branch's tip commit after the update. + /// + public string Commit { get; } + + /// + /// Gets the version configured in the branch's version.json. + /// + public SemanticVersion Version { get; } } } diff --git a/src/NerdBank.GitVersioning/SemanticVersion.cs b/src/NerdBank.GitVersioning/SemanticVersion.cs index f6840f39..572a54b3 100644 --- a/src/NerdBank.GitVersioning/SemanticVersion.cs +++ b/src/NerdBank.GitVersioning/SemanticVersion.cs @@ -1,387 +1,388 @@ -namespace Nerdbank.GitVersioning +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Diagnostics; +using System.Text.RegularExpressions; +using Validation; + +namespace Nerdbank.GitVersioning; + +/// +/// Describes a version with an optional unstable tag. +/// +[DebuggerDisplay("{DebuggerDisplay,nq}")] +public class SemanticVersion : IEquatable { - using System; - using System.Diagnostics; - using System.Text.RegularExpressions; - using Validation; + /// + /// The regular expression with capture groups for semantic versioning. + /// It considers PATCH to be optional and permits the 4th Revision component. + /// + /// + /// Parts of this regex inspired by this code. + /// + private static readonly Regex FullSemVerPattern = new Regex(@"^v?(?0|[1-9][0-9]*)\.(?0|[1-9][0-9]*)(?:\.(?0|[1-9][0-9]*)(?:\.(?0|[1-9][0-9]*))?)?(?-[\da-z\-]+(?:\.[\da-z\-]+)*)?(?\+[\da-z\-]+(?:\.[\da-z\-]+)*)?$", RegexOptions.IgnoreCase); /// - /// Describes a version with an optional unstable tag. + /// The regex pattern that a prerelease must match. /// - [DebuggerDisplay("{DebuggerDisplay,nq}")] - public class SemanticVersion : IEquatable - { - /// - /// The regular expression with capture groups for semantic versioning. - /// It considers PATCH to be optional and permits the 4th Revision component. - /// - /// - /// Parts of this regex inspired by https://github.com/sindresorhus/semver-regex/blob/master/index.js - /// - private static readonly Regex FullSemVerPattern = new Regex(@"^v?(?0|[1-9][0-9]*)\.(?0|[1-9][0-9]*)(?:\.(?0|[1-9][0-9]*)(?:\.(?0|[1-9][0-9]*))?)?(?-[\da-z\-]+(?:\.[\da-z\-]+)*)?(?\+[\da-z\-]+(?:\.[\da-z\-]+)*)?$", RegexOptions.IgnoreCase); + /// + /// Keep in sync with the regex for the version field found in the version.schema.json file. + /// + private static readonly Regex PrereleasePattern = new Regex("-(?:[\\da-z\\-]+|\\{height\\})(?:\\.(?:[\\da-z\\-]+|\\{height\\}))*", RegexOptions.IgnoreCase); - /// - /// The regex pattern that a prerelease must match. - /// - /// - /// Keep in sync with the regex for the version field found in the version.schema.json file. - /// - private static readonly Regex PrereleasePattern = new Regex("-(?:[\\da-z\\-]+|\\{height\\})(?:\\.(?:[\\da-z\\-]+|\\{height\\}))*", RegexOptions.IgnoreCase); + /// + /// The regex pattern that build metadata must match. + /// + /// + /// Keep in sync with the regex for the version field found in the version.schema.json file. + /// + private static readonly Regex BuildMetadataPattern = new Regex("\\+(?:[\\da-z\\-]+|\\{height\\})(?:\\.(?:[\\da-z\\-]+|\\{height\\}))*", RegexOptions.IgnoreCase); - /// - /// The regex pattern that build metadata must match. - /// - /// - /// Keep in sync with the regex for the version field found in the version.schema.json file. - /// - private static readonly Regex BuildMetadataPattern = new Regex("\\+(?:[\\da-z\\-]+|\\{height\\})(?:\\.(?:[\\da-z\\-]+|\\{height\\}))*", RegexOptions.IgnoreCase); + /// + /// The regular expression with capture groups for semantic versioning, + /// allowing for macros such as {height}. + /// + /// + /// Keep in sync with the regex for the version field found in the version.schema.json file. + /// + private static readonly Regex FullSemVerWithMacrosPattern = new Regex("^v?(?0|[1-9][0-9]*)\\.(?0|[1-9][0-9]*)(?:\\.(?0|[1-9][0-9]*)(?:\\.(?0|[1-9][0-9]*))?)?(?" + PrereleasePattern + ")?(?" + BuildMetadataPattern + ")?$", RegexOptions.IgnoreCase); - /// - /// The regular expression with capture groups for semantic versioning, - /// allowing for macros such as {height}. - /// - /// - /// Keep in sync with the regex for the version field found in the version.schema.json file. - /// - private static readonly Regex FullSemVerWithMacrosPattern = new Regex("^v?(?0|[1-9][0-9]*)\\.(?0|[1-9][0-9]*)(?:\\.(?0|[1-9][0-9]*)(?:\\.(?0|[1-9][0-9]*))?)?(?" + PrereleasePattern + ")?(?" + BuildMetadataPattern + ")?$", RegexOptions.IgnoreCase); + /// + /// Initializes a new instance of the class. + /// + /// The numeric version. + /// The prerelease, with leading - character. + /// The build metadata, with leading + character. + public SemanticVersion(Version version, string prerelease = null, string buildMetadata = null) + { + Requires.NotNull(version, nameof(version)); + VerifyPatternMatch(prerelease, PrereleasePattern, nameof(prerelease)); + VerifyPatternMatch(buildMetadata, BuildMetadataPattern, nameof(buildMetadata)); - /// - /// Initializes a new instance of the class. - /// - /// The numeric version. - /// The prerelease, with leading - character. - /// The build metadata, with leading + character. - public SemanticVersion(Version version, string prerelease = null, string buildMetadata = null) - { - Requires.NotNull(version, nameof(version)); - VerifyPatternMatch(prerelease, PrereleasePattern, nameof(prerelease)); - VerifyPatternMatch(buildMetadata, BuildMetadataPattern, nameof(buildMetadata)); + this.Version = version; + this.Prerelease = prerelease ?? string.Empty; + this.BuildMetadata = buildMetadata ?? string.Empty; + } - this.Version = version; - this.Prerelease = prerelease ?? string.Empty; - this.BuildMetadata = buildMetadata ?? string.Empty; - } + /// + /// Initializes a new instance of the class. + /// + /// The x.y.z numeric version. + /// The prerelease, with leading - character. + /// The build metadata, with leading + character. + public SemanticVersion(string version, string prerelease = null, string buildMetadata = null) + : this(new Version(version), prerelease, buildMetadata) + { + } + /// + /// Identifies the various positions in a semantic version. + /// + public enum Position + { /// - /// Initializes a new instance of the class. + /// The component. /// - /// The x.y.z numeric version. - /// The prerelease, with leading - character. - /// The build metadata, with leading + character. - public SemanticVersion(string version, string prerelease = null, string buildMetadata = null) - : this(new Version(version), prerelease, buildMetadata) - { - } + Major, /// - /// Identifies the various positions in a semantic version. + /// The component. /// - public enum Position - { - /// - /// The component. - /// - Major, - - /// - /// The component. - /// - Minor, - - /// - /// The component. - /// - Build, - - /// - /// The component. - /// - Revision, - - /// - /// The portion of the version. - /// - Prerelease, - - /// - /// The portion of the version. - /// - BuildMetadata, - } + Minor, /// - /// Gets the version. + /// The component. /// - public Version Version { get; } + Build, /// - /// Gets an unstable tag (with the leading hyphen), if applicable. + /// The component. /// - /// A string with a leading hyphen or the empty string. - public string Prerelease { get; } + Revision, /// - /// Gets the build metadata (with the leading plus), if applicable. + /// The portion of the version. /// - /// A string with a leading plus or the empty string. - public string BuildMetadata { get; } + Prerelease, /// - /// Gets the position in a computed version that the version height should appear. + /// The portion of the version. /// - public SemanticVersion.Position? VersionHeightPosition + BuildMetadata, + } + + /// + /// Gets the version. + /// + public Version Version { get; } + + /// + /// Gets an unstable tag (with the leading hyphen), if applicable. + /// + /// A string with a leading hyphen or the empty string. + public string Prerelease { get; } + + /// + /// Gets the build metadata (with the leading plus), if applicable. + /// + /// A string with a leading plus or the empty string. + public string BuildMetadata { get; } + + /// + /// Gets the position in a computed version that the version height should appear. + /// + public SemanticVersion.Position? VersionHeightPosition + { + get { - get + if (this.Prerelease?.Contains(VersionOptions.VersionHeightPlaceholder) ?? false) + { + return SemanticVersion.Position.Prerelease; + } + else if (this.Version.Build == -1) + { + return SemanticVersion.Position.Build; + } + else if (this.Version.Revision == -1) { - if (this.Prerelease?.Contains(VersionOptions.VersionHeightPlaceholder) ?? false) - { - return SemanticVersion.Position.Prerelease; - } - else if (this.Version.Build == -1) - { - return SemanticVersion.Position.Build; - } - else if (this.Version.Revision == -1) - { - return SemanticVersion.Position.Revision; - } - else - { - return null; - } + return SemanticVersion.Position.Revision; + } + else + { + return null; } } + } - /// - /// Gets the position in a computed version that the first 16 bits of a git commit ID should appear, if any. - /// - internal SemanticVersion.Position? GitCommitIdPosition + /// + /// Gets the position in a computed version that the first 16 bits of a git commit ID should appear, if any. + /// + internal SemanticVersion.Position? GitCommitIdPosition + { + get { - get + // We can only store the git commit ID info after there was a place to put the version height. + // We don't want to store the commit ID (which is effectively a random integer) in the revision slot + // if the version height does not appear, or only appears later (in the -prerelease tag) since that + // would mess up version ordering. + if (this.VersionHeightPosition == SemanticVersion.Position.Build) { - // We can only store the git commit ID info after there was a place to put the version height. - // We don't want to store the commit ID (which is effectively a random integer) in the revision slot - // if the version height does not appear, or only appears later (in the -prerelease tag) since that - // would mess up version ordering. - if (this.VersionHeightPosition == SemanticVersion.Position.Build) - { - return SemanticVersion.Position.Revision; - } - else - { - return null; - } + return SemanticVersion.Position.Revision; + } + else + { + return null; } } + } - /// - /// Gets a value indicating whether this instance is the default "0.0" instance. - /// - internal bool IsDefault => this.Version?.Major == 0 && this.Version.Minor == 0 && this.Version.Build == -1 && this.Version.Revision == -1 && this.Prerelease is null && this.BuildMetadata is null; + /// + /// Gets a value indicating whether this instance is the default "0.0" instance. + /// + internal bool IsDefault => this.Version?.Major == 0 && this.Version.Minor == 0 && this.Version.Build == -1 && this.Version.Revision == -1 && this.Prerelease is null && this.BuildMetadata is null; - /// - /// Gets the debugger display for this instance. - /// - private string DebuggerDisplay => this.ToString(); + /// + /// Gets the debugger display for this instance. + /// + private string DebuggerDisplay => this.ToString(); - /// - /// Parses a semantic version from the given string. - /// - /// The value which must wholly constitute a semantic version to succeed. - /// Receives the semantic version, if found. - /// true if a semantic version is found; false otherwise. - public static bool TryParse(string semanticVersion, out SemanticVersion version) + /// + /// Parses a semantic version from the given string. + /// + /// The value which must wholly constitute a semantic version to succeed. + /// Receives the semantic version, if found. + /// if a semantic version is found; otherwise. + public static bool TryParse(string semanticVersion, out SemanticVersion version) + { + Requires.NotNullOrEmpty(semanticVersion, nameof(semanticVersion)); + + Match m = FullSemVerWithMacrosPattern.Match(semanticVersion); + if (m.Success) { - Requires.NotNullOrEmpty(semanticVersion, nameof(semanticVersion)); + int major = int.Parse(m.Groups["major"].Value); + int minor = int.Parse(m.Groups["minor"].Value); + string patch = m.Groups["patch"].Value; + string revision = m.Groups["revision"].Value; + Version systemVersion = patch.Length > 0 + ? revision.Length > 0 ? new Version(major, minor, int.Parse(patch), int.Parse(revision)) : new Version(major, minor, int.Parse(patch)) + : new Version(major, minor); + string prerelease = m.Groups["prerelease"].Value; + string buildMetadata = m.Groups["buildMetadata"].Value; + version = new SemanticVersion(systemVersion, prerelease, buildMetadata); + return true; + } - Match m = FullSemVerWithMacrosPattern.Match(semanticVersion); - if (m.Success) - { - var major = int.Parse(m.Groups["major"].Value); - var minor = int.Parse(m.Groups["minor"].Value); - var patch = m.Groups["patch"].Value; - var revision = m.Groups["revision"].Value; - var systemVersion = patch.Length > 0 - ? revision.Length > 0 ? new Version(major, minor, int.Parse(patch), int.Parse(revision)) : new Version(major, minor, int.Parse(patch)) - : new Version(major, minor); - var prerelease = m.Groups["prerelease"].Value; - var buildMetadata = m.Groups["buildMetadata"].Value; - version = new SemanticVersion(systemVersion, prerelease, buildMetadata); - return true; - } + version = null; + return false; + } - version = null; - return false; - } + /// + /// Parses a semantic version from the given string. + /// + /// The value which must wholly constitute a semantic version to succeed. + /// An instance of , initialized to the value specified in . + public static SemanticVersion Parse(string semanticVersion) + { + SemanticVersion result; + Requires.Argument(TryParse(semanticVersion, out result), nameof(semanticVersion), "Unrecognized or unsupported semantic version."); + return result; + } - /// - /// Parses a semantic version from the given string. - /// - /// The value which must wholly constitute a semantic version to succeed. - /// An instance of , initialized to the value specified in . - public static SemanticVersion Parse(string semanticVersion) - { - SemanticVersion result; - Requires.Argument(TryParse(semanticVersion, out result), nameof(semanticVersion), "Unrecognized or unsupported semantic version."); - return result; - } + /// + /// Checks equality against another object. + /// + /// The other instance. + /// if the instances have equal values; otherwise. + public override bool Equals(object obj) + { + return this.Equals(obj as SemanticVersion); + } - /// - /// Checks equality against another object. - /// - /// The other instance. - /// true if the instances have equal values; false otherwise. - public override bool Equals(object obj) + /// + /// Gets a hash code for this instance. + /// + /// The hash code. + public override int GetHashCode() + { + return this.Version.GetHashCode() + this.Prerelease.GetHashCode(); + } + + /// + /// Prints this instance as a string. + /// + /// A string representation of this object. + public override string ToString() + { + return this.Version + this.Prerelease + this.BuildMetadata; + } + + /// + /// Checks equality against another instance of this class. + /// + /// The other instance. + /// if the instances have equal values; otherwise. + public bool Equals(SemanticVersion other) + { + if (other is null) { - return this.Equals(obj as SemanticVersion); + return false; } - /// - /// Gets a hash code for this instance. - /// - /// The hash code. - public override int GetHashCode() + return this.Version == other.Version + && this.Prerelease == other.Prerelease + && this.BuildMetadata == other.BuildMetadata; + } + + /// + /// Tests whether two instances are compatible enough that version height is not reset + /// when progressing from one to the next. + /// + /// The first semantic version. + /// The second semantic version. + /// The position within the version where height is tracked. + /// if transitioning from one version to the next should reset the version height; otherwise. + internal static bool WillVersionChangeResetVersionHeight(SemanticVersion first, SemanticVersion second, SemanticVersion.Position versionHeightPosition) + { + Requires.NotNull(first, nameof(first)); + Requires.NotNull(second, nameof(second)); + + if (first == second) { - return this.Version.GetHashCode() + this.Prerelease.GetHashCode(); + return false; } - /// - /// Prints this instance as a string. - /// - /// A string representation of this object. - public override string ToString() + if (versionHeightPosition == SemanticVersion.Position.Prerelease) { - return this.Version + this.Prerelease + this.BuildMetadata; + // The entire version spec must match exactly. + return !first.Equals(second); } - /// - /// Checks equality against another instance of this class. - /// - /// The other instance. - /// true if the instances have equal values; false otherwise. - public bool Equals(SemanticVersion other) + for (SemanticVersion.Position position = SemanticVersion.Position.Major; position <= versionHeightPosition; position++) { - if (other is null) + int expectedValue = ReadVersionPosition(second.Version, position); + int actualValue = ReadVersionPosition(first.Version, position); + if (expectedValue != actualValue) { - return false; + return true; } - - return this.Version == other.Version - && this.Prerelease == other.Prerelease - && this.BuildMetadata == other.BuildMetadata; } - /// - /// Tests whether two instances are compatible enough that version height is not reset - /// when progressing from one to the next. - /// - /// The first semantic version. - /// The second semantic version. - /// The position within the version where height is tracked. - /// true if transitioning from one version to the next should reset the version height; false otherwise. - internal static bool WillVersionChangeResetVersionHeight(SemanticVersion first, SemanticVersion second, SemanticVersion.Position versionHeightPosition) - { - Requires.NotNull(first, nameof(first)); - Requires.NotNull(second, nameof(second)); - - if (first == second) - { - return false; - } + return false; + } - if (versionHeightPosition == SemanticVersion.Position.Prerelease) - { - // The entire version spec must match exactly. - return !first.Equals(second); - } + internal static int ReadVersionPosition(Version version, Position position) + { + Requires.NotNull(version, nameof(version)); - for (SemanticVersion.Position position = SemanticVersion.Position.Major; position <= versionHeightPosition; position++) - { - int expectedValue = ReadVersionPosition(second.Version, position); - int actualValue = ReadVersionPosition(first.Version, position); - if (expectedValue != actualValue) - { - return true; - } - } + return position switch + { + Position.Major => version.Major, + Position.Minor => version.Minor, + Position.Build => version.Build, + Position.Revision => version.Revision, + _ => throw new ArgumentOutOfRangeException(nameof(position), position, "Must be one of the 4 integer parts."), + }; + } - return false; - } + internal int ReadVersionPosition(Position position) => ReadVersionPosition(this.Version, position); - internal static int ReadVersionPosition(Version version, Position position) + /// + /// Checks whether a given version may have been produced by this semantic version. + /// + /// The version to test. + /// if the is a match; otherwise. + internal bool IsMatchingVersion(Version version) + { + Position lastPositionToConsider = Position.Revision; + if (this.VersionHeightPosition <= lastPositionToConsider) { - Requires.NotNull(version, nameof(version)); - - return position switch - { - Position.Major => version.Major, - Position.Minor => version.Minor, - Position.Build => version.Build, - Position.Revision => version.Revision, - _ => throw new ArgumentOutOfRangeException(nameof(position), position, "Must be one of the 4 integer parts."), - }; + lastPositionToConsider = this.VersionHeightPosition.Value - 1; } - internal int ReadVersionPosition(Position position) => ReadVersionPosition(this.Version, position); - - /// - /// Checks whether a given version may have been produced by this semantic version. - /// - /// The version to test. - /// if the is a match; otherwise. - internal bool IsMatchingVersion(Version version) + for (Position i = Position.Major; i <= lastPositionToConsider; i++) { - Position lastPositionToConsider = Position.Revision; - if (this.VersionHeightPosition <= lastPositionToConsider) - { - lastPositionToConsider = this.VersionHeightPosition.Value - 1; - } - - for (Position i = Position.Major; i <= lastPositionToConsider; i++) + if (this.ReadVersionPosition(i) != ReadVersionPosition(version, i)) { - if (this.ReadVersionPosition(i) != ReadVersionPosition(version, i)) - { - return false; - } + return false; } - - return true; } - /// - /// Checks whether a particular version number - /// belongs to the set of versions represented by this semantic version spec. - /// - /// A version, with major and minor components, and possibly build and/or revision components. - /// true if may have been produced by this semantic version; false otherwise. - internal bool Contains(Version version) - { - return - this.Version.Major == version.Major && - this.Version.Minor == version.Minor && - (this.Version.Build == -1 || this.Version.Build == version.Build) && - (this.Version.Revision == -1 || this.Version.Revision == version.Revision); - } + return true; + } - /// - /// Verifies that the prerelease tag follows semver rules. - /// - /// The input string to test. - /// The regex that the string must conform to. - /// The name of the parameter supplying the . - /// - /// Thrown if the does not match the required . - /// - private static void VerifyPatternMatch(string input, Regex pattern, string parameterName) - { - Requires.NotNull(pattern, nameof(pattern)); + /// + /// Checks whether a particular version number + /// belongs to the set of versions represented by this semantic version spec. + /// + /// A version, with major and minor components, and possibly build and/or revision components. + /// if may have been produced by this semantic version; otherwise. + internal bool Contains(Version version) + { + return + this.Version.Major == version.Major && + this.Version.Minor == version.Minor && + (this.Version.Build == -1 || this.Version.Build == version.Build) && + (this.Version.Revision == -1 || this.Version.Revision == version.Revision); + } - if (string.IsNullOrEmpty(input)) - { - return; - } + /// + /// Verifies that the prerelease tag follows semver rules. + /// + /// The input string to test. + /// The regex that the string must conform to. + /// The name of the parameter supplying the . + /// + /// Thrown if the does not match the required . + /// + private static void VerifyPatternMatch(string input, Regex pattern, string parameterName) + { + Requires.NotNull(pattern, nameof(pattern)); - Requires.Argument(pattern.IsMatch(input), parameterName, $"The prerelease must match the pattern \"{pattern}\"."); + if (string.IsNullOrEmpty(input)) + { + return; } + + Requires.Argument(pattern.IsMatch(input), parameterName, $"The prerelease must match the pattern \"{pattern}\"."); } } diff --git a/src/NerdBank.GitVersioning/SemanticVersionJsonConverter.cs b/src/NerdBank.GitVersioning/SemanticVersionJsonConverter.cs index a4ae7f34..96af49e8 100644 --- a/src/NerdBank.GitVersioning/SemanticVersionJsonConverter.cs +++ b/src/NerdBank.GitVersioning/SemanticVersionJsonConverter.cs @@ -1,44 +1,48 @@ -namespace Nerdbank.GitVersioning -{ - using System; - using System.Reflection; - using Newtonsoft.Json; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Reflection; +using Newtonsoft.Json; + +namespace Nerdbank.GitVersioning; - internal class SemanticVersionJsonConverter : JsonConverter +internal class SemanticVersionJsonConverter : JsonConverter +{ + /// + public override bool CanConvert(Type objectType) { - public override bool CanConvert(Type objectType) - { - return typeof(SemanticVersion).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo()); - } + return typeof(SemanticVersion).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo()); + } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + /// + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (objectType.Equals(typeof(SemanticVersion)) && reader.Value is string) { - if (objectType.Equals(typeof(SemanticVersion)) && reader.Value is string) + SemanticVersion value; + if (SemanticVersion.TryParse((string)reader.Value, out value)) { - SemanticVersion value; - if (SemanticVersion.TryParse((string)reader.Value, out value)) - { - return value; - } - else - { - throw new FormatException($"The value \"{reader.Value}\" is not a valid semantic version."); - } + return value; } - - throw new NotSupportedException(); - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - var version = value as SemanticVersion; - if (version is not null) + else { - writer.WriteValue(version.ToString()); - return; + throw new FormatException($"The value \"{reader.Value}\" is not a valid semantic version."); } + } - throw new NotSupportedException(); + throw new NotSupportedException(); + } + + /// + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var version = value as SemanticVersion; + if (version is not null) + { + writer.WriteValue(version.ToString()); + return; } + + throw new NotSupportedException(); } } diff --git a/src/NerdBank.GitVersioning/VersionExtensions.cs b/src/NerdBank.GitVersioning/VersionExtensions.cs index 792964df..b9097b9c 100644 --- a/src/NerdBank.GitVersioning/VersionExtensions.cs +++ b/src/NerdBank.GitVersioning/VersionExtensions.cs @@ -1,90 +1,91 @@ -namespace Nerdbank.GitVersioning -{ - using System; - using Validation; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Validation; + +namespace Nerdbank.GitVersioning; +/// +/// Extension methods for the class. +/// +public static class VersionExtensions +{ /// - /// Extension methods for the class. + /// Returns a instance where the specified number of components + /// are guaranteed to be non-negative. Any applicable negative components are converted to zeros. /// - public static class VersionExtensions + /// The version to use as a template for the returned value. + /// The number of version components to ensure are non-negative. + /// + /// The same as except with any applicable negative values + /// translated to zeros. + /// + public static Version EnsureNonNegativeComponents(this Version version, int fieldCount = 4) { - /// - /// Returns a instance where the specified number of components - /// are guaranteed to be non-negative. Any applicable negative components are converted to zeros. - /// - /// The version to use as a template for the returned value. - /// The number of version components to ensure are non-negative. - /// - /// The same as except with any applicable negative values - /// translated to zeros. - /// - public static Version EnsureNonNegativeComponents(this Version version, int fieldCount = 4) - { - Requires.NotNull(version, nameof(version)); - Requires.Range(fieldCount >= 0 && fieldCount <= 4, nameof(fieldCount)); + Requires.NotNull(version, nameof(version)); + Requires.Range(fieldCount >= 0 && fieldCount <= 4, nameof(fieldCount)); - int maj = fieldCount >= 1 ? Math.Max(0, version.Major) : version.Major; - int min = fieldCount >= 2 ? Math.Max(0, version.Minor) : version.Minor; - int bld = fieldCount >= 3 ? Math.Max(0, version.Build) : version.Build; - int rev = fieldCount >= 4 ? Math.Max(0, version.Revision) : version.Revision; + int maj = fieldCount >= 1 ? Math.Max(0, version.Major) : version.Major; + int min = fieldCount >= 2 ? Math.Max(0, version.Minor) : version.Minor; + int bld = fieldCount >= 3 ? Math.Max(0, version.Build) : version.Build; + int rev = fieldCount >= 4 ? Math.Max(0, version.Revision) : version.Revision; - if (version.Major == maj && - version.Minor == min && - version.Build == bld && - version.Revision == rev) - { - return version; - } - - if (rev >= 0) - { - return new Version(maj, min, bld, rev); - } - else if (bld >= 0) - { - return new Version(maj, min, bld); - } - else - { - throw Assumes.NotReachable(); - } + if (version.Major == maj && + version.Minor == min && + version.Build == bld && + version.Revision == rev) + { + return version; } - /// - /// Converts the value of the current System.Version object to its equivalent System.String - /// representation. A specified count indicates the number of components to return. - /// - /// The instance to serialize as a string. - /// The number of components to return. The fieldCount ranges from 0 to 4. - /// - /// The System.String representation of the values of the major, minor, build, and - /// revision components of the current System.Version object, each separated by a - /// period character ('.'). The fieldCount parameter determines how many components - /// are returned.fieldCount Return Value 0 An empty string (""). 1 major 2 major.minor - /// 3 major.minor.build 4 major.minor.build.revision For example, if you create System.Version - /// object using the constructor Version(1,3,5), ToString(2) returns "1.3" and ToString(4) - /// returns "1.3.5.0". - /// - public static string ToStringSafe(this Version version, int fieldCount) + if (rev >= 0) { - return version.EnsureNonNegativeComponents(fieldCount).ToString(fieldCount); + return new Version(maj, min, bld, rev); } - - /// - /// Initializes a new instance of the class, - /// allowing for the last two integers to possibly be -1. - /// - /// The major version. - /// The minor version. - /// The build version. - /// The revision. - /// - internal static Version Create(int major, int minor, int build, int revision) + else if (bld >= 0) + { + return new Version(maj, min, bld); + } + else { - return - build == -1 ? new Version(major, minor) : - revision == -1 ? new Version(major, minor, build) : - new Version(major, minor, build, revision); + throw Assumes.NotReachable(); } } + + /// + /// Converts the value of the current System.Version object to its equivalent System.String + /// representation. A specified count indicates the number of components to return. + /// + /// The instance to serialize as a string. + /// The number of components to return. The fieldCount ranges from 0 to 4. + /// + /// The System.String representation of the values of the major, minor, build, and + /// revision components of the current System.Version object, each separated by a + /// period character ('.'). The fieldCount parameter determines how many components + /// are returned.fieldCount Return Value 0 An empty string (""). 1 major 2 major.minor + /// 3 major.minor.build 4 major.minor.build.revision For example, if you create System.Version + /// object using the constructor Version(1,3,5), ToString(2) returns "1.3" and ToString(4) + /// returns "1.3.5.0". + /// + public static string ToStringSafe(this Version version, int fieldCount) + { + return version.EnsureNonNegativeComponents(fieldCount).ToString(fieldCount); + } + + /// + /// Initializes a new instance of the class, + /// allowing for the last two integers to possibly be -1. + /// + /// The major version. + /// The minor version. + /// The build version. + /// The revision. + /// The newly created . + internal static Version Create(int major, int minor, int build, int revision) + { + return + build == -1 ? new Version(major, minor) : + revision == -1 ? new Version(major, minor, build) : + new Version(major, minor, build, revision); + } } diff --git a/src/NerdBank.GitVersioning/VersionFile.cs b/src/NerdBank.GitVersioning/VersionFile.cs index 936fbb26..02f78a94 100644 --- a/src/NerdBank.GitVersioning/VersionFile.cs +++ b/src/NerdBank.GitVersioning/VersionFile.cs @@ -1,245 +1,244 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable -using System; -using System.IO; using Newtonsoft.Json; using Validation; -namespace Nerdbank.GitVersioning +namespace Nerdbank.GitVersioning; + +/// +/// Exposes queries and mutations on a version.json or version.txt file. +/// +public abstract class VersionFile { /// - /// Exposes queries and mutations on a version.json or version.txt file. + /// The filename of the version.txt file. + /// + public const string TxtFileName = "version.txt"; + + /// + /// The filename of the version.json file. /// - public abstract class VersionFile + public const string JsonFileName = "version.json"; + + /// + /// Initializes a new instance of the class. + /// + /// The git context to use when reading version files. + protected VersionFile(GitContext context) { - /// - /// The filename of the version.txt file. - /// - public const string TxtFileName = "version.txt"; - - /// - /// The filename of the version.json file. - /// - public const string JsonFileName = "version.json"; - - /// - /// Initializes a new instance of the class. - /// - /// The git context to use when reading version files. - protected VersionFile(GitContext context) - { - this.Context = context; - } + this.Context = context; + } + + /// + /// Gets the git context to use when reading version files. + /// + protected GitContext Context { get; } + + /// + /// Checks whether a version file is defined. + /// + /// if the version file is found; otherwise . + public bool IsVersionDefined() => this.GetVersion() is object; + + /// + public VersionOptions? GetWorkingCopyVersion() => this.GetWorkingCopyVersion(out _); + + /// + /// Reads the version file from the working tree and returns the deserialized from it. + /// + /// Set to the actual directory that the version file was found in, which may be or one of its ancestors. + /// The version information read from the file, or if the file wasn't found. + public VersionOptions? GetWorkingCopyVersion(out string? actualDirectory) => this.GetWorkingCopyVersion(this.Context.AbsoluteProjectDirectory, out actualDirectory); - /// - /// Gets the git context to use when reading version files. - /// - protected GitContext Context { get; } - - /// - /// Checks whether a version file is defined. - /// - /// true if the version file is found; otherwise false. - public bool IsVersionDefined() => this.GetVersion() is object; - - /// - public VersionOptions? GetWorkingCopyVersion() => this.GetWorkingCopyVersion(out string _); - - /// - /// Reads the version file from the working tree and returns the deserialized from it. - /// - /// Set to the actual directory that the version file was found in, which may be or one of its ancestors. - /// The version information read from the file, or null if the file wasn't found. - public VersionOptions? GetWorkingCopyVersion(out string? actualDirectory) => this.GetWorkingCopyVersion(this.Context.AbsoluteProjectDirectory, out actualDirectory); - - /// - /// The optional unstable tag to include in the file. + /// + /// The optional unstable tag to include in the file. #pragma warning disable CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do) - public string SetVersion(string projectDirectory, System.Version version, string? unstableTag = null, bool includeSchemaProperty = false) + public string SetVersion(string projectDirectory, System.Version version, string? unstableTag = null, bool includeSchemaProperty = false) #pragma warning restore CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do) - { - return this.SetVersion(projectDirectory, VersionOptions.FromVersion(version, unstableTag), includeSchemaProperty); - } + { + return this.SetVersion(projectDirectory, VersionOptions.FromVersion(version, unstableTag), includeSchemaProperty); + } - /// - /// Writes the version.json file to a directory within a repo with the specified version information. - /// - /// - /// The path to the directory in which to write the version.json file. - /// The file's impact will be all descendent projects and directories from this specified directory, - /// except where any of those directories have their own version.json file. - /// - /// The version information to write to the file. - /// A value indicating whether to serialize the $schema property for easier editing in most JSON editors. - /// The path to the file written. - public string SetVersion(string projectDirectory, VersionOptions version, bool includeSchemaProperty = true) - { - Requires.NotNullOrEmpty(projectDirectory, nameof(projectDirectory)); - Requires.NotNull(version, nameof(version)); - Requires.Argument(version.Version is object || version.Inherit, nameof(version), $"{nameof(VersionOptions.Version)} must be set for a root-level version.json file."); + /// + /// Writes the version.json file to a directory within a repo with the specified version information. + /// + /// + /// The path to the directory in which to write the version.json file. + /// The file's impact will be all descendent projects and directories from this specified directory, + /// except where any of those directories have their own version.json file. + /// + /// The version information to write to the file. + /// A value indicating whether to serialize the $schema property for easier editing in most JSON editors. + /// The path to the file written. + public string SetVersion(string projectDirectory, VersionOptions version, bool includeSchemaProperty = true) + { + Requires.NotNullOrEmpty(projectDirectory, nameof(projectDirectory)); + Requires.NotNull(version, nameof(version)); + Requires.Argument(version.Version is object || version.Inherit, nameof(version), $"{nameof(VersionOptions.Version)} must be set for a root-level version.json file."); - Directory.CreateDirectory(projectDirectory); + Directory.CreateDirectory(projectDirectory); - string versionTxtPath = Path.Combine(projectDirectory, TxtFileName); - if (File.Exists(versionTxtPath)) + string versionTxtPath = Path.Combine(projectDirectory, TxtFileName); + if (File.Exists(versionTxtPath)) + { + if (version.IsDefaultVersionTheOnlyPropertySet) { - if (version.IsDefaultVersionTheOnlyPropertySet) - { - File.WriteAllLines( - versionTxtPath, - new[] { version.Version?.Version.ToString(), version.Version?.Prerelease }); - return versionTxtPath; - } - else - { - // The file must be upgraded to use the more descriptive JSON format. - File.Delete(versionTxtPath); - } + File.WriteAllLines( + versionTxtPath, + new[] { version.Version?.Version.ToString() ?? string.Empty, version.Version?.Prerelease ?? string.Empty }); + return versionTxtPath; + } + else + { + // The file must be upgraded to use the more descriptive JSON format. + File.Delete(versionTxtPath); } - - string repoRelativeProjectDirectory = this.Context.GetRepoRelativePath(projectDirectory); - string versionJsonPath = Path.Combine(projectDirectory, JsonFileName); - string jsonContent = JsonConvert.SerializeObject( - version, - VersionOptions.GetJsonSettings(version.Inherit, includeSchemaProperty, repoRelativeProjectDirectory)); - File.WriteAllText(versionJsonPath, jsonContent); - return versionJsonPath; } - /// - /// Reads the version file from in the and returns the deserialized from it. - /// - /// Receives the absolute path to the directory where the version file was found, if any. - /// The version information read from the file, or if the file wasn't found. - /// This method is only called if is not null. - protected abstract VersionOptions? GetVersionCore(out string? actualDirectory); - - /// - public VersionOptions? GetVersion() => this.GetVersion(out string? actualDirectory); - - /// - /// Reads the version file from the selected git commit (or working copy if no commit is selected) and returns the deserialized from it. - /// - /// Receives the absolute path to the directory where the version file was found, if any. - /// The version information read from the file, or if the file wasn't found. - public VersionOptions? GetVersion(out string? actualDirectory) + string repoRelativeProjectDirectory = this.Context.GetRepoRelativePath(projectDirectory); + string versionJsonPath = Path.Combine(projectDirectory, JsonFileName); + string jsonContent = JsonConvert.SerializeObject( + version, + VersionOptions.GetJsonSettings(version.Inherit, includeSchemaProperty, repoRelativeProjectDirectory)); + File.WriteAllText(versionJsonPath, jsonContent); + return versionJsonPath; + } + + /// + public VersionOptions? GetVersion() => this.GetVersion(out string? actualDirectory); + + /// + /// Reads the version file from the selected git commit (or working copy if no commit is selected) and returns the deserialized from it. + /// + /// Receives the absolute path to the directory where the version file was found, if any. + /// The version information read from the file, or if the file wasn't found. + public VersionOptions? GetVersion(out string? actualDirectory) + { + return this.Context.GitCommitId is null + ? this.GetWorkingCopyVersion(out actualDirectory) + : this.GetVersionCore(out actualDirectory); + } + + /// + /// Tries to read a version.json file from the specified string, but favors returning null instead of throwing a . + /// + /// The content of the version.json file. + /// Directory that this version.json file is relative to the root of the repository. + /// The deserialized object, if deserialization was successful. + protected static VersionOptions? TryReadVersionJsonContent(string jsonContent, string? repoRelativeBaseDirectory) + { + try { - return this.Context.GitCommitId is null - ? this.GetWorkingCopyVersion(out actualDirectory) - : this.GetVersionCore(out actualDirectory); + return JsonConvert.DeserializeObject(jsonContent, VersionOptions.GetJsonSettings(repoRelativeBaseDirectory: repoRelativeBaseDirectory)); } + catch (JsonSerializationException) + { + return null; + } + } - /// - /// Tries to read a version.json file from the specified string, but favors returning null instead of throwing a . - /// - /// The content of the version.json file. - /// Directory that this version.json file is relative to the root of the repository. - /// The deserialized object, if deserialization was successful. - protected static VersionOptions? TryReadVersionJsonContent(string jsonContent, string? repoRelativeBaseDirectory) + /// + /// Reads the version.txt file and returns the and prerelease tag from it. + /// + /// The content of the version.txt file to read. + /// The version information read from the file; or if a deserialization error occurs. + protected static VersionOptions TryReadVersionFile(TextReader versionTextContent) + { + string? versionLine = versionTextContent.ReadLine(); + string? prereleaseVersion = versionTextContent.ReadLine(); + if (!string.IsNullOrEmpty(prereleaseVersion)) { - try - { - return JsonConvert.DeserializeObject(jsonContent, VersionOptions.GetJsonSettings(repoRelativeBaseDirectory: repoRelativeBaseDirectory)); - } - catch (JsonSerializationException) + if (!prereleaseVersion.StartsWith("-")) { - return null; + // SemVer requires that prerelease suffixes begin with a hyphen, so add one if it's missing. + prereleaseVersion = "-" + prereleaseVersion; } } - /// - /// Reads the version.txt file and returns the and prerelease tag from it. - /// - /// The content of the version.txt file to read. - /// The version information read from the file; or null if a deserialization error occurs. - protected static VersionOptions TryReadVersionFile(TextReader versionTextContent) + SemanticVersion semVer; + Verify.Operation(SemanticVersion.TryParse(versionLine + prereleaseVersion, out semVer), "Unrecognized version format."); + return new VersionOptions + { + Version = semVer, + }; + } + + /// + /// Reads the version file from in the and returns the deserialized from it. + /// + /// Receives the absolute path to the directory where the version file was found, if any. + /// The version information read from the file, or if the file wasn't found. + /// This method is only called if is not null. + protected abstract VersionOptions? GetVersionCore(out string? actualDirectory); + + /// + /// Reads a version file from the working tree, without any regard to a git repo. + /// + /// The path to start the search from. + /// Receives the directory where the version file was found. + /// The version options, if found. + protected VersionOptions? GetWorkingCopyVersion(string startingDirectory, out string? actualDirectory) + { + string? searchDirectory = startingDirectory; + while (searchDirectory is object) { - string? versionLine = versionTextContent.ReadLine(); - string? prereleaseVersion = versionTextContent.ReadLine(); - if (!string.IsNullOrEmpty(prereleaseVersion)) + // Do not search above the working tree root. + string? parentDirectory = string.Equals(searchDirectory, this.Context.WorkingTreePath, StringComparison.OrdinalIgnoreCase) + ? null + : Path.GetDirectoryName(searchDirectory); + string versionTxtPath = Path.Combine(searchDirectory, TxtFileName); + if (File.Exists(versionTxtPath)) { - if (!prereleaseVersion.StartsWith("-")) + using var sr = new StreamReader(File.OpenRead(versionTxtPath)); + VersionOptions? result = TryReadVersionFile(sr); + if (result is object) { - // SemVer requires that prerelease suffixes begin with a hyphen, so add one if it's missing. - prereleaseVersion = "-" + prereleaseVersion; + actualDirectory = searchDirectory; + return result; } } - SemanticVersion semVer; - Verify.Operation(SemanticVersion.TryParse(versionLine + prereleaseVersion, out semVer), "Unrecognized version format."); - return new VersionOptions + string versionJsonPath = Path.Combine(searchDirectory, JsonFileName); + if (File.Exists(versionJsonPath)) { - Version = semVer, - }; - } + string versionJsonContent = File.ReadAllText(versionJsonPath); - /// - /// Reads a version file from the working tree, without any regard to a git repo. - /// - /// The path to start the search from. - /// Receives the directory where the version file was found. - /// The version options, if found. - protected VersionOptions? GetWorkingCopyVersion(string startingDirectory, out string? actualDirectory) - { - string? searchDirectory = startingDirectory; - while (searchDirectory is object) - { - // Do not search above the working tree root. - string? parentDirectory = string.Equals(searchDirectory, this.Context.WorkingTreePath, StringComparison.OrdinalIgnoreCase) - ? null - : Path.GetDirectoryName(searchDirectory); - string versionTxtPath = Path.Combine(searchDirectory, TxtFileName); - if (File.Exists(versionTxtPath)) + string? repoRelativeBaseDirectory = this.Context.GetRepoRelativePath(searchDirectory); + VersionOptions? result = + TryReadVersionJsonContent(versionJsonContent, repoRelativeBaseDirectory); + if (result?.Inherit ?? false) { - using (var sr = new StreamReader(File.OpenRead(versionTxtPath))) + if (parentDirectory is object) { - var result = TryReadVersionFile(sr); + result = this.GetWorkingCopyVersion(parentDirectory, out _); if (result is object) { + JsonConvert.PopulateObject( + versionJsonContent, + result, + VersionOptions.GetJsonSettings(repoRelativeBaseDirectory: repoRelativeBaseDirectory)); actualDirectory = searchDirectory; return result; } } - } - string versionJsonPath = Path.Combine(searchDirectory, JsonFileName); - if (File.Exists(versionJsonPath)) + throw new InvalidOperationException( + $"\"{versionJsonPath}\" inherits from a parent directory version.json file but none exists."); + } + else if (result is object) { - string versionJsonContent = File.ReadAllText(versionJsonPath); - - var repoRelativeBaseDirectory = this.Context.GetRepoRelativePath(searchDirectory); - VersionOptions? result = - TryReadVersionJsonContent(versionJsonContent, repoRelativeBaseDirectory); - if (result?.Inherit ?? false) - { - if (parentDirectory is object) - { - result = this.GetWorkingCopyVersion(parentDirectory, out string _); - if (result is object) - { - JsonConvert.PopulateObject(versionJsonContent, result, - VersionOptions.GetJsonSettings( - repoRelativeBaseDirectory: repoRelativeBaseDirectory)); - actualDirectory = searchDirectory; - return result; - } - } - - throw new InvalidOperationException( - $"\"{versionJsonPath}\" inherits from a parent directory version.json file but none exists."); - } - else if (result is object) - { - actualDirectory = searchDirectory; - return result; - } + actualDirectory = searchDirectory; + return result; } - - searchDirectory = parentDirectory; } - actualDirectory = null; - return null; + searchDirectory = parentDirectory; } + + actualDirectory = null; + return null; } } diff --git a/src/NerdBank.GitVersioning/VersionOptions.cs b/src/NerdBank.GitVersioning/VersionOptions.cs index b2af20bd..7e47f06c 100644 --- a/src/NerdBank.GitVersioning/VersionOptions.cs +++ b/src/NerdBank.GitVersioning/VersionOptions.cs @@ -1,533 +1,1074 @@ -#nullable enable - -namespace Nerdbank.GitVersioning +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; +using Validation; +using EditorBrowsableAttribute = System.ComponentModel.EditorBrowsableAttribute; +using EditorBrowsableState = System.ComponentModel.EditorBrowsableState; + +#nullable enable + +namespace Nerdbank.GitVersioning; + +/// +/// Describes the various versions and options required for the build. +/// +[DebuggerDisplay("{DebuggerDisplay,nq}")] +public class VersionOptions : IEquatable { - using System; - using System.Collections.Generic; - using System.Collections.ObjectModel; - using System.Diagnostics; - using System.Linq; - using System.Reflection; - using Newtonsoft.Json; - using Newtonsoft.Json.Converters; - using Newtonsoft.Json.Serialization; - using Validation; - using EditorBrowsableAttribute = System.ComponentModel.EditorBrowsableAttribute; - using EditorBrowsableState = System.ComponentModel.EditorBrowsableState; - - /// - /// Describes the various versions and options required for the build. - /// - [DebuggerDisplay("{DebuggerDisplay,nq}")] - public class VersionOptions : IEquatable + /// + /// Default value for . + /// + public const VersionPrecision DefaultVersionPrecision = VersionPrecision.Minor; + + /// + /// The placeholder that may appear in the property's + /// to specify where the version height should appear in a computed semantic version. + /// + /// + /// When this macro does not appear in the string, the version height is set as the first unspecified integer of the 4-integer version. + /// If all 4 integers in a version are specified, and the macro does not appear, the version height isn't inserted anywhere. + /// + public const string VersionHeightPlaceholder = "{height}"; + + /// + /// The default value for the property. + /// + public const int DefaultGitCommitIdShortFixedLength = 10; + + /// + /// The default value for the property. + /// + private const int DefaultSemVer1NumericIdentifierPadding = 4; + + /// + /// A value indicating whether mutations of this instance are not allowed. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private bool isFrozen; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string? gitCommitIdPrefix; + + /// + /// Backing field for the property. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private SemanticVersion? version; + + /// + /// Backing field for the property. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private AssemblyVersionOptions? assemblyVersion; + + /// + /// Backing field for the property. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private int? buildNumberOffset; + + /// + /// Backing field for the property. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private int? semVer1NumericIdentifierPadding; + + /// + /// Backing field for the property. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private int? gitCommitIdShortFixedLength; + + /// + /// Backing field for the property. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private int? gitCommitIdShortAutoMinimum; + + /// + /// Backing field for the property. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private NuGetPackageVersionOptions? nuGetPackageVersion; + + /// + /// Backing field for the property. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private IReadOnlyList? publicReleaseRefSpec; + + /// + /// Backing field for the property. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private CloudBuildOptions? cloudBuild; + + /// + /// Backing field for the property. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private ReleaseOptions? release; + + /// + /// Backing field for the property. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private IReadOnlyList? pathFilters; + + /// + /// Backing field for the property. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private bool inherit; + + /// + /// Initializes a new instance of the class. + /// + public VersionOptions() { - /// - /// A value indicating whether mutations of this instance are not allowed. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private bool isFrozen; + } - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private string? gitCommitIdPrefix; + /// + /// Initializes a new instance of the class. + /// + /// Another instance to copy values from. + public VersionOptions(VersionOptions copyFrom) + { + Requires.NotNull(copyFrom, nameof(copyFrom)); + + this.gitCommitIdPrefix = copyFrom.gitCommitIdPrefix; + this.version = copyFrom.version; + this.assemblyVersion = copyFrom.assemblyVersion is object ? new AssemblyVersionOptions(copyFrom.assemblyVersion) : null; + this.buildNumberOffset = copyFrom.buildNumberOffset; + this.semVer1NumericIdentifierPadding = copyFrom.semVer1NumericIdentifierPadding; + this.gitCommitIdShortFixedLength = copyFrom.gitCommitIdShortFixedLength; + this.gitCommitIdShortAutoMinimum = copyFrom.gitCommitIdShortAutoMinimum; + this.nuGetPackageVersion = copyFrom.nuGetPackageVersion is object ? new NuGetPackageVersionOptions(copyFrom.nuGetPackageVersion) : null; + this.publicReleaseRefSpec = copyFrom.publicReleaseRefSpec?.ToList(); + this.cloudBuild = copyFrom.cloudBuild is object ? new CloudBuildOptions(copyFrom.cloudBuild) : null; + this.release = copyFrom.release is object ? new ReleaseOptions(copyFrom.release) : null; + this.pathFilters = copyFrom.pathFilters?.ToList(); + } + /// + /// The last component to control in a 4 integer version. + /// + public enum VersionPrecision + { /// - /// Backing field for the property. + /// The first integer is the last number set. The rest will be zeros. /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private SemanticVersion? version; + Major, /// - /// Backing field for the property. + /// The second integer is the last number set. The rest will be zeros. /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private AssemblyVersionOptions? assemblyVersion; + Minor, /// - /// Backing field for the property. + /// The third integer is the last number set. The fourth will be zero. /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private int? buildNumberOffset; + Build, /// - /// Backing field for the property. + /// All four integers will be set. /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private int? semVer1NumericIdentifierPadding; + Revision, + } + /// + /// The conditions a commit ID is included in a cloud build number. + /// + public enum CloudBuildNumberCommitWhen + { /// - /// Backing field for the property. + /// Always include the commit information in the cloud Build Number. /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private int? gitCommitIdShortFixedLength; + Always, /// - /// Backing field for the property. + /// Only include the commit information when building a non-PublicRelease. /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private int? gitCommitIdShortAutoMinimum; + NonPublicReleaseOnly, /// - /// Backing field for the property. + /// Never include the commit information. /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private NuGetPackageVersionOptions? nuGetPackageVersion; + Never, + } + /// + /// The position a commit ID can appear in a cloud build number. + /// + public enum CloudBuildNumberCommitWhere + { /// - /// Backing field for the property. + /// The commit ID appears in build metadata (e.g. +ga1b2c3). /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private IReadOnlyList? publicReleaseRefSpec; + BuildMetadata, /// - /// Backing field for the property. + /// The commit ID appears as the 4th integer in the version (e.g. 1.2.3.23523). /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private CloudBuildOptions? cloudBuild; + FourthVersionComponent, + } + /// + /// Possible increments of the version after creating release branches. + /// + public enum ReleaseVersionIncrement + { /// - /// Backing field for the property. + /// Increment the major version after creating a release branch. /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private ReleaseOptions? release; + Major, /// - /// Backing field for the property. + /// Increment the minor version after creating a release branch. /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private IReadOnlyList? pathFilters; + Minor, /// - /// Backing field for the property. + /// Increment the build number (the third number in a version) after creating a release branch. /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private bool inherit; + Build, + } - /// - /// Default value for . - /// - public const VersionPrecision DefaultVersionPrecision = VersionPrecision.Minor; + /// + /// Gets the $schema field that should be serialized when writing. + /// + [JsonProperty(PropertyName = "$schema")] + public string Schema => "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json"; - /// - /// The placeholder that may appear in the property's - /// to specify where the version height should appear in a computed semantic version. - /// - /// - /// When this macro does not appear in the string, the version height is set as the first unspecified integer of the 4-integer version. - /// If all 4 integers in a version are specified, and the macro does not appear, the version height isn't inserted anywhere. - /// - public const string VersionHeightPlaceholder = "{height}"; + /// + /// Gets or sets the default version to use. + /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public SemanticVersion? Version + { + get => this.version; + set => this.SetIfNotReadOnly(ref this.version, value); + } - /// - /// The default value for the property. - /// - private const int DefaultSemVer1NumericIdentifierPadding = 4; + /// + /// Gets or sets the version to use particularly for the + /// instead of the default . + /// + /// An instance of or to simply use the default . + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public AssemblyVersionOptions? AssemblyVersion + { + get => this.assemblyVersion; + set => this.SetIfNotReadOnly(ref this.assemblyVersion, value); + } + + /// + /// Gets or sets the prefix for git commit id in version. + /// Because of semver rules the prefix must lead with a [A-z_] character (not a number) and it cannot be the empty string. + /// If 'g' will be used. + /// + /// A prefix for git commit id. + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public string? GitCommitIdPrefix + { + get => this.gitCommitIdPrefix; + set + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentNullException(nameof(value), $"{nameof(this.GitCommitIdPrefix)} can't be empty"); + } + + char first = value![0]; + if (first < 'A' || (first > 'Z' && first < 'a' && first != '_') || first > 'z') + { + throw new ArgumentException(nameof(value), $"{nameof(this.GitCommitIdPrefix)} must lead with a [A-z_] character (not a number)"); + } + + this.SetIfNotReadOnly(ref this.gitCommitIdPrefix, value); + } + } + + /// + /// Gets the version to use particularly for the + /// instead of the default . + /// + /// An instance of or to simply use the default . + [JsonIgnore] + public AssemblyVersionOptions AssemblyVersionOrDefault => this.AssemblyVersion ?? AssemblyVersionOptions.DefaultInstance; + + /// + /// Gets or sets a number to add to the git height when calculating the version height, + /// which typically is used in the portion of the computed version. + /// + /// Any integer (0, positive, or negative). + /// + /// An error will result if this value is negative with such a magnitude as to exceed the git height, + /// resulting in a negative build number. + /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + [Obsolete("Use " + nameof(VersionHeightOffset) + " instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public int? BuildNumberOffset + { + get => this.buildNumberOffset; + set => this.SetIfNotReadOnly(ref this.buildNumberOffset, value); + } + + /// + /// Gets or sets a number to add to the git height when calculating the number. + /// + /// Any integer (0, positive, or negative). + /// + /// An error will result if this value is negative with such a magnitude as to exceed the git height, + /// resulting in a negative build number. + /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public int? VersionHeightOffset + { +#pragma warning disable CS0618 + get => this.BuildNumberOffset; + set => this.BuildNumberOffset = value; +#pragma warning restore CS0618 + } + + /// + /// Gets a number to add to the git height when calculating the number. + /// + /// Any integer (0, positive, or negative). + /// + /// An error will result if this value is negative with such a magnitude as to exceed the git height, + /// resulting in a negative build number. + /// + [JsonIgnore] + [Obsolete("Use " + nameof(VersionHeightOffsetOrDefault) + " instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public int BuildNumberOffsetOrDefault => this.BuildNumberOffset ?? 0; + + /// + /// Gets a number to add to the git height when calculating the number. + /// + /// Any integer (0, positive, or negative). + /// + /// An error will result if this value is negative with such a magnitude as to exceed the git height, + /// resulting in a negative build number. + /// + [JsonIgnore] + public int VersionHeightOffsetOrDefault + { +#pragma warning disable CS0618 + get => this.BuildNumberOffsetOrDefault; +#pragma warning restore CS0618 + } + + /// + /// Gets or sets the minimum number of digits to use for numeric identifiers in SemVer 1. + /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public int? SemVer1NumericIdentifierPadding + { + get => this.semVer1NumericIdentifierPadding; + set => this.SetIfNotReadOnly(ref this.semVer1NumericIdentifierPadding, value); + } + + /// + /// Gets the minimum number of digits to use for numeric identifiers in SemVer 1. + /// + [JsonIgnore] + public int SemVer1NumericIdentifierPaddingOrDefault => this.SemVer1NumericIdentifierPadding ?? DefaultSemVer1NumericIdentifierPadding; + + /// + /// Gets or sets the abbreviated git commit hash length. + /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public int? GitCommitIdShortFixedLength + { + get => this.gitCommitIdShortFixedLength; + set => this.SetIfNotReadOnly(ref this.gitCommitIdShortFixedLength, value); + } + + /// + /// Gets or sets the abbreviated git commit hash length minimum value. + /// The git repository provides the value. + /// If set to 0 or a git repository is not available, is used. + /// The value is 0 by default. + /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public int? GitCommitIdShortAutoMinimum + { + get => this.gitCommitIdShortAutoMinimum; + set => this.SetIfNotReadOnly(ref this.gitCommitIdShortAutoMinimum, value); + } + + /// + /// Gets or sets the options around NuGet version strings. + /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public NuGetPackageVersionOptions? NuGetPackageVersion + { + get => this.nuGetPackageVersion; + set => this.SetIfNotReadOnly(ref this.nuGetPackageVersion, value); + } + + /// + /// Gets the options around NuGet version strings. + /// + [JsonIgnore] + public NuGetPackageVersionOptions NuGetPackageVersionOrDefault => this.NuGetPackageVersion ?? NuGetPackageVersionOptions.DefaultInstance; + + /// + /// Gets or sets an array of regular expressions that describes branch or tag names that should + /// be built with PublicRelease=true as the default value on build servers. + /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public IReadOnlyList? PublicReleaseRefSpec + { + get => this.publicReleaseRefSpec; + set => this.SetIfNotReadOnly(ref this.publicReleaseRefSpec, value); + } + + /// + /// Gets an array of regular expressions that describes branch or tag names that should + /// be built with PublicRelease=true as the default value on build servers. + /// + [JsonIgnore] + public IReadOnlyList PublicReleaseRefSpecOrDefault => this.PublicReleaseRefSpec ?? Array.Empty(); + + /// + /// Gets or sets the options around cloud build. + /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public CloudBuildOptions? CloudBuild + { + get => this.cloudBuild; + set => this.SetIfNotReadOnly(ref this.cloudBuild, value); + } + + /// + /// Gets the options around cloud build. + /// + [JsonIgnore] + public CloudBuildOptions CloudBuildOrDefault => this.CloudBuild ?? CloudBuildOptions.DefaultInstance; + + /// + /// Gets or sets the options for the prepare-release command. + /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public ReleaseOptions? Release + { + get => this.release; + set => this.SetIfNotReadOnly(ref this.release, value); + } + + /// + /// Gets the options for the prepare-release command. + /// + [JsonIgnore] + public ReleaseOptions ReleaseOrDefault => this.Release ?? ReleaseOptions.DefaultInstance; + + /// + /// Gets or sets a list of paths to use to filter commits when calculating version height. + /// If a given commit does not affect any paths in this filter, it is ignored for version height calculations. + /// Paths should be relative to the root of the repository. + /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public IReadOnlyList? PathFilters + { + get => this.pathFilters; + set => this.SetIfNotReadOnly(ref this.pathFilters, value); + } + + /// + /// Gets or sets a value indicating whether this options object should inherit from an ancestor any settings that are not explicitly set in this one. + /// + /// + /// When this is , this object may not completely describe the options to be applied. + /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool Inherit + { + get => this.inherit; + set => this.SetIfNotReadOnly(ref this.inherit, value); + } + + /// + /// Gets a value indicating whether this instance rejects all attempts to mutate it. + /// + [JsonIgnore] + public bool IsFrozen => this.isFrozen; + + /// + /// Gets the position in a computed version that the version height should appear. + /// + [JsonIgnore] + public SemanticVersion.Position? VersionHeightPosition + { + get + { + return this.version?.VersionHeightPosition; + } + } + + /// + /// Gets the position in a computed version that the first 16 bits of a git commit ID should appear, if any. + /// + [JsonIgnore] + internal SemanticVersion.Position? GitCommitIdPosition => this.version?.GitCommitIdPosition; + + /// + /// Gets a value indicating whether is + /// set and the only property on this class that is set. + /// + internal bool IsDefaultVersionTheOnlyPropertySet + { + get + { + return this.Version is not null && this.AssemblyVersion is null && (this.CloudBuild?.IsDefault ?? true) + && this.VersionHeightOffset == 0 + && !this.SemVer1NumericIdentifierPadding.HasValue + && !this.Inherit; + } + } + + /// + /// Gets the debugger display for this instance. + /// + private string DebuggerDisplay => this.Version?.ToString() ?? (this.Inherit ? "Inheriting version info" : "(missing version)"); + + /// + /// Initializes a new instance of the class + /// with initialized with the specified parameters. + /// + /// The version number. + /// The prerelease tag, if any. + /// The new instance of . + public static VersionOptions FromVersion(Version version, string? unstableTag = null) + { + return new VersionOptions + { + Version = new SemanticVersion(version, unstableTag), + }; + } + + /// + /// Gets the to use based on certain requirements. + /// The $schema property is not serialized when using this overload. + /// + /// A value indicating whether default values should be serialized. + /// The serializer settings to use. + public static JsonSerializerSettings GetJsonSettings(bool includeDefaults) => GetJsonSettings(includeDefaults, includeSchemaProperty: false); + + /// + /// Gets the to use based on certain requirements. + /// Path filters cannot be serialized or deserialized when using this overload. + /// + /// A value indicating whether default values should be serialized. + /// A value indicating whether the $schema property should be serialized. + /// The serializer settings to use. + public static JsonSerializerSettings GetJsonSettings(bool includeDefaults, bool includeSchemaProperty) => GetJsonSettings(includeDefaults, includeSchemaProperty, repoRelativeBaseDirectory: null); + + /// + /// Gets the to use based on certain requirements. + /// + /// A value indicating whether default values should be serialized. + /// A value indicating whether the $schema property should be serialized. + /// + /// Directory (relative to the root of the repository) that path + /// filters should be relative to. + /// This should be the directory where the version.json file resides. + /// An empty string represents the root of the repository. + /// Passing will mean path filters cannot be serialized. + /// + /// The serializer settings to use. + public static JsonSerializerSettings GetJsonSettings(bool includeDefaults = false, bool includeSchemaProperty = false, string? repoRelativeBaseDirectory = null) + { + return new JsonSerializerSettings + { + Converters = new JsonConverter[] + { + new VersionConverter(), + new SemanticVersionJsonConverter(), + new AssemblyVersionOptionsConverter(includeDefaults), + new StringEnumConverter() { NamingStrategy = new CamelCaseNamingStrategy() }, + new FilterPathJsonConverter(repoRelativeBaseDirectory), + }, + ContractResolver = new VersionOptionsContractResolver + { + IncludeDefaults = includeDefaults, + IncludeSchemaProperty = includeSchemaProperty, + }, + Formatting = Formatting.Indented, + }; + } + + /// + /// Checks equality against another object. + /// + /// The other instance. + /// if the instances have equal values; otherwise. + public override bool Equals(object? obj) + { + return this.Equals(obj as VersionOptions); + } + + /// + /// Gets a hash code for this instance. + /// + /// The hash code. + public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); + + /// + /// Checks equality against another instance of this class. + /// + /// The other instance. + /// if the instances have equal values; otherwise. + public bool Equals(VersionOptions? other) => EqualWithDefaultsComparer.Singleton.Equals(this, other); + + /// + /// Freezes this instance so no more changes can be made to it. + /// + public void Freeze() + { + if (!this.isFrozen) + { + this.isFrozen = true; + this.assemblyVersion?.Freeze(); + this.nuGetPackageVersion?.Freeze(); + this.publicReleaseRefSpec = this.publicReleaseRefSpec is object ? new ReadOnlyCollection(this.publicReleaseRefSpec.ToList()) : null; + this.cloudBuild?.Freeze(); + this.release?.Freeze(); + this.pathFilters = this.pathFilters is object ? new ReadOnlyCollection(this.pathFilters.ToList()) : null; + } + } + + /// + /// Sets the value of a field if this instance is not marked as read only. + /// + /// The type of the value stored by the field. + /// The field to change. + /// The value to set. + private void SetIfNotReadOnly(ref T field, T value) + { + Verify.Operation(!this.isFrozen, "This instance is read only."); + field = value; + } + /// + /// The class that contains settings for the property. + /// + public class NuGetPackageVersionOptions : IEquatable + { /// - /// The default value for the property. + /// Default value for . /// - public const int DefaultGitCommitIdShortFixedLength = 10; + public const VersionPrecision DefaultPrecision = VersionPrecision.Build; /// - /// The $schema field that should be serialized when writing + /// The default (uninitialized) instance. /// - [JsonProperty(PropertyName = "$schema")] - public string Schema => "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json"; + internal static readonly NuGetPackageVersionOptions DefaultInstance = new NuGetPackageVersionOptions() + { + isFrozen = true, + semVer = 1.0f, + precision = DefaultPrecision, + }; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private bool isFrozen; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private float? semVer; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private VersionPrecision? precision; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public VersionOptions() + public NuGetPackageVersionOptions() { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// Another instance to copy values from. - public VersionOptions(VersionOptions copyFrom) + /// The existing instance to copy from. + public NuGetPackageVersionOptions(NuGetPackageVersionOptions copyFrom) { - Requires.NotNull(copyFrom, nameof(copyFrom)); - - this.gitCommitIdPrefix = copyFrom.gitCommitIdPrefix; - this.version = copyFrom.version; - this.assemblyVersion = copyFrom.assemblyVersion is object ? new AssemblyVersionOptions(copyFrom.assemblyVersion) : null; - this.buildNumberOffset = copyFrom.buildNumberOffset; - this.semVer1NumericIdentifierPadding = copyFrom.semVer1NumericIdentifierPadding; - this.gitCommitIdShortFixedLength = copyFrom.gitCommitIdShortFixedLength; - this.gitCommitIdShortAutoMinimum = copyFrom.gitCommitIdShortAutoMinimum; - this.nuGetPackageVersion = copyFrom.nuGetPackageVersion is object ? new NuGetPackageVersionOptions(copyFrom.nuGetPackageVersion) : null; - this.publicReleaseRefSpec = copyFrom.publicReleaseRefSpec?.ToList(); - this.cloudBuild = copyFrom.cloudBuild is object ? new CloudBuildOptions(copyFrom.cloudBuild) : null; - this.release = copyFrom.release is object ? new ReleaseOptions(copyFrom.release) : null; - this.pathFilters = copyFrom.pathFilters?.ToList(); + this.semVer = copyFrom.semVer; } /// - /// Gets or sets the default version to use. + /// Gets or sets the version of SemVer (e.g. 1 or 2) that should be used when generating the package version. /// [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public SemanticVersion? Version + public float? SemVer { - get => this.version; - set => this.SetIfNotReadOnly(ref this.version, value); + get => this.semVer; + set => this.SetIfNotReadOnly(ref this.semVer, value); } /// - /// Gets or sets the version to use particularly for the - /// instead of the default . + /// Gets the version of SemVer (e.g. 1 or 2) that should be used when generating the package version. /// - /// An instance of or null to simply use the default . - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public AssemblyVersionOptions? AssemblyVersion - { - get => this.assemblyVersion; - set => this.SetIfNotReadOnly(ref this.assemblyVersion, value); - } + [JsonIgnore] + public float? SemVerOrDefault => this.SemVer ?? DefaultInstance.SemVer; /// - /// Gets or sets the prefix for git commit id in version. - /// Because of semver rules the prefix must lead with a [A-z_] character (not a number) and it cannot be the empty string. - /// If null 'g' will be used. + /// Gets or sets number of version components to include when generating the package version. /// - /// A prefix for git commit id. [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public string? GitCommitIdPrefix + public VersionPrecision? Precision { - get => this.gitCommitIdPrefix; - set - { - if (string.IsNullOrWhiteSpace(value)) - { - throw new ArgumentNullException(nameof(value), $"{nameof(this.GitCommitIdPrefix)} can't be empty"); - } - char first = value![0]; - if (first < 'A' || (first > 'Z' && first < 'a' && first != '_') || first > 'z') - { - throw new ArgumentException(nameof(value), $"{nameof(this.GitCommitIdPrefix)} must lead with a [A-z_] character (not a number)"); - } - - this.SetIfNotReadOnly(ref this.gitCommitIdPrefix, value); - } + get => this.precision; + set => this.SetIfNotReadOnly(ref this.precision, value); } /// - /// Gets the version to use particularly for the - /// instead of the default . + /// Gets the number of version components to include when generating the package version. /// - /// An instance of or null to simply use the default . [JsonIgnore] - public AssemblyVersionOptions AssemblyVersionOrDefault => this.AssemblyVersion ?? AssemblyVersionOptions.DefaultInstance; + public VersionPrecision PrecisionOrDefault => this.Precision ?? DefaultInstance.Precision!.Value; /// - /// Gets or sets a number to add to the git height when calculating the version height, - /// which typically is used in the portion of the computed version. + /// Gets a value indicating whether this instance rejects all attempts to mutate it. /// - /// Any integer (0, positive, or negative). - /// - /// An error will result if this value is negative with such a magnitude as to exceed the git height, - /// resulting in a negative build number. - /// - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - [Obsolete("Use " + nameof(VersionHeightOffset) + " instead.")] - [EditorBrowsable(EditorBrowsableState.Never)] - public int? BuildNumberOffset - { - get => this.buildNumberOffset; - set => this.SetIfNotReadOnly(ref this.buildNumberOffset, value); - } + [JsonIgnore] + public bool IsFrozen => this.isFrozen; /// - /// Gets or sets a number to add to the git height when calculating the number. + /// Gets a value indicating whether this instance is equivalent to the default instance. /// - /// Any integer (0, positive, or negative). - /// - /// An error will result if this value is negative with such a magnitude as to exceed the git height, - /// resulting in a negative build number. - /// - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public int? VersionHeightOffset - { -#pragma warning disable CS0618 - get => this.BuildNumberOffset; - set => this.BuildNumberOffset = value; -#pragma warning restore CS0618 - } + internal bool IsDefault => this.Equals(DefaultInstance); /// - /// Gets a number to add to the git height when calculating the number. + /// Freezes this instance so no more changes can be made to it. /// - /// Any integer (0, positive, or negative). - /// - /// An error will result if this value is negative with such a magnitude as to exceed the git height, - /// resulting in a negative build number. - /// - [JsonIgnore] - [Obsolete("Use " + nameof(VersionHeightOffsetOrDefault) + " instead.")] - [EditorBrowsable(EditorBrowsableState.Never)] - public int BuildNumberOffsetOrDefault => this.BuildNumberOffset ?? 0; + public void Freeze() => this.isFrozen = true; + + /// + public override bool Equals(object? obj) => this.Equals(obj as NuGetPackageVersionOptions); + + /// + public bool Equals(NuGetPackageVersionOptions? other) => EqualWithDefaultsComparer.Singleton.Equals(this, other); + + /// + public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); + + /// + /// Sets the value of a field if this instance is not marked as read only. + /// + /// The type of the value stored by the field. + /// The field to change. + /// The value to set. + private void SetIfNotReadOnly(ref T field, T value) + { + Verify.Operation(!this.isFrozen, "This instance is read only."); + field = value; + } + + internal class EqualWithDefaultsComparer : IEqualityComparer + { + internal static readonly EqualWithDefaultsComparer Singleton = new EqualWithDefaultsComparer(); + + private EqualWithDefaultsComparer() + { + } + + /// + public bool Equals(NuGetPackageVersionOptions? x, NuGetPackageVersionOptions? y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + if (x is null || y is null) + { + return false; + } + + return x.SemVerOrDefault == y.SemVerOrDefault && + x.PrecisionOrDefault == y.PrecisionOrDefault; + } + + /// + public int GetHashCode(NuGetPackageVersionOptions? obj) + { + if (obj is null) + { + return 0; + } + + unchecked + { + int hash = obj.SemVerOrDefault.GetHashCode() * 397; + hash ^= obj.PrecisionOrDefault.GetHashCode(); + return hash; + } + } + } + } + /// + /// Describes the details of how the AssemblyVersion value will be calculated. + /// + public class AssemblyVersionOptions : IEquatable + { /// - /// Gets a number to add to the git height when calculating the number. + /// The default (uninitialized) instance. /// - /// Any integer (0, positive, or negative). - /// - /// An error will result if this value is negative with such a magnitude as to exceed the git height, - /// resulting in a negative build number. - /// - [JsonIgnore] - public int VersionHeightOffsetOrDefault + internal static readonly AssemblyVersionOptions DefaultInstance = new AssemblyVersionOptions() { -#pragma warning disable CS0618 - get => this.BuildNumberOffsetOrDefault; -#pragma warning restore CS0618 - } + isFrozen = true, + precision = DefaultVersionPrecision, + }; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private bool isFrozen; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private Version? version; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private VersionPrecision? precision; /// - /// Gets or sets the minimum number of digits to use for numeric identifiers in SemVer 1. + /// Initializes a new instance of the class. /// - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public int? SemVer1NumericIdentifierPadding + public AssemblyVersionOptions() { - get => this.semVer1NumericIdentifierPadding; - set => this.SetIfNotReadOnly(ref this.semVer1NumericIdentifierPadding, value); } /// - /// Gets the minimum number of digits to use for numeric identifiers in SemVer 1. - /// - [JsonIgnore] - public int SemVer1NumericIdentifierPaddingOrDefault => this.SemVer1NumericIdentifierPadding ?? DefaultSemVer1NumericIdentifierPadding; - - /// - /// Gets or sets the abbreviated git commit hash length. + /// Initializes a new instance of the class. /// - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public int? GitCommitIdShortFixedLength + /// The assembly version (with major.minor components). + /// The additional version precision to add toward matching the AssemblyFileVersion. + public AssemblyVersionOptions(Version version, VersionPrecision? precision = null) { - get => this.gitCommitIdShortFixedLength; - set => this.SetIfNotReadOnly(ref this.gitCommitIdShortFixedLength, value); + this.Version = version; + this.Precision = precision; } /// - /// Gets or sets the abbreviated git commit hash length minimum value. - /// The git repository provides the value. - /// If set to 0 or a git repository is not available, is used. - /// The value is 0 by default. + /// Initializes a new instance of the class. /// - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public int? GitCommitIdShortAutoMinimum + /// The existing instance to copy from. + public AssemblyVersionOptions(AssemblyVersionOptions copyFrom) { - get => this.gitCommitIdShortAutoMinimum; - set => this.SetIfNotReadOnly(ref this.gitCommitIdShortAutoMinimum, value); + this.version = copyFrom.version; + this.precision = copyFrom.precision; } /// - /// Gets or sets the options around NuGet version strings + /// Gets or sets the components of the assembly version (2-4 components). /// [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public NuGetPackageVersionOptions? NuGetPackageVersion + public Version? Version { - get => this.nuGetPackageVersion; - set => this.SetIfNotReadOnly(ref this.nuGetPackageVersion, value); + get => this.version; + set => this.SetIfNotReadOnly(ref this.version, value); } /// - /// Gets the options around NuGet version strings - /// - [JsonIgnore] - public NuGetPackageVersionOptions NuGetPackageVersionOrDefault => this.NuGetPackageVersion ?? NuGetPackageVersionOptions.DefaultInstance; - - /// - /// Gets or sets an array of regular expressions that describes branch or tag names that should - /// be built with PublicRelease=true as the default value on build servers. + /// Gets or sets the additional version precision to add toward matching the AssemblyFileVersion. /// [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public IReadOnlyList? PublicReleaseRefSpec + public VersionPrecision? Precision { - get => this.publicReleaseRefSpec; - set => this.SetIfNotReadOnly(ref this.publicReleaseRefSpec, value); + get => this.precision; + set => this.SetIfNotReadOnly(ref this.precision, value); } /// - /// Gets an array of regular expressions that describes branch or tag names that should - /// be built with PublicRelease=true as the default value on build servers. + /// Gets the additional version precision to add toward matching the AssemblyFileVersion. /// [JsonIgnore] - public IReadOnlyList PublicReleaseRefSpecOrDefault => this.PublicReleaseRefSpec ?? Array.Empty(); - - /// - /// Gets or sets the options around cloud build. - /// - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public CloudBuildOptions? CloudBuild - { - get => this.cloudBuild; - set => this.SetIfNotReadOnly(ref this.cloudBuild, value); - } + public VersionPrecision PrecisionOrDefault => this.Precision ?? DefaultVersionPrecision; /// - /// Gets the options around cloud build. + /// Gets a value indicating whether this instance rejects all attempts to mutate it. /// [JsonIgnore] - public CloudBuildOptions CloudBuildOrDefault => this.CloudBuild ?? CloudBuildOptions.DefaultInstance; + public bool IsFrozen => this.isFrozen; /// - /// Gets or sets the options for the prepare-release command + /// Gets a value indicating whether this instance is equivalent to the default instance. /// - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public ReleaseOptions? Release - { - get => this.release; - set => this.SetIfNotReadOnly(ref this.release, value); - } + internal bool IsDefault => this.Equals(DefaultInstance); /// - /// Gets the options for the prepare-release command + /// Freezes this instance so no more changes can be made to it. /// - [JsonIgnore] - public ReleaseOptions ReleaseOrDefault => this.Release ?? ReleaseOptions.DefaultInstance; + public void Freeze() => this.isFrozen = true; + + /// + public override bool Equals(object? obj) => this.Equals(obj as AssemblyVersionOptions); + + /// + public bool Equals(AssemblyVersionOptions? other) => EqualWithDefaultsComparer.Singleton.Equals(this, other); + + /// + public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); /// - /// Gets or sets a list of paths to use to filter commits when calculating version height. - /// If a given commit does not affect any paths in this filter, it is ignored for version height calculations. - /// Paths should be relative to the root of the repository. + /// Sets the value of a field if this instance is not marked as read only. /// - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public IReadOnlyList? PathFilters + /// The type of the value stored by the field. + /// The field to change. + /// The value to set. + private void SetIfNotReadOnly(ref T field, T value) { - get => this.pathFilters; - set => this.SetIfNotReadOnly(ref this.pathFilters, value); + Verify.Operation(!this.isFrozen, "This instance is read only."); + field = value; } - /// - /// Gets or sets a value indicating whether this options object should inherit from an ancestor any settings that are not explicitly set in this one. - /// - /// - /// When this is true, this object may not completely describe the options to be applied. - /// - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public bool Inherit + internal class EqualWithDefaultsComparer : IEqualityComparer { - get => this.inherit; - set => this.SetIfNotReadOnly(ref this.inherit, value); + internal static readonly EqualWithDefaultsComparer Singleton = new EqualWithDefaultsComparer(); + + private EqualWithDefaultsComparer() + { + } + + /// + public bool Equals(AssemblyVersionOptions? x, AssemblyVersionOptions? y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + if (x is null || y is null) + { + return false; + } + + return EqualityComparer.Default.Equals(x.Version, y.Version) + && x.PrecisionOrDefault == y.PrecisionOrDefault; + } + + /// + public int GetHashCode(AssemblyVersionOptions? obj) + { + if (obj is null) + { + return 0; + } + + return (obj.Version?.GetHashCode() ?? 0) + (int)obj.PrecisionOrDefault; + } } + } + /// + /// Options that are applicable specifically to cloud builds (e.g. VSTS, AppVeyor, TeamCity). + /// + public class CloudBuildOptions : IEquatable + { /// - /// Gets a value indicating whether this instance rejects all attempts to mutate it. + /// The default (uninitialized) instance. /// - [JsonIgnore] - public bool IsFrozen => this.isFrozen; + internal static readonly CloudBuildOptions DefaultInstance = new CloudBuildOptions() + { + isFrozen = true, + setAllVariables = false, + setVersionVariables = true, + }; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private bool isFrozen; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private bool? setAllVariables; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private bool? setVersionVariables; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private CloudBuildNumberOptions? buildNumber; /// - /// Gets the position in a computed version that the version height should appear. + /// Initializes a new instance of the class. /// - [JsonIgnore] - public SemanticVersion.Position? VersionHeightPosition + public CloudBuildOptions() { - get - { - return this.version?.VersionHeightPosition; - } } /// - /// Gets the position in a computed version that the first 16 bits of a git commit ID should appear, if any. + /// Initializes a new instance of the class. /// - [JsonIgnore] - internal SemanticVersion.Position? GitCommitIdPosition => this.version?.GitCommitIdPosition; + /// Another instance to copy values from. + public CloudBuildOptions(CloudBuildOptions copyFrom) + { + this.setAllVariables = copyFrom.setAllVariables; + this.setVersionVariables = copyFrom.setVersionVariables; + this.buildNumber = copyFrom.buildNumber is object ? new CloudBuildNumberOptions(copyFrom.buildNumber) : null; + } /// - /// Gets the debugger display for this instance. + /// Gets or sets a value indicating whether to elevate all build properties to cloud build variables prefaced with "NBGV_". /// - private string DebuggerDisplay => this.Version?.ToString() ?? (this.Inherit ? "Inheriting version info" : "(missing version)"); + public bool? SetAllVariables + { + get => this.setAllVariables; + set => this.SetIfNotReadOnly(ref this.setAllVariables, value); + } /// - /// Initializes a new instance of the class - /// with initialized with the specified parameters. + /// Gets or sets a value indicating whether to elevate certain calculated version build properties to cloud build variables. /// - /// The version number. - /// The prerelease tag, if any. - /// The new instance of . - public static VersionOptions FromVersion(Version version, string? unstableTag = null) + public bool? SetVersionVariables { - return new VersionOptions - { - Version = new SemanticVersion(version, unstableTag), - }; + get => this.setVersionVariables; + set => this.SetIfNotReadOnly(ref this.setVersionVariables, value); } /// - /// Gets the to use based on certain requirements. - /// The $schema property is not serialized when using this overload. + /// Gets a value indicating whether to elevate all build properties to cloud build variables prefaced with "NBGV_". /// - /// A value indicating whether default values should be serialized. - /// The serializer settings to use. - public static JsonSerializerSettings GetJsonSettings(bool includeDefaults) => GetJsonSettings(includeDefaults, includeSchemaProperty: false); + [JsonIgnore] + public bool SetAllVariablesOrDefault => this.SetAllVariables ?? DefaultInstance.SetAllVariables!.Value; /// - /// Gets the to use based on certain requirements. - /// Path filters cannot be serialized or deserialized when using this overload. + /// Gets a value indicating whether to elevate certain calculated version build properties to cloud build variables. /// - /// A value indicating whether default values should be serialized. - /// A value indicating whether the $schema property should be serialized. - /// The serializer settings to use. - public static JsonSerializerSettings GetJsonSettings(bool includeDefaults, bool includeSchemaProperty) => GetJsonSettings(includeDefaults, includeSchemaProperty, repoRelativeBaseDirectory: null); + [JsonIgnore] + public bool SetVersionVariablesOrDefault => this.SetVersionVariables ?? DefaultInstance.SetVersionVariables!.Value; /// - /// Gets the to use based on certain requirements. + /// Gets or sets options around how and whether to set the build number preset by the cloud build with one enriched with version information. /// - /// A value indicating whether default values should be serialized. - /// A value indicating whether the $schema property should be serialized. - /// - /// Directory (relative to the root of the repository) that path - /// filters should be relative to. - /// This should be the directory where the version.json file resides. - /// An empty string represents the root of the repository. - /// Passing null will mean path filters cannot be serialized. - /// - /// The serializer settings to use. - public static JsonSerializerSettings GetJsonSettings(bool includeDefaults = false, bool includeSchemaProperty = false, string? repoRelativeBaseDirectory = null) + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public CloudBuildNumberOptions? BuildNumber { - return new JsonSerializerSettings - { - Converters = new JsonConverter[] { - new VersionConverter(), - new SemanticVersionJsonConverter(), - new AssemblyVersionOptionsConverter(includeDefaults), - new StringEnumConverter() { NamingStrategy = new CamelCaseNamingStrategy() }, - new FilterPathJsonConverter(repoRelativeBaseDirectory), - }, - ContractResolver = new VersionOptionsContractResolver - { - IncludeDefaults = includeDefaults, - IncludeSchemaProperty = includeSchemaProperty, - }, - Formatting = Formatting.Indented, - }; + get => this.buildNumber; + set => this.SetIfNotReadOnly(ref this.buildNumber, value); } /// - /// Checks equality against another object. + /// Gets options around how and whether to set the build number preset by the cloud build with one enriched with version information. /// - /// The other instance. - /// true if the instances have equal values; false otherwise. - public override bool Equals(object? obj) - { - return this.Equals(obj as VersionOptions); - } + [JsonIgnore] + public CloudBuildNumberOptions BuildNumberOrDefault => this.BuildNumber ?? CloudBuildNumberOptions.DefaultInstance; /// - /// Gets a hash code for this instance. + /// Gets a value indicating whether this instance rejects all attempts to mutate it. /// - /// The hash code. - public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); + [JsonIgnore] + public bool IsFrozen => this.isFrozen; /// - /// Checks equality against another instance of this class. + /// Gets a value indicating whether this instance is equivalent to the default instance. /// - /// The other instance. - /// true if the instances have equal values; false otherwise. - public bool Equals(VersionOptions? other) => EqualWithDefaultsComparer.Singleton.Equals(this, other); + internal bool IsDefault => this.Equals(DefaultInstance); /// /// Freezes this instance so no more changes can be made to it. @@ -537,29 +1078,18 @@ public void Freeze() if (!this.isFrozen) { this.isFrozen = true; - this.assemblyVersion?.Freeze(); - this.nuGetPackageVersion?.Freeze(); - this.publicReleaseRefSpec = this.publicReleaseRefSpec is object ? new ReadOnlyCollection(this.publicReleaseRefSpec.ToList()) : null; - this.cloudBuild?.Freeze(); - this.release?.Freeze(); - this.pathFilters = this.pathFilters is object ? new ReadOnlyCollection(this.pathFilters.ToList()) : null; + this.buildNumber?.Freeze(); } } - /// - /// Gets a value indicating whether is - /// set and the only property on this class that is set. - /// - internal bool IsDefaultVersionTheOnlyPropertySet - { - get - { - return this.Version is not null && this.AssemblyVersion is null && (this.CloudBuild?.IsDefault ?? true) - && this.VersionHeightOffset == 0 - && !this.SemVer1NumericIdentifierPadding.HasValue - && !this.Inherit; - } - } + /// + public override bool Equals(object? obj) => this.Equals(obj as CloudBuildOptions); + + /// + public bool Equals(CloudBuildOptions? other) => EqualWithDefaultsComparer.Singleton.Equals(this, other); + + /// + public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); /// /// Sets the value of a field if this instance is not marked as read only. @@ -573,779 +1103,316 @@ private void SetIfNotReadOnly(ref T field, T value) field = value; } - /// - /// The class that contains settings for the property. - /// - public class NuGetPackageVersionOptions : IEquatable + internal class EqualWithDefaultsComparer : IEqualityComparer { - /// - /// The default (uninitialized) instance. - /// - internal static readonly NuGetPackageVersionOptions DefaultInstance = new NuGetPackageVersionOptions() - { - isFrozen = true, - semVer = 1.0f, - precision = DefaultPrecision, - }; - - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private bool isFrozen; - - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private float? semVer; - - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private VersionPrecision? precision; - - /// - /// Default value for . - /// - public const VersionPrecision DefaultPrecision = VersionPrecision.Build; - - /// - /// Initializes a new instance of the class. - /// - public NuGetPackageVersionOptions() - { - } - - /// - /// Initializes a new instance of the class. - /// - public NuGetPackageVersionOptions(NuGetPackageVersionOptions copyFrom) - { - this.semVer = copyFrom.semVer; - } - - /// - /// Gets or sets the version of SemVer (e.g. 1 or 2) that should be used when generating the package version. - /// - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public float? SemVer - { - get => this.semVer; - set => this.SetIfNotReadOnly(ref this.semVer, value); - } + internal static readonly EqualWithDefaultsComparer Singleton = new EqualWithDefaultsComparer(); - /// - /// Gets the version of SemVer (e.g. 1 or 2) that should be used when generating the package version. - /// - [JsonIgnore] - public float? SemVerOrDefault => this.SemVer ?? DefaultInstance.SemVer; - - /// - /// Gets or sets number of version components to include when generating the package version. - /// - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public VersionPrecision? Precision + private EqualWithDefaultsComparer() { - get => this.precision; - set => this.SetIfNotReadOnly(ref this.precision, value); } - /// - /// Gets the number of version components to include when generating the package version. - /// - [JsonIgnore] - public VersionPrecision PrecisionOrDefault => this.Precision ?? DefaultInstance.Precision!.Value; - - /// - /// Gets a value indicating whether this instance rejects all attempts to mutate it. - /// - [JsonIgnore] - public bool IsFrozen => this.isFrozen; - - /// - /// Freezes this instance so no more changes can be made to it. - /// - public void Freeze() => this.isFrozen = true; - - /// - public override bool Equals(object? obj) => this.Equals(obj as NuGetPackageVersionOptions); - - /// - public bool Equals(NuGetPackageVersionOptions? other) => EqualWithDefaultsComparer.Singleton.Equals(this, other); - /// - public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); - - /// - /// Gets a value indicating whether this instance is equivalent to the default instance. - /// - internal bool IsDefault => this.Equals(DefaultInstance); - - /// - /// Sets the value of a field if this instance is not marked as read only. - /// - /// The type of the value stored by the field. - /// The field to change. - /// The value to set. - private void SetIfNotReadOnly(ref T field, T value) - { - Verify.Operation(!this.isFrozen, "This instance is read only."); - field = value; - } - - internal class EqualWithDefaultsComparer : IEqualityComparer + public bool Equals(CloudBuildOptions? x, CloudBuildOptions? y) { - internal static readonly EqualWithDefaultsComparer Singleton = new EqualWithDefaultsComparer(); - - private EqualWithDefaultsComparer() { } - - /// - public bool Equals(NuGetPackageVersionOptions? x, NuGetPackageVersionOptions? y) + if (ReferenceEquals(x, y)) { - if (ReferenceEquals(x, y)) - { - return true; - } - - if (x is null || y is null) - { - return false; - } - - return x.SemVerOrDefault == y.SemVerOrDefault && - x.PrecisionOrDefault == y.PrecisionOrDefault; + return true; } - /// - public int GetHashCode(NuGetPackageVersionOptions? obj) + if (x is null || y is null) { - if (obj is null) - return 0; - - unchecked - { - var hash = obj.SemVerOrDefault.GetHashCode() * 397; - hash ^= obj.PrecisionOrDefault.GetHashCode(); - return hash; - } + return false; } - } - } - - /// - /// Describes the details of how the AssemblyVersion value will be calculated. - /// - public class AssemblyVersionOptions : IEquatable - { - /// - /// The default (uninitialized) instance. - /// - internal static readonly AssemblyVersionOptions DefaultInstance = new AssemblyVersionOptions() - { - isFrozen = true, - precision = DefaultVersionPrecision, - }; - - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private bool isFrozen; - - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private Version? version; - - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private VersionPrecision? precision; - - /// - /// Initializes a new instance of the class. - /// - public AssemblyVersionOptions() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The assembly version (with major.minor components). - /// The additional version precision to add toward matching the AssemblyFileVersion. - public AssemblyVersionOptions(Version version, VersionPrecision? precision = null) - { - this.Version = version; - this.Precision = precision; - } - - /// - /// Initializes a new instance of the class. - /// - public AssemblyVersionOptions(AssemblyVersionOptions copyFrom) - { - this.version = copyFrom.version; - this.precision = copyFrom.precision; - } - - /// - /// Gets or sets the components of the assembly version (2-4 components). - /// - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public Version? Version - { - get => this.version; - set => this.SetIfNotReadOnly(ref this.version, value); - } - /// - /// Gets or sets the additional version precision to add toward matching the AssemblyFileVersion. - /// - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public VersionPrecision? Precision - { - get => this.precision; - set => this.SetIfNotReadOnly(ref this.precision, value); + return x.SetVersionVariablesOrDefault == y.SetVersionVariablesOrDefault + && x.SetAllVariablesOrDefault == y.SetAllVariablesOrDefault + && CloudBuildNumberOptions.EqualWithDefaultsComparer.Singleton.Equals(x.BuildNumberOrDefault, y.BuildNumberOrDefault); } - /// - /// Gets the additional version precision to add toward matching the AssemblyFileVersion. - /// - [JsonIgnore] - public VersionPrecision PrecisionOrDefault => this.Precision ?? DefaultVersionPrecision; - - /// - /// Gets a value indicating whether this instance rejects all attempts to mutate it. - /// - [JsonIgnore] - public bool IsFrozen => this.isFrozen; - - /// - /// Freezes this instance so no more changes can be made to it. - /// - public void Freeze() => this.isFrozen = true; - - /// - public override bool Equals(object? obj) => this.Equals(obj as AssemblyVersionOptions); - - /// - public bool Equals(AssemblyVersionOptions? other) => EqualWithDefaultsComparer.Singleton.Equals(this, other); - /// - public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); - - /// - /// Gets a value indicating whether this instance is equivalent to the default instance. - /// - internal bool IsDefault => this.Equals(DefaultInstance); - - /// - /// Sets the value of a field if this instance is not marked as read only. - /// - /// The type of the value stored by the field. - /// The field to change. - /// The value to set. - private void SetIfNotReadOnly(ref T field, T value) + public int GetHashCode(CloudBuildOptions? obj) { - Verify.Operation(!this.isFrozen, "This instance is read only."); - field = value; - } - - internal class EqualWithDefaultsComparer : IEqualityComparer - { - internal static readonly EqualWithDefaultsComparer Singleton = new EqualWithDefaultsComparer(); - - private EqualWithDefaultsComparer() { } - - /// - public bool Equals(AssemblyVersionOptions? x, AssemblyVersionOptions? y) + if (obj is null) { - if (ReferenceEquals(x, y)) - { - return true; - } - - if (x is null || y is null) - { - return false; - } - - return EqualityComparer.Default.Equals(x.Version, y.Version) - && x.PrecisionOrDefault == y.PrecisionOrDefault; + return 0; } - /// - public int GetHashCode(AssemblyVersionOptions? obj) - { - if (obj is null) - { - return 0; - } - - return (obj.Version?.GetHashCode() ?? 0) + (int)obj.PrecisionOrDefault; - } + return (obj.SetVersionVariablesOrDefault ? 1 : 0) + + (obj.SetAllVariablesOrDefault ? 1 : 0) + + obj.BuildNumberOrDefault.GetHashCode(); } } + } + /// + /// Override the build number preset by the cloud build with one enriched with version information. + /// + public class CloudBuildNumberOptions : IEquatable + { /// - /// Options that are applicable specifically to cloud builds (e.g. VSTS, AppVeyor, TeamCity) + /// The default (uninitialized) instance. /// - public class CloudBuildOptions : IEquatable + internal static readonly CloudBuildNumberOptions DefaultInstance = new CloudBuildNumberOptions() { - /// - /// The default (uninitialized) instance. - /// - internal static readonly CloudBuildOptions DefaultInstance = new CloudBuildOptions() - { - isFrozen = true, - setAllVariables = false, - setVersionVariables = true, - }; - - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private bool isFrozen; - - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private bool? setAllVariables; - - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private bool? setVersionVariables; - - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private CloudBuildNumberOptions? buildNumber; - - /// - /// Initializes a new instance of the class. - /// - public CloudBuildOptions() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// Another instance to copy values from - public CloudBuildOptions(CloudBuildOptions copyFrom) - { - this.setAllVariables = copyFrom.setAllVariables; - this.setVersionVariables = copyFrom.setVersionVariables; - this.buildNumber = copyFrom.buildNumber is object ? new CloudBuildNumberOptions(copyFrom.buildNumber) : null; - } - - /// - /// Gets or sets a value indicating whether to elevate all build properties to cloud build variables prefaced with "NBGV_". - /// - public bool? SetAllVariables - { - get => this.setAllVariables; - set => this.SetIfNotReadOnly(ref this.setAllVariables, value); - } - - /// - /// Gets or sets a value indicating whether to elevate certain calculated version build properties to cloud build variables. - /// - public bool? SetVersionVariables - { - get => this.setVersionVariables; - set => this.SetIfNotReadOnly(ref this.setVersionVariables, value); - } - - /// - /// Gets a value indicating whether to elevate all build properties to cloud build variables prefaced with "NBGV_". - /// - [JsonIgnore] - public bool SetAllVariablesOrDefault => this.SetAllVariables ?? DefaultInstance.SetAllVariables!.Value; - - /// - /// Gets a value indicating whether to elevate certain calculated version build properties to cloud build variables. - /// - [JsonIgnore] - public bool SetVersionVariablesOrDefault => this.SetVersionVariables ?? DefaultInstance.SetVersionVariables!.Value; - - /// - /// Gets or sets options around how and whether to set the build number preset by the cloud build with one enriched with version information. - /// - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public CloudBuildNumberOptions? BuildNumber - { - get => this.buildNumber; - set => this.SetIfNotReadOnly(ref this.buildNumber, value); - } + isFrozen = true, + enabled = false, + }; - /// - /// Gets options around how and whether to set the build number preset by the cloud build with one enriched with version information. - /// - [JsonIgnore] - public CloudBuildNumberOptions BuildNumberOrDefault => this.BuildNumber ?? CloudBuildNumberOptions.DefaultInstance; - - /// - /// Gets a value indicating whether this instance rejects all attempts to mutate it. - /// - [JsonIgnore] - public bool IsFrozen => this.isFrozen; - - /// - /// Freezes this instance so no more changes can be made to it. - /// - public void Freeze() - { - if (!this.isFrozen) - { - this.isFrozen = true; - this.buildNumber?.Freeze(); - } - } - - /// - public override bool Equals(object? obj) => this.Equals(obj as CloudBuildOptions); - - /// - public bool Equals(CloudBuildOptions? other) => EqualWithDefaultsComparer.Singleton.Equals(this, other); - - /// - public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); - - /// - /// Gets a value indicating whether this instance is equivalent to the default instance. - /// - internal bool IsDefault => this.Equals(DefaultInstance); - - /// - /// Sets the value of a field if this instance is not marked as read only. - /// - /// The type of the value stored by the field. - /// The field to change. - /// The value to set. - private void SetIfNotReadOnly(ref T field, T value) - { - Verify.Operation(!this.isFrozen, "This instance is read only."); - field = value; - } + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private bool isFrozen; - internal class EqualWithDefaultsComparer : IEqualityComparer - { - internal static readonly EqualWithDefaultsComparer Singleton = new EqualWithDefaultsComparer(); + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private bool? enabled; + private CloudBuildNumberCommitIdOptions? includeCommitId; - private EqualWithDefaultsComparer() { } + /// + /// Initializes a new instance of the class. + /// + public CloudBuildNumberOptions() + { + } - /// - public bool Equals(CloudBuildOptions? x, CloudBuildOptions? y) - { - if (ReferenceEquals(x, y)) - { - return true; - } - - if (x is null || y is null) - { - return false; - } - - return x.SetVersionVariablesOrDefault == y.SetVersionVariablesOrDefault - && x.SetAllVariablesOrDefault == y.SetAllVariablesOrDefault - && CloudBuildNumberOptions.EqualWithDefaultsComparer.Singleton.Equals(x.BuildNumberOrDefault, y.BuildNumberOrDefault); - } + /// + /// Initializes a new instance of the class. + /// + /// The existing instance to copy from. + public CloudBuildNumberOptions(CloudBuildNumberOptions copyFrom) + { + this.enabled = copyFrom.enabled; + this.includeCommitId = copyFrom.includeCommitId is object ? new CloudBuildNumberCommitIdOptions(copyFrom.includeCommitId) : null; + } - /// - public int GetHashCode(CloudBuildOptions? obj) - { - if (obj is null) - { - return 0; - } - - return (obj.SetVersionVariablesOrDefault ? 1 : 0) - + (obj.SetAllVariablesOrDefault ? 1 : 0) - + obj.BuildNumberOrDefault.GetHashCode(); - } - } + /// + /// Gets or sets a value indicating whether to override the build number preset by the cloud build. + /// + public bool? Enabled + { + get => this.enabled; + set => this.SetIfNotReadOnly(ref this.enabled, value); } /// - /// Override the build number preset by the cloud build with one enriched with version information. + /// Gets a value indicating whether to override the build number preset by the cloud build. /// - public class CloudBuildNumberOptions : IEquatable + [JsonIgnore] + public bool EnabledOrDefault => this.Enabled ?? DefaultInstance.Enabled!.Value; + + /// + /// Gets or sets when and where to include information about the git commit being built. + /// + public CloudBuildNumberCommitIdOptions? IncludeCommitId { - /// - /// The default (uninitialized) instance. - /// - internal static readonly CloudBuildNumberOptions DefaultInstance = new CloudBuildNumberOptions() - { - isFrozen = true, - enabled = false, - }; + get => this.includeCommitId; + set => this.SetIfNotReadOnly(ref this.includeCommitId, value); + } - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private bool isFrozen; + /// + /// Gets when and where to include information about the git commit being built. + /// + [JsonIgnore] + public CloudBuildNumberCommitIdOptions IncludeCommitIdOrDefault => this.IncludeCommitId ?? CloudBuildNumberCommitIdOptions.DefaultInstance; - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private bool? enabled; - private CloudBuildNumberCommitIdOptions? includeCommitId; + /// + /// Gets a value indicating whether this instance rejects all attempts to mutate it. + /// + [JsonIgnore] + public bool IsFrozen => this.isFrozen; - /// - /// Initializes a new instance of the class. - /// - public CloudBuildNumberOptions() - { - } + /// + /// Gets a value indicating whether this instance is equivalent to the default instance. + /// + internal bool IsDefault => this.Equals(DefaultInstance); - /// - /// Initializes a new instance of the class. - /// - public CloudBuildNumberOptions(CloudBuildNumberOptions copyFrom) + /// + /// Freezes this instance so no more changes can be made to it. + /// + public void Freeze() + { + if (!this.isFrozen) { - this.enabled = copyFrom.enabled; - this.includeCommitId = copyFrom.includeCommitId is object ? new CloudBuildNumberCommitIdOptions(copyFrom.includeCommitId) : null; + this.isFrozen = true; + this.IncludeCommitId?.Freeze(); } + } - /// - /// Gets or sets a value indicating whether to override the build number preset by the cloud build. - /// - public bool? Enabled - { - get => this.enabled; - set => this.SetIfNotReadOnly(ref this.enabled, value); - } + /// + public override bool Equals(object? obj) => this.Equals(obj as CloudBuildNumberOptions); + + /// + public bool Equals(CloudBuildNumberOptions? other) => EqualWithDefaultsComparer.Singleton.Equals(this, other); + + /// + public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); + + /// + /// Sets the value of a field if this instance is not marked as read only. + /// + /// The type of the value stored by the field. + /// The field to change. + /// The value to set. + private void SetIfNotReadOnly(ref T field, T value) + { + Verify.Operation(!this.isFrozen, "This instance is read only."); + field = value; + } - /// - /// Gets a value indicating whether to override the build number preset by the cloud build. - /// - [JsonIgnore] - public bool EnabledOrDefault => this.Enabled ?? DefaultInstance.Enabled!.Value; + internal class EqualWithDefaultsComparer : IEqualityComparer + { + internal static readonly EqualWithDefaultsComparer Singleton = new EqualWithDefaultsComparer(); - /// - /// Gets or sets when and where to include information about the git commit being built. - /// - public CloudBuildNumberCommitIdOptions? IncludeCommitId + private EqualWithDefaultsComparer() { - get => this.includeCommitId; - set => this.SetIfNotReadOnly(ref this.includeCommitId, value); } - /// - /// Gets when and where to include information about the git commit being built. - /// - [JsonIgnore] - public CloudBuildNumberCommitIdOptions IncludeCommitIdOrDefault => this.IncludeCommitId ?? CloudBuildNumberCommitIdOptions.DefaultInstance; - - /// - /// Gets a value indicating whether this instance rejects all attempts to mutate it. - /// - [JsonIgnore] - public bool IsFrozen => this.isFrozen; - - /// - /// Freezes this instance so no more changes can be made to it. - /// - public void Freeze() + /// + public bool Equals(CloudBuildNumberOptions? x, CloudBuildNumberOptions? y) { - if (!this.isFrozen) + if (ReferenceEquals(x, y)) { - this.isFrozen = true; - this.IncludeCommitId?.Freeze(); + return true; } - } - - /// - public override bool Equals(object? obj) => this.Equals(obj as CloudBuildNumberOptions); - /// - public bool Equals(CloudBuildNumberOptions? other) => EqualWithDefaultsComparer.Singleton.Equals(this, other); + if (x is null || y is null) + { + return false; + } - /// - public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); - - /// - /// Gets a value indicating whether this instance is equivalent to the default instance. - /// - internal bool IsDefault => this.Equals(DefaultInstance); - - /// - /// Sets the value of a field if this instance is not marked as read only. - /// - /// The type of the value stored by the field. - /// The field to change. - /// The value to set. - private void SetIfNotReadOnly(ref T field, T value) - { - Verify.Operation(!this.isFrozen, "This instance is read only."); - field = value; + return x.EnabledOrDefault == y.EnabledOrDefault + && CloudBuildNumberCommitIdOptions.EqualWithDefaultsComparer.Singleton.Equals(x.IncludeCommitIdOrDefault, y.IncludeCommitIdOrDefault); } - internal class EqualWithDefaultsComparer : IEqualityComparer + /// + public int GetHashCode(CloudBuildNumberOptions? obj) { - internal static readonly EqualWithDefaultsComparer Singleton = new EqualWithDefaultsComparer(); - - private EqualWithDefaultsComparer() { } - - /// - public bool Equals(CloudBuildNumberOptions? x, CloudBuildNumberOptions? y) + if (obj is null) { - if (ReferenceEquals(x, y)) - { - return true; - } - - if (x is null || y is null) - { - return false; - } - - return x.EnabledOrDefault == y.EnabledOrDefault - && CloudBuildNumberCommitIdOptions.EqualWithDefaultsComparer.Singleton.Equals(x.IncludeCommitIdOrDefault, y.IncludeCommitIdOrDefault); + return 0; } - /// - public int GetHashCode(CloudBuildNumberOptions? obj) - { - if (obj is null) - { - return 0; - } - - return obj.EnabledOrDefault ? 1 : 0 - + obj.IncludeCommitIdOrDefault.GetHashCode(); - } + return obj.EnabledOrDefault ? 1 : 0 + + obj.IncludeCommitIdOrDefault.GetHashCode(); } } + } + /// + /// Describes when and where to include information about the git commit being built. + /// + public class CloudBuildNumberCommitIdOptions : IEquatable + { /// - /// Describes when and where to include information about the git commit being built. + /// The default (uninitialized) instance. /// - public class CloudBuildNumberCommitIdOptions : IEquatable + internal static readonly CloudBuildNumberCommitIdOptions DefaultInstance = new CloudBuildNumberCommitIdOptions() { - /// - /// The default (uninitialized) instance. - /// - internal static readonly CloudBuildNumberCommitIdOptions DefaultInstance = new CloudBuildNumberCommitIdOptions() - { - isFrozen = true, - when = CloudBuildNumberCommitWhen.NonPublicReleaseOnly, - where = CloudBuildNumberCommitWhere.BuildMetadata, - }; - - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private bool isFrozen; + isFrozen = true, + when = CloudBuildNumberCommitWhen.NonPublicReleaseOnly, + where = CloudBuildNumberCommitWhere.BuildMetadata, + }; - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private CloudBuildNumberCommitWhen? when; - - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private CloudBuildNumberCommitWhere? where; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private bool isFrozen; - /// - /// Initializes a new instance of the class. - /// - public CloudBuildNumberCommitIdOptions() - { - } + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private CloudBuildNumberCommitWhen? when; - /// - /// Initializes a new instance of the class. - /// - public CloudBuildNumberCommitIdOptions(CloudBuildNumberCommitIdOptions copyFrom) - { - this.when = copyFrom.when; - this.where = copyFrom.where; - } + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private CloudBuildNumberCommitWhere? where; - /// - /// Gets or sets the conditions when the commit ID is included in the build number. - /// - public CloudBuildNumberCommitWhen? When - { - get => this.when; - set => this.SetIfNotReadOnly(ref this.when, value); - } + /// + /// Initializes a new instance of the class. + /// + public CloudBuildNumberCommitIdOptions() + { + } - /// - /// Gets the conditions when the commit ID is included in the build number. - /// - [JsonIgnore] - public CloudBuildNumberCommitWhen WhenOrDefault => this.When ?? DefaultInstance.When!.Value; + /// + /// Initializes a new instance of the class. + /// + /// The instance to copy from. + public CloudBuildNumberCommitIdOptions(CloudBuildNumberCommitIdOptions copyFrom) + { + this.when = copyFrom.when; + this.where = copyFrom.where; + } - /// - /// Gets or sets the position to include the commit ID information. - /// - public CloudBuildNumberCommitWhere? Where - { - get => this.where; - set => this.SetIfNotReadOnly(ref this.where, value); - } + /// + /// Gets or sets the conditions when the commit ID is included in the build number. + /// + public CloudBuildNumberCommitWhen? When + { + get => this.when; + set => this.SetIfNotReadOnly(ref this.when, value); + } - /// - /// Gets the position to include the commit ID information. - /// - [JsonIgnore] - public CloudBuildNumberCommitWhere WhereOrDefault => this.Where ?? DefaultInstance.Where!.Value; + /// + /// Gets the conditions when the commit ID is included in the build number. + /// + [JsonIgnore] + public CloudBuildNumberCommitWhen WhenOrDefault => this.When ?? DefaultInstance.When!.Value; - /// - /// Gets a value indicating whether this instance rejects all attempts to mutate it. - /// - [JsonIgnore] - public bool IsFrozen => this.isFrozen; + /// + /// Gets or sets the position to include the commit ID information. + /// + public CloudBuildNumberCommitWhere? Where + { + get => this.where; + set => this.SetIfNotReadOnly(ref this.where, value); + } - /// - /// Freezes this instance so no more changes can be made to it. - /// - public void Freeze() => this.isFrozen = true; + /// + /// Gets the position to include the commit ID information. + /// + [JsonIgnore] + public CloudBuildNumberCommitWhere WhereOrDefault => this.Where ?? DefaultInstance.Where!.Value; - /// - public override bool Equals(object? obj) => this.Equals(obj as CloudBuildNumberCommitIdOptions); + /// + /// Gets a value indicating whether this instance rejects all attempts to mutate it. + /// + [JsonIgnore] + public bool IsFrozen => this.isFrozen; - /// - public bool Equals(CloudBuildNumberCommitIdOptions? other) => EqualWithDefaultsComparer.Singleton.Equals(this, other); - /// - public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); - - /// - /// Gets a value indicating whether this instance is equivalent to the default instance. - /// - internal bool IsDefault => this.Equals(DefaultInstance); - - /// - /// Sets the value of a field if this instance is not marked as read only. - /// - /// The type of the value stored by the field. - /// The field to change. - /// The value to set. - private void SetIfNotReadOnly(ref T field, T value) - { - Verify.Operation(!this.isFrozen, "This instance is read only."); - field = value; - } + /// + /// Gets a value indicating whether this instance is equivalent to the default instance. + /// + internal bool IsDefault => this.Equals(DefaultInstance); - internal class EqualWithDefaultsComparer : IEqualityComparer - { - internal static readonly EqualWithDefaultsComparer Singleton = new EqualWithDefaultsComparer(); + /// + /// Freezes this instance so no more changes can be made to it. + /// + public void Freeze() => this.isFrozen = true; - private EqualWithDefaultsComparer() { } + /// + public override bool Equals(object? obj) => this.Equals(obj as CloudBuildNumberCommitIdOptions); - /// - public bool Equals(CloudBuildNumberCommitIdOptions? x, CloudBuildNumberCommitIdOptions? y) - { - if (ReferenceEquals(x, y)) - { - return true; - } - - if (x is null || y is null) - { - return false; - } - - return x.WhenOrDefault == y.WhenOrDefault - && x.WhereOrDefault == y.WhereOrDefault; - } + /// + public bool Equals(CloudBuildNumberCommitIdOptions? other) => EqualWithDefaultsComparer.Singleton.Equals(this, other); - /// - public int GetHashCode(CloudBuildNumberCommitIdOptions? obj) - { - if (obj is null) - { - return 0; - } + /// + public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); - return (int)obj.WhereOrDefault + (int)obj.WhenOrDefault * 0x10; - } - } + /// + /// Sets the value of a field if this instance is not marked as read only. + /// + /// The type of the value stored by the field. + /// The field to change. + /// The value to set. + private void SetIfNotReadOnly(ref T field, T value) + { + Verify.Operation(!this.isFrozen, "This instance is read only."); + field = value; } - private class EqualWithDefaultsComparer : IEqualityComparer + internal class EqualWithDefaultsComparer : IEqualityComparer { internal static readonly EqualWithDefaultsComparer Singleton = new EqualWithDefaultsComparer(); - private EqualWithDefaultsComparer() { } + private EqualWithDefaultsComparer() + { + } /// - public bool Equals(VersionOptions? x, VersionOptions? y) + public bool Equals(CloudBuildNumberCommitIdOptions? x, CloudBuildNumberCommitIdOptions? y) { if (ReferenceEquals(x, y)) { @@ -1357,274 +1424,255 @@ public bool Equals(VersionOptions? x, VersionOptions? y) return false; } - return EqualityComparer.Default.Equals(x.Version, y.Version) - && AssemblyVersionOptions.EqualWithDefaultsComparer.Singleton.Equals(x.AssemblyVersionOrDefault, y.AssemblyVersionOrDefault) - && NuGetPackageVersionOptions.EqualWithDefaultsComparer.Singleton.Equals(x.NuGetPackageVersionOrDefault, y.NuGetPackageVersionOrDefault) - && CloudBuildOptions.EqualWithDefaultsComparer.Singleton.Equals(x.CloudBuildOrDefault, y.CloudBuildOrDefault) - && ReleaseOptions.EqualWithDefaultsComparer.Singleton.Equals(x.ReleaseOrDefault, y.ReleaseOrDefault) - && x.VersionHeightOffset == y.VersionHeightOffset; + return x.WhenOrDefault == y.WhenOrDefault + && x.WhereOrDefault == y.WhereOrDefault; } /// - public int GetHashCode(VersionOptions? obj) + public int GetHashCode(CloudBuildNumberCommitIdOptions? obj) { - return obj?.Version?.GetHashCode() ?? 0; + if (obj is null) + { + return 0; + } + + return (int)obj.WhereOrDefault + ((int)obj.WhenOrDefault * 0x10); } } + } + /// + /// Encapsulates settings for the "prepare-release" and "tag" commands. + /// + public class ReleaseOptions : IEquatable + { /// - /// The last component to control in a 4 integer version. + /// The default (uninitialized) instance. /// - public enum VersionPrecision + internal static readonly ReleaseOptions DefaultInstance = new ReleaseOptions() { - /// - /// The first integer is the last number set. The rest will be zeros. - /// - Major, - - /// - /// The second integer is the last number set. The rest will be zeros. - /// - Minor, - - /// - /// The third integer is the last number set. The fourth will be zero. - /// - Build, - - /// - /// All four integers will be set. - /// - Revision, - } + isFrozen = true, + tagName = "v{version}", + branchName = "v{version}", + versionIncrement = ReleaseVersionIncrement.Minor, + firstUnstableTag = "alpha", + }; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private bool isFrozen; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string? tagName; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string? branchName; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private ReleaseVersionIncrement? versionIncrement; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string? firstUnstableTag; /// - /// The conditions a commit ID is included in a cloud build number. + /// Initializes a new instance of the class. /// - public enum CloudBuildNumberCommitWhen + public ReleaseOptions() { - /// - /// Always include the commit information in the cloud Build Number. - /// - Always, - - /// - /// Only include the commit information when building a non-PublicRelease. - /// - NonPublicReleaseOnly, - - /// - /// Never include the commit information. - /// - Never, } /// - /// The position a commit ID can appear in a cloud build number. + /// Initializes a new instance of the class. /// - public enum CloudBuildNumberCommitWhere + /// The existing instance to copy from. + public ReleaseOptions(ReleaseOptions copyFrom) { - /// - /// The commit ID appears in build metadata (e.g. +ga1b2c3). - /// - BuildMetadata, - - /// - /// The commit ID appears as the 4th integer in the version (e.g. 1.2.3.23523). - /// - FourthVersionComponent, + this.tagName = copyFrom.tagName; + this.branchName = copyFrom.branchName; + this.versionIncrement = copyFrom.versionIncrement; + this.firstUnstableTag = copyFrom.firstUnstableTag; } /// - /// Encapsulates settings for the "prepare-release" command + /// Gets or sets the tag name template for tagging. /// - public class ReleaseOptions : IEquatable + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public string? TagName { - /// - /// The default (uninitialized) instance. - /// - internal static readonly ReleaseOptions DefaultInstance = new ReleaseOptions() - { - isFrozen = true, - branchName = "v{version}", - versionIncrement = ReleaseVersionIncrement.Minor, - firstUnstableTag = "alpha" - }; + get => this.tagName; + set => this.SetIfNotReadOnly(ref this.tagName, value); + } - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private bool isFrozen; + /// + /// Gets the tag name template for tagging. + /// + [JsonIgnore] + public string TagNameOrDefault => this.TagName ?? DefaultInstance.TagName!; - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private string? branchName; + /// + /// Gets or sets the branch name template for release branches. + /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public string? BranchName + { + get => this.branchName; + set => this.SetIfNotReadOnly(ref this.branchName, value); + } - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private ReleaseVersionIncrement? versionIncrement; + /// + /// Gets the branch name template for release branches. + /// + [JsonIgnore] + public string BranchNameOrDefault => this.BranchName ?? DefaultInstance.BranchName!; - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private string? firstUnstableTag; + /// + /// Gets or sets the setting specifying how to increment the version when creating a release. + /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public ReleaseVersionIncrement? VersionIncrement + { + get => this.versionIncrement; + set => this.SetIfNotReadOnly(ref this.versionIncrement, value); + } - /// - /// Initializes a new instance of the class - /// - public ReleaseOptions() - { - } + /// + /// Gets the setting specifying how to increment the version when creating a release. + /// + [JsonIgnore] + public ReleaseVersionIncrement VersionIncrementOrDefault => this.VersionIncrement ?? DefaultInstance.VersionIncrement!.Value; - /// - /// Initializes a new instance of the class - /// - public ReleaseOptions(ReleaseOptions copyFrom) - { - this.branchName = copyFrom.branchName; - this.versionIncrement = copyFrom.versionIncrement; - this.firstUnstableTag = copyFrom.firstUnstableTag; - } + /// + /// Gets or sets the first/default prerelease tag for new versions. + /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public string? FirstUnstableTag + { + get => this.firstUnstableTag; + set => this.SetIfNotReadOnly(ref this.firstUnstableTag, value); + } - /// - /// Gets or sets the branch name template for release branches - /// - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public string? BranchName - { - get => this.branchName; - set => this.SetIfNotReadOnly(ref this.branchName, value); - } + /// + /// Gets the first/default prerelease tag for new versions. + /// + [JsonIgnore] + public string FirstUnstableTagOrDefault => this.FirstUnstableTag ?? DefaultInstance.FirstUnstableTag!; - /// - /// Gets the set branch name template for release branches - /// - [JsonIgnore] - public string BranchNameOrDefault => this.BranchName ?? DefaultInstance.BranchName!; - - /// - /// Gets or sets the setting specifying how to increment the version when creating a release - /// - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public ReleaseVersionIncrement? VersionIncrement - { - get => this.versionIncrement; - set => this.SetIfNotReadOnly(ref this.versionIncrement, value); - } + /// + /// Gets a value indicating whether this instance rejects all attempts to mutate it. + /// + [JsonIgnore] + public bool IsFrozen => this.isFrozen; - /// - /// Gets or sets the setting specifying how to increment the version when creating a release. - /// - [JsonIgnore] - public ReleaseVersionIncrement VersionIncrementOrDefault => this.VersionIncrement ?? DefaultInstance.VersionIncrement!.Value; - - /// - /// Gets or sets the first/default prerelease tag for new versions. - /// - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public string? FirstUnstableTag - { - get => this.firstUnstableTag; - set => this.SetIfNotReadOnly(ref this.firstUnstableTag, value); - } + /// + /// Gets a value indicating whether this instance is equivalent to the default instance. + /// + internal bool IsDefault => this.Equals(DefaultInstance); - /// - /// Gets or sets the first/default prerelease tag for new versions. - /// - [JsonIgnore] - public string FirstUnstableTagOrDefault => this.FirstUnstableTag ?? DefaultInstance.FirstUnstableTag!; + /// + /// Freezes this instance so no more changes can be made to it. + /// + public void Freeze() => this.isFrozen = true; - /// - /// Gets a value indicating whether this instance rejects all attempts to mutate it. - /// - [JsonIgnore] - public bool IsFrozen => this.isFrozen; + /// + public override bool Equals(object? obj) => this.Equals(obj as ReleaseOptions); - /// - /// Freezes this instance so no more changes can be made to it. - /// - public void Freeze() => this.isFrozen = true; + /// + public bool Equals(ReleaseOptions? other) => EqualWithDefaultsComparer.Singleton.Equals(this, other); - /// - public override bool Equals(object? obj) => this.Equals(obj as ReleaseOptions); + /// + public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); - /// - public bool Equals(ReleaseOptions? other) => EqualWithDefaultsComparer.Singleton.Equals(this, other); + /// + /// Sets the value of a field if this instance is not marked as read only. + /// + /// The type of the value stored by the field. + /// The field to change. + /// The value to set. + private void SetIfNotReadOnly(ref T field, T value) + { + Verify.Operation(!this.isFrozen, "This instance is read only."); + field = value; + } - /// - public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); - - /// - /// Gets a value indicating whether this instance is equivalent to the default instance. - /// - internal bool IsDefault => this.Equals(DefaultInstance); - - /// - /// Sets the value of a field if this instance is not marked as read only. - /// - /// The type of the value stored by the field. - /// The field to change. - /// The value to set. - private void SetIfNotReadOnly(ref T field, T value) + internal class EqualWithDefaultsComparer : IEqualityComparer + { + internal static readonly EqualWithDefaultsComparer Singleton = new EqualWithDefaultsComparer(); + + private EqualWithDefaultsComparer() { - Verify.Operation(!this.isFrozen, "This instance is read only."); - field = value; } - internal class EqualWithDefaultsComparer : IEqualityComparer + /// + public bool Equals(ReleaseOptions? x, ReleaseOptions? y) { - internal static readonly EqualWithDefaultsComparer Singleton = new EqualWithDefaultsComparer(); + if (ReferenceEquals(x, y)) + { + return true; + } + + if (x is null || y is null) + { + return false; + } - private EqualWithDefaultsComparer() { } + return StringComparer.Ordinal.Equals(x.TagNameOrDefault, y.TagNameOrDefault) && + StringComparer.Ordinal.Equals(x.BranchNameOrDefault, y.BranchNameOrDefault) && + x.VersionIncrementOrDefault == y.VersionIncrementOrDefault && + StringComparer.Ordinal.Equals(x.FirstUnstableTagOrDefault, y.FirstUnstableTagOrDefault); + } - /// - public bool Equals(ReleaseOptions? x, ReleaseOptions? y) + /// + public int GetHashCode(ReleaseOptions? obj) + { + if (obj is null) { - if (ReferenceEquals(x, y)) - { - return true; - } - - if (x is null || y is null) - { - return false; - } - - return StringComparer.Ordinal.Equals(x.BranchNameOrDefault, y.BranchNameOrDefault) && - x.VersionIncrementOrDefault == y.VersionIncrementOrDefault && - StringComparer.Ordinal.Equals(x.FirstUnstableTagOrDefault, y.FirstUnstableTagOrDefault); + return 0; } - /// - public int GetHashCode(ReleaseOptions? obj) + unchecked { - if (obj is null) - return 0; - - unchecked - { - var hash = StringComparer.Ordinal.GetHashCode(obj.BranchNameOrDefault) * 397; - hash ^= (int)obj.VersionIncrementOrDefault; - hash ^= StringComparer.Ordinal.GetHashCode(obj.FirstUnstableTagOrDefault); - return hash; - } + int hash = StringComparer.Ordinal.GetHashCode(obj.TagNameOrDefault) * 397; + hash ^= StringComparer.Ordinal.GetHashCode(obj.BranchNameOrDefault); + hash ^= (int)obj.VersionIncrementOrDefault; + hash ^= StringComparer.Ordinal.GetHashCode(obj.FirstUnstableTagOrDefault); + return hash; } } } + } + + private class EqualWithDefaultsComparer : IEqualityComparer + { + internal static readonly EqualWithDefaultsComparer Singleton = new EqualWithDefaultsComparer(); + + private EqualWithDefaultsComparer() + { + } + + /// + public bool Equals(VersionOptions? x, VersionOptions? y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + if (x is null || y is null) + { + return false; + } + + return EqualityComparer.Default.Equals(x.Version, y.Version) + && AssemblyVersionOptions.EqualWithDefaultsComparer.Singleton.Equals(x.AssemblyVersionOrDefault, y.AssemblyVersionOrDefault) + && NuGetPackageVersionOptions.EqualWithDefaultsComparer.Singleton.Equals(x.NuGetPackageVersionOrDefault, y.NuGetPackageVersionOrDefault) + && CloudBuildOptions.EqualWithDefaultsComparer.Singleton.Equals(x.CloudBuildOrDefault, y.CloudBuildOrDefault) + && ReleaseOptions.EqualWithDefaultsComparer.Singleton.Equals(x.ReleaseOrDefault, y.ReleaseOrDefault) + && x.VersionHeightOffset == y.VersionHeightOffset; + } - /// - /// Possible increments of the version after creating release branches - /// - public enum ReleaseVersionIncrement + /// + public int GetHashCode(VersionOptions? obj) { - /// - /// Increment the major version after creating a release branch - /// - Major, - - /// - /// Increment the minor version after creating a release branch - /// - Minor, - - /// - /// Increment the build number (the third number in a version) after creating a release branch. - /// - Build, + return obj?.Version?.GetHashCode() ?? 0; } } } diff --git a/src/NerdBank.GitVersioning/VersionOptionsContractResolver.cs b/src/NerdBank.GitVersioning/VersionOptionsContractResolver.cs index 00944f67..5b0355fa 100644 --- a/src/NerdBank.GitVersioning/VersionOptionsContractResolver.cs +++ b/src/NerdBank.GitVersioning/VersionOptionsContractResolver.cs @@ -1,146 +1,152 @@ -namespace Nerdbank.GitVersioning +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Nerdbank.GitVersioning; + +internal class VersionOptionsContractResolver : CamelCasePropertyNamesContractResolver { - using System; - using System.Collections.Generic; - using System.Reflection; - using Newtonsoft.Json; - using Newtonsoft.Json.Serialization; + private static readonly object TypeContractCacheLock = new object(); + + private static readonly Dictionary, JsonContract> ContractCache = new Dictionary, JsonContract>(); - internal class VersionOptionsContractResolver : CamelCasePropertyNamesContractResolver + public VersionOptionsContractResolver() { - private static readonly object TypeContractCacheLock = new object(); + } - private static readonly Dictionary, JsonContract> contractCache = new Dictionary, JsonContract>(); + internal bool IncludeSchemaProperty { get; set; } + + internal bool IncludeDefaults { get; set; } = true; + + /// + /// Obtains a contract for a given type. + /// + /// The type to obtain a contract for. + /// The contract. + /// + /// This override changes the caching policy from the base class, which caches based on this.GetType(). + /// The inherited policy is problematic because we have instance properties that change the contract. + /// So instead, we cache with a complex key to capture the settings as well. + /// + public override JsonContract ResolveContract(Type type) + { + var contractKey = Tuple.Create(this.IncludeSchemaProperty, this.IncludeDefaults, type); - public VersionOptionsContractResolver() + JsonContract contract; + lock (TypeContractCacheLock) { + if (ContractCache.TryGetValue(contractKey, out contract)) + { + return contract; + } } - internal bool IncludeSchemaProperty { get; set; } - - internal bool IncludeDefaults { get; set; } = true; - - /// - /// Obtains a contract for a given type. - /// - /// The type to obtain a contract for. - /// The contract. - /// - /// This override changes the caching policy from the base class, which caches based on this.GetType(). - /// The inherited policy is problematic because we have instance properties that change the contract. - /// So instead, we cache with a complex key to capture the settings as well. - /// - public override JsonContract ResolveContract(Type type) + contract = this.CreateContract(type); + + lock (TypeContractCacheLock) { - var contractKey = Tuple.Create(this.IncludeSchemaProperty, this.IncludeDefaults, type); + if (!ContractCache.ContainsKey(contractKey)) + { + ContractCache.Add(contractKey, contract); + } + } - JsonContract contract; - lock (TypeContractCacheLock) + return contract; + } + + /// + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + JsonProperty property = base.CreateProperty(member, memberSerialization); + + if (property.DeclaringType == typeof(VersionOptions) && member.Name == nameof(VersionOptions.Schema)) + { + property.ShouldSerialize = instance => this.IncludeSchemaProperty; + } + + if (!this.IncludeDefaults) + { + if (property.DeclaringType == typeof(VersionOptions) && member.Name == nameof(VersionOptions.AssemblyVersion)) { - if (contractCache.TryGetValue(contractKey, out contract)) - { - return contract; - } + property.ShouldSerialize = instance => !((VersionOptions)instance).AssemblyVersionOrDefault.IsDefault; } - contract = base.CreateContract(type); +#pragma warning disable CS0618 // Type or member is obsolete + if (property.DeclaringType == typeof(VersionOptions) && member.Name == nameof(VersionOptions.BuildNumberOffset)) +#pragma warning restore CS0618 // Type or member is obsolete + { + property.ShouldSerialize = instance => false; // always serialized by its new name + } - lock (TypeContractCacheLock) + if (property.DeclaringType == typeof(VersionOptions) && member.Name == nameof(VersionOptions.VersionHeightOffset)) { - if (!contractCache.ContainsKey(contractKey)) - { - contractCache.Add(contractKey, contract); - } + property.ShouldSerialize = instance => ((VersionOptions)instance).VersionHeightOffsetOrDefault != 0; } - return contract; - } + if (property.DeclaringType == typeof(VersionOptions) && member.Name == nameof(VersionOptions.NuGetPackageVersion)) + { + property.ShouldSerialize = instance => !((VersionOptions)instance).NuGetPackageVersionOrDefault.IsDefault; + } - protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) - { - var property = base.CreateProperty(member, memberSerialization); + if (property.DeclaringType == typeof(VersionOptions) && member.Name == nameof(VersionOptions.CloudBuild)) + { + property.ShouldSerialize = instance => !((VersionOptions)instance).CloudBuildOrDefault.IsDefault; + } - if (property.DeclaringType == typeof(VersionOptions) && member.Name == nameof(VersionOptions.Schema)) + if (property.DeclaringType == typeof(VersionOptions.CloudBuildOptions) && member.Name == nameof(VersionOptions.CloudBuildOptions.SetAllVariables)) { - property.ShouldSerialize = instance => this.IncludeSchemaProperty; + property.ShouldSerialize = instance => ((VersionOptions.CloudBuildOptions)instance).SetAllVariablesOrDefault != VersionOptions.CloudBuildOptions.DefaultInstance.SetAllVariables.Value; } - if (!this.IncludeDefaults) + if (property.DeclaringType == typeof(VersionOptions.CloudBuildOptions) && member.Name == nameof(VersionOptions.CloudBuildOptions.SetVersionVariables)) { - if (property.DeclaringType == typeof(VersionOptions) && member.Name == nameof(VersionOptions.AssemblyVersion)) - { - property.ShouldSerialize = instance => !((VersionOptions)instance).AssemblyVersionOrDefault.IsDefault; - } + property.ShouldSerialize = instance => ((VersionOptions.CloudBuildOptions)instance).SetVersionVariablesOrDefault != VersionOptions.CloudBuildOptions.DefaultInstance.SetVersionVariables.Value; + } -#pragma warning disable CS0618 // Type or member is obsolete - if (property.DeclaringType == typeof(VersionOptions) && member.Name == nameof(VersionOptions.BuildNumberOffset)) -#pragma warning restore CS0618 // Type or member is obsolete - { - property.ShouldSerialize = instance => false; // always serialized by its new name - } - - if (property.DeclaringType == typeof(VersionOptions) && member.Name == nameof(VersionOptions.VersionHeightOffset)) - { - property.ShouldSerialize = instance => ((VersionOptions)instance).VersionHeightOffsetOrDefault != 0; - } - - if (property.DeclaringType == typeof(VersionOptions) && member.Name == nameof(VersionOptions.NuGetPackageVersion)) - { - property.ShouldSerialize = instance => !((VersionOptions)instance).NuGetPackageVersionOrDefault.IsDefault; - } - - if (property.DeclaringType == typeof(VersionOptions) && member.Name == nameof(VersionOptions.CloudBuild)) - { - property.ShouldSerialize = instance => !((VersionOptions)instance).CloudBuildOrDefault.IsDefault; - } - - if (property.DeclaringType == typeof(VersionOptions.CloudBuildOptions) && member.Name == nameof(VersionOptions.CloudBuildOptions.SetAllVariables)) - { - property.ShouldSerialize = instance => ((VersionOptions.CloudBuildOptions)instance).SetAllVariablesOrDefault != VersionOptions.CloudBuildOptions.DefaultInstance.SetAllVariables.Value; - } - - if (property.DeclaringType == typeof(VersionOptions.CloudBuildOptions) && member.Name == nameof(VersionOptions.CloudBuildOptions.SetVersionVariables)) - { - property.ShouldSerialize = instance => ((VersionOptions.CloudBuildOptions)instance).SetVersionVariablesOrDefault != VersionOptions.CloudBuildOptions.DefaultInstance.SetVersionVariables.Value; - } - - if (property.DeclaringType == typeof(VersionOptions.CloudBuildNumberOptions) && member.Name == nameof(VersionOptions.CloudBuildNumberOptions.IncludeCommitId)) - { - property.ShouldSerialize = instance => !((VersionOptions.CloudBuildNumberOptions)instance).IncludeCommitIdOrDefault.IsDefault; - } - - if (property.DeclaringType == typeof(VersionOptions.CloudBuildNumberCommitIdOptions) && member.Name == nameof(VersionOptions.CloudBuildNumberCommitIdOptions.When)) - { - property.ShouldSerialize = instance => ((VersionOptions.CloudBuildNumberCommitIdOptions)instance).WhenOrDefault != VersionOptions.CloudBuildNumberCommitIdOptions.DefaultInstance.When.Value; - } - - if (property.DeclaringType == typeof(VersionOptions.CloudBuildNumberCommitIdOptions) && member.Name == nameof(VersionOptions.CloudBuildNumberCommitIdOptions.Where)) - { - property.ShouldSerialize = instance => ((VersionOptions.CloudBuildNumberCommitIdOptions)instance).WhereOrDefault != VersionOptions.CloudBuildNumberCommitIdOptions.DefaultInstance.Where.Value; - } - - if (property.DeclaringType == typeof(VersionOptions) && member.Name == nameof(VersionOptions.Release)) - { - property.ShouldSerialize = instance => !((VersionOptions)instance).ReleaseOrDefault.IsDefault; - } - - if (property.DeclaringType == typeof(VersionOptions.ReleaseOptions) && member.Name == nameof(VersionOptions.ReleaseOptions.BranchName)) - { - property.ShouldSerialize = instance => ((VersionOptions.ReleaseOptions)instance).BranchNameOrDefault != VersionOptions.ReleaseOptions.DefaultInstance.BranchName; - } - - if (property.DeclaringType == typeof(VersionOptions.ReleaseOptions) && member.Name == nameof(VersionOptions.ReleaseOptions.VersionIncrement)) - { - property.ShouldSerialize = instance => ((VersionOptions.ReleaseOptions)instance).VersionIncrementOrDefault != VersionOptions.ReleaseOptions.DefaultInstance.VersionIncrement.Value; - } - - if (property.DeclaringType == typeof(VersionOptions.ReleaseOptions) && member.Name == nameof(VersionOptions.ReleaseOptions.FirstUnstableTag)) - { - property.ShouldSerialize = instance => ((VersionOptions.ReleaseOptions)instance).FirstUnstableTagOrDefault != VersionOptions.ReleaseOptions.DefaultInstance.FirstUnstableTag; - } - } - - return property; + if (property.DeclaringType == typeof(VersionOptions.CloudBuildNumberOptions) && member.Name == nameof(VersionOptions.CloudBuildNumberOptions.IncludeCommitId)) + { + property.ShouldSerialize = instance => !((VersionOptions.CloudBuildNumberOptions)instance).IncludeCommitIdOrDefault.IsDefault; + } + + if (property.DeclaringType == typeof(VersionOptions.CloudBuildNumberCommitIdOptions) && member.Name == nameof(VersionOptions.CloudBuildNumberCommitIdOptions.When)) + { + property.ShouldSerialize = instance => ((VersionOptions.CloudBuildNumberCommitIdOptions)instance).WhenOrDefault != VersionOptions.CloudBuildNumberCommitIdOptions.DefaultInstance.When.Value; + } + + if (property.DeclaringType == typeof(VersionOptions.CloudBuildNumberCommitIdOptions) && member.Name == nameof(VersionOptions.CloudBuildNumberCommitIdOptions.Where)) + { + property.ShouldSerialize = instance => ((VersionOptions.CloudBuildNumberCommitIdOptions)instance).WhereOrDefault != VersionOptions.CloudBuildNumberCommitIdOptions.DefaultInstance.Where.Value; + } + + if (property.DeclaringType == typeof(VersionOptions) && member.Name == nameof(VersionOptions.Release)) + { + property.ShouldSerialize = instance => !((VersionOptions)instance).ReleaseOrDefault.IsDefault; + } + + if (property.DeclaringType == typeof(VersionOptions.ReleaseOptions) && member.Name == nameof(VersionOptions.ReleaseOptions.TagName)) + { + property.ShouldSerialize = instance => ((VersionOptions.ReleaseOptions)instance).TagNameOrDefault != VersionOptions.ReleaseOptions.DefaultInstance.TagName; + } + + if (property.DeclaringType == typeof(VersionOptions.ReleaseOptions) && member.Name == nameof(VersionOptions.ReleaseOptions.BranchName)) + { + property.ShouldSerialize = instance => ((VersionOptions.ReleaseOptions)instance).BranchNameOrDefault != VersionOptions.ReleaseOptions.DefaultInstance.BranchName; + } + + if (property.DeclaringType == typeof(VersionOptions.ReleaseOptions) && member.Name == nameof(VersionOptions.ReleaseOptions.VersionIncrement)) + { + property.ShouldSerialize = instance => ((VersionOptions.ReleaseOptions)instance).VersionIncrementOrDefault != VersionOptions.ReleaseOptions.DefaultInstance.VersionIncrement.Value; + } + + if (property.DeclaringType == typeof(VersionOptions.ReleaseOptions) && member.Name == nameof(VersionOptions.ReleaseOptions.FirstUnstableTag)) + { + property.ShouldSerialize = instance => ((VersionOptions.ReleaseOptions)instance).FirstUnstableTagOrDefault != VersionOptions.ReleaseOptions.DefaultInstance.FirstUnstableTag; + } } + + return property; } } diff --git a/src/NerdBank.GitVersioning/VersionOracle.cs b/src/NerdBank.GitVersioning/VersionOracle.cs index a416e79e..c3d2160a 100644 --- a/src/NerdBank.GitVersioning/VersionOracle.cs +++ b/src/NerdBank.GitVersioning/VersionOracle.cs @@ -1,531 +1,541 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Nerdbank.GitVersioning -{ - using System; - using System.Collections.Generic; - using System.Globalization; - using System.Linq; - using System.Reflection; - using System.Text.RegularExpressions; +using System.Globalization; +using System.Reflection; +using System.Text.RegularExpressions; + +#nullable enable +namespace Nerdbank.GitVersioning; + +/// +/// Assembles version information in a variety of formats. +/// +public class VersionOracle +{ /// - /// Assembles version information in a variety of formats. + /// The 0.0 version. /// - public class VersionOracle - { - private const bool UseLibGit2 = false; + private protected static readonly Version Version0 = new Version(0, 0); - /// - /// The 0.0 version. - /// - private protected static readonly Version Version0 = new Version(0, 0); + private const bool UseLibGit2 = false; - private readonly GitContext context; + private readonly GitContext context; - private readonly ICloudBuild? cloudBuild; + private readonly ICloudBuild? cloudBuild; - /// - /// The number of version components (up to the 4 integers) to include in . - /// - private readonly int assemblyInformationalVersionComponentCount; + /// + /// The number of version components (up to the 4 integers) to include in . + /// + private readonly int assemblyInformationalVersionComponentCount; - /// - /// Initializes a new instance of the class. - /// - /// The git context from which to calculate version data. - /// An optional cloud build provider that may offer additional context. Typically set to . - /// An optional value to override the version height offset. - public VersionOracle(GitContext context, ICloudBuild? cloudBuild = null, int? overrideVersionHeightOffset = null) - { - this.context = context; - this.cloudBuild = cloudBuild; + /// + /// Initializes a new instance of the class. + /// + /// The git context from which to calculate version data. + /// An optional cloud build provider that may offer additional context. Typically set to . + /// An optional value to override the version height offset. + public VersionOracle(GitContext context, ICloudBuild? cloudBuild = null, int? overrideVersionHeightOffset = null) + { + this.context = context; + this.cloudBuild = cloudBuild; - this.CommittedVersion = context.VersionFile.GetVersion(); + this.CommittedVersion = context.VersionFile.GetVersion(); - // Consider the working version only if the commit being inspected is HEAD. - // Otherwise we're looking at historical data and should not consider the state of the working tree at all. - this.WorkingVersion = context.IsHead ? context.VersionFile.GetWorkingCopyVersion() : this.CommittedVersion; + // Consider the working version only if the commit being inspected is HEAD. + // Otherwise we're looking at historical data and should not consider the state of the working tree at all. + this.WorkingVersion = context.IsHead ? context.VersionFile.GetWorkingCopyVersion() : this.CommittedVersion; - if (overrideVersionHeightOffset.HasValue) + if (overrideVersionHeightOffset.HasValue) + { + if (this.CommittedVersion is object) { - if (this.CommittedVersion is object) - { - this.CommittedVersion.VersionHeightOffset = overrideVersionHeightOffset.Value; - } - - if (this.WorkingVersion is object) - { - this.WorkingVersion.VersionHeightOffset = overrideVersionHeightOffset.Value; - } + this.CommittedVersion.VersionHeightOffset = overrideVersionHeightOffset.Value; } - this.BuildingRef = cloudBuild?.BuildingTag ?? cloudBuild?.BuildingBranch ?? context.HeadCanonicalName; - try - { - this.VersionHeight = context.CalculateVersionHeight(this.CommittedVersion, this.WorkingVersion); - } - catch (GitException ex) when (context.IsShallow && ex.ErrorCode == GitException.ErrorCodes.ObjectNotFound) - { - // Our managed git implementation throws this on shallow clones. - throw ThrowShallowClone(ex); - } - catch (InvalidOperationException ex) when (context.IsShallow && (ex.InnerException is NullReferenceException || ex.InnerException is LibGit2Sharp.NotFoundException)) + if (this.WorkingVersion is object) { - // Libgit2 throws this on shallow clones. - throw ThrowShallowClone(ex); + this.WorkingVersion.VersionHeightOffset = overrideVersionHeightOffset.Value; } + } + + this.BuildingRef = cloudBuild?.BuildingTag ?? cloudBuild?.BuildingBranch ?? context.HeadCanonicalName; + try + { + this.VersionHeight = context.CalculateVersionHeight(this.CommittedVersion, this.WorkingVersion); + } + catch (GitException ex) when (context.IsShallow && ex.ErrorCode == GitException.ErrorCodes.ObjectNotFound) + { + // Our managed git implementation throws this on shallow clones. + throw ThrowShallowClone(ex); + } + catch (InvalidOperationException ex) when (context.IsShallow && (ex.InnerException is NullReferenceException || ex.InnerException is LibGit2Sharp.NotFoundException)) + { + // Libgit2 throws this on shallow clones. + throw ThrowShallowClone(ex); + } - static Exception ThrowShallowClone(Exception inner) => throw new GitException("Shallow clone lacks the objects required to calculate version height. Use full clones or clones with a history at least as deep as the last version height resetting change.", inner) { iSShallowClone = true, ErrorCode = GitException.ErrorCodes.ObjectNotFound }; + static Exception ThrowShallowClone(Exception inner) => throw new GitException("Shallow clone lacks the objects required to calculate version height. Use full clones or clones with a history at least as deep as the last version height resetting change.", inner) { IsShallowClone = true, ErrorCode = GitException.ErrorCodes.ObjectNotFound }; - this.VersionOptions = this.CommittedVersion ?? this.WorkingVersion; - this.Version = this.VersionOptions?.Version?.Version ?? Version0; - this.assemblyInformationalVersionComponentCount = this.VersionOptions?.VersionHeightPosition == SemanticVersion.Position.Revision ? 4 : 3; + this.VersionOptions = this.CommittedVersion ?? this.WorkingVersion; + this.Version = this.VersionOptions?.Version?.Version ?? Version0; + this.assemblyInformationalVersionComponentCount = this.VersionOptions?.VersionHeightPosition == SemanticVersion.Position.Revision ? 4 : 3; - // Override the typedVersion with the special build number and revision components, when available. - if (context.IsRepository) - { - this.Version = context.GetIdAsVersion(this.CommittedVersion, this.WorkingVersion, this.VersionHeight); - } + // Override the typedVersion with the special build number and revision components, when available. + if (context.IsRepository) + { + this.Version = context.GetIdAsVersion(this.CommittedVersion, this.WorkingVersion, this.VersionHeight); + } - this.CloudBuildNumberOptions = this.VersionOptions?.CloudBuild?.BuildNumberOrDefault ?? VersionOptions.CloudBuildNumberOptions.DefaultInstance; + this.CloudBuildNumberOptions = this.VersionOptions?.CloudBuild?.BuildNumberOrDefault ?? VersionOptions.CloudBuildNumberOptions.DefaultInstance; - // get the commit id abbreviation only if the commit id is set - if (!string.IsNullOrEmpty(this.GitCommitId)) - { - var gitCommitIdShortFixedLength = this.VersionOptions?.GitCommitIdShortFixedLength ?? VersionOptions.DefaultGitCommitIdShortFixedLength; - var gitCommitIdShortAutoMinimum = this.VersionOptions?.GitCommitIdShortAutoMinimum ?? 0; + // get the commit id abbreviation only if the commit id is set + if (!string.IsNullOrEmpty(this.GitCommitId)) + { + int gitCommitIdShortFixedLength = this.VersionOptions?.GitCommitIdShortFixedLength ?? VersionOptions.DefaultGitCommitIdShortFixedLength; + int gitCommitIdShortAutoMinimum = this.VersionOptions?.GitCommitIdShortAutoMinimum ?? 0; - // Get it from the git repository if there is a repository present and it is enabled. - this.GitCommitIdShort = this.GitCommitId is object && gitCommitIdShortAutoMinimum > 0 - ? this.context.GetShortUniqueCommitId(gitCommitIdShortAutoMinimum) - : this.GitCommitId!.Substring(0, gitCommitIdShortFixedLength); - } + // Get it from the git repository if there is a repository present and it is enabled. + this.GitCommitIdShort = this.GitCommitId is object && gitCommitIdShortAutoMinimum > 0 + ? this.context.GetShortUniqueCommitId(gitCommitIdShortAutoMinimum) + : this.GitCommitId!.Substring(0, gitCommitIdShortFixedLength); + } - if (!string.IsNullOrEmpty(this.BuildingRef) && this.VersionOptions?.PublicReleaseRefSpec?.Count > 0) - { - this.PublicRelease = this.VersionOptions.PublicReleaseRefSpec.Any( - expr => Regex.IsMatch(this.BuildingRef, expr)); - } + if (!string.IsNullOrEmpty(this.BuildingRef) && this.VersionOptions?.PublicReleaseRefSpec?.Count > 0) + { + this.PublicRelease = this.VersionOptions.PublicReleaseRefSpec.Any( + expr => Regex.IsMatch(this.BuildingRef, expr)); } + } - /// - /// Gets the that were deserialized from the contextual commit, if any. - /// - protected VersionOptions? CommittedVersion { get; } + /// + /// Gets the BuildNumber to set the cloud build to (if applicable). + /// + public string CloudBuildNumber + { + get + { + VersionOptions.CloudBuildNumberCommitIdOptions? commitIdOptions = this.CloudBuildNumberOptions.IncludeCommitIdOrDefault; + bool includeCommitInfo = commitIdOptions.WhenOrDefault == VersionOptions.CloudBuildNumberCommitWhen.Always || + (commitIdOptions.WhenOrDefault == VersionOptions.CloudBuildNumberCommitWhen.NonPublicReleaseOnly && !this.PublicRelease); + bool commitIdInBuildMetadata = includeCommitInfo && commitIdOptions.WhereOrDefault == VersionOptions.CloudBuildNumberCommitWhere.BuildMetadata; + + // Include the revision in the build number if, either + // - The commit id is configured to be included as a revision or + // - 3 version fields are configured in version.json (and thus the version height is encoded as revision) or + // - 4 version fields are configured in version.json. + bool includeRevision = (includeCommitInfo && commitIdOptions.WhereOrDefault == VersionOptions.CloudBuildNumberCommitWhere.FourthVersionComponent) || + this.VersionOptions?.Version?.VersionHeightPosition == SemanticVersion.Position.Revision || + this.VersionOptions?.Version?.Version.Revision != -1; + + string buildNumberMetadata = FormatBuildMetadata(commitIdInBuildMetadata ? this.BuildMetadataWithCommitId : this.BuildMetadata); + + Version buildNumberVersion = includeRevision ? this.Version : this.SimpleVersion; + return $"{buildNumberVersion}{this.PrereleaseVersion}{buildNumberMetadata}"; + } + } - /// - /// Gets the that were deserialized from the working tree, if any. - /// - protected VersionOptions? WorkingVersion { get; } + /// + /// Gets a value indicating whether the cloud build number should be set. + /// + [Ignore] + public bool CloudBuildNumberEnabled => this.CloudBuildNumberOptions.EnabledOrDefault; - /// - /// Gets the BuildNumber to set the cloud build to (if applicable). - /// - public string CloudBuildNumber + /// + /// Gets the build metadata identifiers, including the git commit ID as the first identifier if appropriate. + /// + [Ignore] + public IEnumerable BuildMetadataWithCommitId + { + get { - get + if (!string.IsNullOrEmpty(this.GitCommitIdShort)) { - var commitIdOptions = this.CloudBuildNumberOptions.IncludeCommitIdOrDefault; - bool includeCommitInfo = commitIdOptions.WhenOrDefault == VersionOptions.CloudBuildNumberCommitWhen.Always || - (commitIdOptions.WhenOrDefault == VersionOptions.CloudBuildNumberCommitWhen.NonPublicReleaseOnly && !this.PublicRelease); - bool commitIdInBuildMetadata = includeCommitInfo && commitIdOptions.WhereOrDefault == VersionOptions.CloudBuildNumberCommitWhere.BuildMetadata; - - // Include the revision in the build number if, either - // - The commit id is configured to be included as a revision or - // - 3 version fields are configured in version.json (and thus the version height is encoded as revision) or - // - 4 version fields are configured in version.json. - bool includeRevision = includeCommitInfo && commitIdOptions.WhereOrDefault == VersionOptions.CloudBuildNumberCommitWhere.FourthVersionComponent || - this.VersionOptions?.Version?.VersionHeightPosition == SemanticVersion.Position.Revision || - this.VersionOptions?.Version?.Version.Revision != -1; - - string buildNumberMetadata = FormatBuildMetadata(commitIdInBuildMetadata ? this.BuildMetadataWithCommitId : this.BuildMetadata); - - Version buildNumberVersion = includeRevision ? this.Version : this.SimpleVersion; - return $"{buildNumberVersion}{this.PrereleaseVersion}{buildNumberMetadata}"; + yield return this.GitCommitIdShort!; + } + + foreach (string identifier in this.BuildMetadata) + { + yield return identifier; } } + } + + /// + /// Gets a value indicating whether a version.json or version.txt file was found. + /// + public bool VersionFileFound => this.VersionOptions is object; + + /// + /// Gets the version options used to initialize this instance. + /// + [Ignore] + public VersionOptions? VersionOptions { get; } + + /// + /// Gets the version string to use for the . + /// + public Version AssemblyVersion => GetAssemblyVersion(this.Version, this.VersionOptions).EnsureNonNegativeComponents(); + + /// + /// Gets the version string to use for the . + /// + public Version AssemblyFileVersion => this.Version; + + /// + /// Gets the version string to use for the . + /// + public string AssemblyInformationalVersion => + $"{this.Version.ToStringSafe(this.assemblyInformationalVersionComponentCount)}{this.PrereleaseVersion}{FormatBuildMetadata(this.BuildMetadataWithCommitId)}"; + + /// + /// Gets or sets a value indicating whether the project is building + /// in PublicRelease mode. + /// + public bool PublicRelease { get; set; } + + /// + /// Gets the prerelease version information, including a leading hyphen. + /// + public string PrereleaseVersion => this.ReplaceMacros(this.VersionOptions?.Version?.Prerelease ?? string.Empty); + + /// + /// Gets the prerelease version information, omitting the leading hyphen, if any. + /// + public string? PrereleaseVersionNoLeadingHyphen => this.PrereleaseVersion?.TrimStart('-'); + + /// + /// Gets the version information without a Revision component. + /// + public Version SimpleVersion => this.Version.Build >= 0 + ? new Version(this.Version.Major, this.Version.Minor, this.Version.Build) + : new Version(this.Version.Major, this.Version.Minor); + + /// + /// Gets the build number (i.e. third integer, or PATCH) for this version. + /// + public int BuildNumber => Math.Max(0, this.Version.Build); + + /// + /// Gets the component of the . + /// + public int VersionRevision => this.Version.Revision; + + /// + /// Gets the major.minor version string. + /// + /// + /// The x.y string (no build number or revision number). + /// + public Version MajorMinorVersion => new Version(this.Version.Major, this.Version.Minor); + + /// + /// Gets the component of the . + /// + public int VersionMajor => this.Version.Major; + + /// + /// Gets the component of the . + /// + public int VersionMinor => this.Version.Minor; + + /// + /// Gets the Git revision control commit id for HEAD (the current source code version). + /// + public string? GitCommitId => this.context.GitCommitId ?? this.cloudBuild?.GitCommitId; + + /// + /// Gets the first several characters of the Git revision control commit id for HEAD (the current source code version). + /// + public string? GitCommitIdShort { get; } + + /// + /// Gets the Git revision control commit date for HEAD (the current source code version). + /// + public DateTimeOffset? GitCommitDate => this.context.GitCommitDate; + + /// + /// Gets or sets the number of commits in the longest single path between + /// the specified commit and the most distant ancestor (inclusive) + /// that set the version to the value at HEAD. + /// + public int VersionHeight { get; protected set; } + + /// + /// Gets the offset to add to the + /// when calculating the integer to use as the + /// or elsewhere that the {height} macro is used. + /// + public int VersionHeightOffset => this.VersionOptions?.VersionHeightOffsetOrDefault ?? 0; + + /// + /// Gets or sets the ref (branch or tag) being built. + /// + public string? BuildingRef { get; protected set; } + + /// + /// Gets or sets the version for this project, with up to 4 components. + /// + public Version Version { get; protected set; } + + /// + /// Gets a value indicating whether to set all cloud build variables prefaced with "NBGV_". + /// + [Ignore] + public bool CloudBuildAllVarsEnabled => this.VersionOptions?.CloudBuildOrDefault.SetAllVariablesOrDefault + ?? VersionOptions.CloudBuildOptions.DefaultInstance.SetAllVariablesOrDefault; - /// - /// Gets a value indicating whether the cloud build number should be set. - /// - [Ignore] - public bool CloudBuildNumberEnabled => this.CloudBuildNumberOptions.EnabledOrDefault; - - /// - /// Gets the build metadata identifiers, including the git commit ID as the first identifier if appropriate. - /// - [Ignore] - public IEnumerable BuildMetadataWithCommitId + /// + /// Gets a dictionary of all cloud build variables that applies to this project, + /// regardless of the current setting of . + /// + [Ignore] + public IDictionary CloudBuildAllVars + { + get { - get + var variables = new Dictionary(StringComparer.OrdinalIgnoreCase); + + PropertyInfo[] properties = this.GetType().GetTypeInfo().GetProperties(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance); + foreach (PropertyInfo property in properties) { - if (!string.IsNullOrEmpty(this.GitCommitIdShort)) + if (property.GetCustomAttribute() is not null) { - yield return this.GitCommitIdShort!; + continue; } - foreach (string identifier in this.BuildMetadata) + object? propertyValue = property.GetValue(this); + if (propertyValue is null) { - yield return identifier; + continue; } - } - } - /// - /// Gets a value indicating whether a version.json or version.txt file was found. - /// - public bool VersionFileFound => this.VersionOptions is object; - - /// - /// Gets the version options used to initialize this instance. - /// - public VersionOptions? VersionOptions { get; } - - /// - /// Gets the version string to use for the . - /// - public Version AssemblyVersion => GetAssemblyVersion(this.Version, this.VersionOptions).EnsureNonNegativeComponents(); - - /// - /// Gets the version string to use for the . - /// - public Version AssemblyFileVersion => this.Version; - - /// - /// Gets the version string to use for the . - /// - public string AssemblyInformationalVersion => - $"{this.Version.ToStringSafe(this.assemblyInformationalVersionComponentCount)}{this.PrereleaseVersion}{FormatBuildMetadata(this.BuildMetadataWithCommitId)}"; - - /// - /// Gets or sets a value indicating whether the project is building - /// in PublicRelease mode. - /// - public bool PublicRelease { get; set; } - - /// - /// Gets the prerelease version information, including a leading hyphen. - /// - public string PrereleaseVersion => this.ReplaceMacros(this.VersionOptions?.Version?.Prerelease ?? string.Empty); - - /// - /// Gets the prerelease version information, omitting the leading hyphen, if any. - /// - public string? PrereleaseVersionNoLeadingHyphen => this.PrereleaseVersion?.TrimStart('-'); - - /// - /// Gets the version information without a Revision component. - /// - public Version SimpleVersion => this.Version.Build >= 0 - ? new Version(this.Version.Major, this.Version.Minor, this.Version.Build) - : new Version(this.Version.Major, this.Version.Minor); - - /// - /// Gets the build number (i.e. third integer, or PATCH) for this version. - /// - public int BuildNumber => Math.Max(0, this.Version.Build); - - /// - /// Gets the component of the . - /// - public int VersionRevision => this.Version.Revision; - - /// - /// Gets the major.minor version string. - /// - /// - /// The x.y string (no build number or revision number). - /// - public Version MajorMinorVersion => new Version(this.Version.Major, this.Version.Minor); - - /// - /// Gets the component of the . - /// - public int VersionMajor => this.Version.Major; - - /// - /// Gets the component of the . - /// - public int VersionMinor => this.Version.Minor; - - /// - /// Gets the Git revision control commit id for HEAD (the current source code version). - /// - public string? GitCommitId => this.context.GitCommitId ?? this.cloudBuild?.GitCommitId; - - /// - /// Gets the first several characters of the Git revision control commit id for HEAD (the current source code version). - /// - public string? GitCommitIdShort { get; } - - /// - /// Gets the Git revision control commit date for HEAD (the current source code version). - /// - public DateTimeOffset? GitCommitDate => this.context.GitCommitDate; - - /// - /// Gets the number of commits in the longest single path between - /// the specified commit and the most distant ancestor (inclusive) - /// that set the version to the value at HEAD. - /// - public int VersionHeight { get; protected set; } - - /// - /// The offset to add to the - /// when calculating the integer to use as the - /// or elsewhere that the {height} macro is used. - /// - public int VersionHeightOffset => this.VersionOptions?.VersionHeightOffsetOrDefault ?? 0; - - /// - /// Gets the ref (branch or tag) being built. - /// - public string? BuildingRef { get; protected set; } - - /// - /// Gets the version for this project, with up to 4 components. - /// - public Version Version { get; protected set; } - - /// - /// Gets a value indicating whether to set all cloud build variables prefaced with "NBGV_". - /// - [Ignore] - public bool CloudBuildAllVarsEnabled => this.VersionOptions?.CloudBuildOrDefault.SetAllVariablesOrDefault - ?? VersionOptions.CloudBuildOptions.DefaultInstance.SetAllVariablesOrDefault; - - /// - /// Gets a dictionary of all cloud build variables that applies to this project, - /// regardless of the current setting of . - /// - [Ignore] - public IDictionary CloudBuildAllVars - { - get - { - var variables = new Dictionary(StringComparer.OrdinalIgnoreCase); - - var properties = this.GetType().GetTypeInfo().GetProperties(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance); - foreach (var property in properties) + string value = propertyValue switch { - if (property.GetCustomAttribute() is null) - { - var value = property.GetValue(this); - if (value is object) - { - variables.Add($"NBGV_{property.Name}", value.ToString() ?? string.Empty); - } - } - } + DateTimeOffset dateTimeOffset => dateTimeOffset.ToString("o", CultureInfo.InvariantCulture), + _ => Convert.ToString(propertyValue, CultureInfo.InvariantCulture) ?? string.Empty, + }; - return variables; + variables.Add($"NBGV_{property.Name}", value); } + + return variables; } + } - /// - /// Gets a value indicating whether to set cloud build version variables. - /// - [Ignore] - public bool CloudBuildVersionVarsEnabled => this.VersionOptions?.CloudBuildOrDefault.SetVersionVariablesOrDefault - ?? VersionOptions.CloudBuildOptions.DefaultInstance.SetVersionVariablesOrDefault; - - /// - /// Gets a dictionary of cloud build variables that applies to this project, - /// regardless of the current setting of . - /// - [Ignore] - public IDictionary CloudBuildVersionVars + /// + /// Gets a value indicating whether to set cloud build version variables. + /// + [Ignore] + public bool CloudBuildVersionVarsEnabled => this.VersionOptions?.CloudBuildOrDefault.SetVersionVariablesOrDefault + ?? VersionOptions.CloudBuildOptions.DefaultInstance.SetVersionVariablesOrDefault; + + /// + /// Gets a dictionary of cloud build variables that applies to this project, + /// regardless of the current setting of . + /// + [Ignore] + public IDictionary CloudBuildVersionVars + { + get { - get + return new Dictionary(StringComparer.OrdinalIgnoreCase) { - return new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { "GitAssemblyInformationalVersion", this.AssemblyInformationalVersion }, - { "GitBuildVersion", this.Version.ToString() }, - { "GitBuildVersionSimple", this.SimpleVersion.ToString() }, - }; - } + { "GitAssemblyInformationalVersion", this.AssemblyInformationalVersion }, + { "GitBuildVersion", this.Version.ToString() }, + { "GitBuildVersionSimple", this.SimpleVersion.ToString() }, + }; } + } + + /// + /// Gets the list of build metadata identifiers to include in semver version strings. + /// + [Ignore] + public List BuildMetadata { get; } = new List(); + + /// + /// Gets the +buildMetadata fragment for the semantic version. + /// + public string BuildMetadataFragment => FormatBuildMetadata(this.BuildMetadataWithCommitId); + + /// + /// Gets the version to use for NuGet packages. + /// + public string NuGetPackageVersion => this.VersionOptions?.NuGetPackageVersionOrDefault.SemVerOrDefault == 1 ? this.NuGetSemVer1 : this.NuGetSemVer2; + + /// + /// Gets the version to use for Chocolatey packages. + /// + /// + /// This always returns the NuGet subset of SemVer 1.0. + /// + public string ChocolateyPackageVersion => this.NuGetSemVer1; + + /// + /// Gets the version to use for NPM packages. + /// + public string NpmPackageVersion => $"{this.Version.ToStringSafe(3)}{this.PrereleaseVersion}"; - /// - /// Gets the list of build metadata identifiers to include in semver version strings. - /// - [Ignore] - public List BuildMetadata { get; } = new List(); - - /// - /// Gets the +buildMetadata fragment for the semantic version. - /// - public string BuildMetadataFragment => FormatBuildMetadata(this.BuildMetadataWithCommitId); - - /// - /// Gets the version to use for NuGet packages. - /// - public string NuGetPackageVersion => this.VersionOptions?.NuGetPackageVersionOrDefault.SemVerOrDefault == 1 ? this.NuGetSemVer1 : this.NuGetSemVer2; - - /// - /// Gets the version to use for Chocolatey packages. - /// - /// - /// This always returns the NuGet subset of SemVer 1.0. - /// - public string ChocolateyPackageVersion => this.NuGetSemVer1; - - /// - /// Gets the version to use for NPM packages. - /// - public string NpmPackageVersion => this.SemVer2; - - /// - /// Gets a SemVer 1.0 compliant string that represents this version, including the -COMMITID suffix - /// when is false. - /// - public string SemVer1 => - $"{this.Version.ToStringSafe(3)}{this.PrereleaseVersionSemVer1}{this.SemVer1BuildMetadata}"; - - /// - /// Gets a SemVer 2.0 compliant string that represents this version, including a +COMMITID suffix - /// when is false. - /// - public string SemVer2 => - $"{this.Version.ToStringSafe(3)}{this.PrereleaseVersion}{this.SemVer2BuildMetadata}"; - - /// - /// Gets the minimum number of digits to use for numeric identifiers in SemVer 1. - /// - public int SemVer1NumericIdentifierPadding => this.VersionOptions?.SemVer1NumericIdentifierPaddingOrDefault ?? 4; - - /// - /// Gets or sets the . - /// - protected VersionOptions.CloudBuildNumberOptions CloudBuildNumberOptions { get; set; } - - /// - /// Gets the build metadata, compliant to the NuGet-compatible subset of SemVer 1.0. - /// - /// - /// When adding the git commit ID in a -prerelease tag, prefix a `g` because - /// older NuGet clients (the ones that support only a subset of semver 1.0) - /// cannot handle prerelease tags that begin with a number (which a git commit ID might). - /// See this discussion. - /// - private string NuGetSemVer1BuildMetadata => - this.PublicRelease ? string.Empty : $"-{this.VersionOptions?.GitCommitIdPrefix ?? "g"}{this.GitCommitIdShort}"; - - /// - /// Gets the build metadata, compliant to SemVer 1.0. - /// - private string SemVer1BuildMetadata => - this.PublicRelease ? string.Empty : $"-{this.GitCommitIdShort}"; - - /// - /// Gets a SemVer 1.0 compliant string that represents this version, including the -gCOMMITID suffix - /// when is false. - /// - private string NuGetSemVer1 + /// + /// Gets a SemVer 1.0 compliant string that represents this version, including the -COMMITID suffix + /// when is . + /// + public string SemVer1 => + $"{this.Version.ToStringSafe(3)}{this.PrereleaseVersionSemVer1}{this.SemVer1BuildMetadata}"; + + /// + /// Gets a SemVer 2.0 compliant string that represents this version, including a +COMMITID suffix + /// when is . + /// + public string SemVer2 => + $"{this.Version.ToStringSafe(3)}{this.PrereleaseVersion}{this.SemVer2BuildMetadata}"; + + /// + /// Gets the minimum number of digits to use for numeric identifiers in SemVer 1. + /// + public int SemVer1NumericIdentifierPadding => this.VersionOptions?.SemVer1NumericIdentifierPaddingOrDefault ?? 4; + + /// + /// Gets or sets the . + /// + protected VersionOptions.CloudBuildNumberOptions CloudBuildNumberOptions { get; set; } + + /// + /// Gets the that were deserialized from the contextual commit, if any. + /// + protected VersionOptions? CommittedVersion { get; } + + /// + /// Gets the that were deserialized from the working tree, if any. + /// + protected VersionOptions? WorkingVersion { get; } + + /// + /// Gets the build metadata, compliant to the NuGet-compatible subset of SemVer 1.0. + /// + /// + /// When adding the git commit ID in a -prerelease tag, prefix a `g` because + /// older NuGet clients (the ones that support only a subset of semver 1.0) + /// cannot handle prerelease tags that begin with a number (which a git commit ID might). + /// See this discussion. + /// + private string NuGetSemVer1BuildMetadata => + this.PublicRelease ? string.Empty : $"-{this.VersionOptions?.GitCommitIdPrefix ?? "g"}{this.GitCommitIdShort}"; + + /// + /// Gets the build metadata, compliant to SemVer 1.0. + /// + private string SemVer1BuildMetadata => + this.PublicRelease ? string.Empty : $"-{this.GitCommitIdShort}"; + + /// + /// Gets a SemVer 1.0 compliant string that represents this version, including the -gCOMMITID suffix + /// when is . + /// + private string NuGetSemVer1 + { + get { - get - { - var precision = this.VersionOptions?.NuGetPackageVersionOrDefault.PrecisionOrDefault ?? VersionOptions.NuGetPackageVersionOptions.DefaultPrecision; - var version = this.Version.EnsureNonNegativeComponents(); - version = ApplyVersionPrecision(version, precision); + VersionOptions.VersionPrecision precision = this.VersionOptions?.NuGetPackageVersionOrDefault.PrecisionOrDefault ?? VersionOptions.NuGetPackageVersionOptions.DefaultPrecision; + Version? version = this.Version.EnsureNonNegativeComponents(); + version = ApplyVersionPrecision(version, precision); - // If precision is set to include the 4th version component, return all 4 version fields, otherwise return 3 fields. - var fieldCount = precision >= VersionOptions.VersionPrecision.Revision ? 4 : 3; + // If precision is set to include the 4th version component, return all 4 version fields, otherwise return 3 fields. + int fieldCount = precision >= VersionOptions.VersionPrecision.Revision ? 4 : 3; - return $"{version.ToStringSafe(fieldCount)}{this.PrereleaseVersionSemVer1}{this.NuGetSemVer1BuildMetadata}"; - } + return $"{version.ToStringSafe(fieldCount)}{this.PrereleaseVersionSemVer1}{this.NuGetSemVer1BuildMetadata}"; } + } - /// - /// Gets a SemVer 2.0 compliant string that represents this version, including the -gCOMMITID suffix - /// when is false. - /// - private string NuGetSemVer2 + /// + /// Gets a SemVer 2.0 compliant string that represents this version, including the -gCOMMITID suffix + /// when is . + /// + private string NuGetSemVer2 + { + get { - get - { - var precision = this.VersionOptions?.NuGetPackageVersionOrDefault.PrecisionOrDefault ?? VersionOptions.NuGetPackageVersionOptions.DefaultPrecision; - var version = this.Version.EnsureNonNegativeComponents(); - version = ApplyVersionPrecision(version, precision); + VersionOptions.VersionPrecision precision = this.VersionOptions?.NuGetPackageVersionOrDefault.PrecisionOrDefault ?? VersionOptions.NuGetPackageVersionOptions.DefaultPrecision; + Version? version = this.Version.EnsureNonNegativeComponents(); + version = ApplyVersionPrecision(version, precision); - // If precision is set to include the 4th version component, return all 4 version fields, otherwise return 3 fields. - var fieldCount = precision >= VersionOptions.VersionPrecision.Revision ? 4 : 3; + // If precision is set to include the 4th version component, return all 4 version fields, otherwise return 3 fields. + int fieldCount = precision >= VersionOptions.VersionPrecision.Revision ? 4 : 3; - return $"{version.ToStringSafe(fieldCount)}{this.PrereleaseVersion}{this.SemVer2BuildMetadata}"; - } + return $"{version.ToStringSafe(fieldCount)}{this.PrereleaseVersion}{this.SemVer2BuildMetadata}"; } + } - /// - /// Gets the build metadata that is appropriate for SemVer2 use. - /// - /// - /// We always put the commit ID in the -prerelease tag for non-public releases. - /// But for public releases, we don't include it in the +buildMetadata section since it may be confusing for NuGet. - /// See https://github.com/dotnet/Nerdbank.GitVersioning/pull/132#issuecomment-307208561 - /// - private string SemVer2BuildMetadata => - (this.PublicRelease ? string.Empty : this.GitCommitIdShortForNonPublicPrereleaseTag) + FormatBuildMetadata(this.BuildMetadata); - - private string PrereleaseVersionSemVer1 => SemanticVersionExtensions.MakePrereleaseSemVer1Compliant(this.PrereleaseVersion, this.SemVer1NumericIdentifierPadding); - - /// - /// Gets the -gc0ffee or .gc0ffee suffix for the version. - /// The g in the prefix might be changed if is set. - /// - /// - /// The prefix to the commit ID is to remain SemVer2 compliant particularly when the partial commit ID we use is made up entirely of numerals. - /// SemVer2 forbids numerals to begin with leading zeros, but a git commit just might, so we begin with prefix always to avoid failures when the commit ID happens to be problematic. - /// - private string GitCommitIdShortForNonPublicPrereleaseTag => (string.IsNullOrEmpty(this.PrereleaseVersion) ? "-" : ".") + (this.VersionOptions?.GitCommitIdPrefix ?? "g") + this.GitCommitIdShort; - - private int VersionHeightWithOffset => this.VersionHeight + this.VersionHeightOffset; - - private static string FormatBuildMetadata(IEnumerable identifiers) => - (identifiers?.Any() ?? false) ? "+" + string.Join(".", identifiers) : string.Empty; - - private static Version GetAssemblyVersion(Version version, VersionOptions? versionOptions) - { - // If there is no repo, "version" could have uninitialized components (-1). - version = version.EnsureNonNegativeComponents(); + /// + /// Gets the build metadata that is appropriate for SemVer2 use. + /// + /// + /// We always put the commit ID in the -prerelease tag for non-public releases. + /// But for public releases, we don't include it in the +buildMetadata section since it may be confusing for NuGet. + /// + /// + private string SemVer2BuildMetadata => + (this.PublicRelease ? string.Empty : this.GitCommitIdShortForNonPublicPrereleaseTag) + FormatBuildMetadata(this.BuildMetadata); - Version assemblyVersion; + private string PrereleaseVersionSemVer1 => SemanticVersionExtensions.MakePrereleaseSemVer1Compliant(this.PrereleaseVersion, this.SemVer1NumericIdentifierPadding); - if (versionOptions?.AssemblyVersion?.Version is not null) - { - // When specified explicitly, use the assembly version as the user defines it. - assemblyVersion = versionOptions.AssemblyVersion.Version; - } - else - { - // Otherwise consider precision to base the assembly version off of the main computed version. - VersionOptions.VersionPrecision precision = versionOptions?.AssemblyVersion?.Precision ?? VersionOptions.DefaultVersionPrecision; - assemblyVersion = ApplyVersionPrecision(version, precision); - } + /// + /// Gets the -gc0ffee or .gc0ffee suffix for the version. + /// The g in the prefix might be changed if is set. + /// + /// + /// The prefix to the commit ID is to remain SemVer2 compliant particularly when the partial commit ID we use is made up entirely of numerals. + /// SemVer2 forbids numerals to begin with leading zeros, but a git commit just might, so we begin with prefix always to avoid failures when the commit ID happens to be problematic. + /// + private string GitCommitIdShortForNonPublicPrereleaseTag => (string.IsNullOrEmpty(this.PrereleaseVersion) ? "-" : ".") + (this.VersionOptions?.GitCommitIdPrefix ?? "g") + this.GitCommitIdShort; - return assemblyVersion.EnsureNonNegativeComponents(4); - } + private int VersionHeightWithOffset => this.VersionHeight + this.VersionHeightOffset; - private static Version ApplyVersionPrecision(Version version, VersionOptions.VersionPrecision precision) - { - return new Version( - version.Major, - precision >= VersionOptions.VersionPrecision.Minor ? version.Minor : 0, - precision >= VersionOptions.VersionPrecision.Build ? version.Build : 0, - precision >= VersionOptions.VersionPrecision.Revision ? version.Revision : 0); - } + private static string FormatBuildMetadata(IEnumerable identifiers) => + (identifiers?.Any() ?? false) ? "+" + string.Join(".", identifiers) : string.Empty; - /// - /// Replaces any macros found in a prerelease or build metadata string. - /// - /// The prerelease or build metadata. - /// The specified string, with macros substituted for actual values. - private string ReplaceMacros(string prereleaseOrBuildMetadata) => prereleaseOrBuildMetadata.Replace(VersionOptions.VersionHeightPlaceholder, this.VersionHeightWithOffset.ToString(CultureInfo.InvariantCulture)); + private static Version GetAssemblyVersion(Version version, VersionOptions? versionOptions) + { + // If there is no repo, "version" could have uninitialized components (-1). + version = version.EnsureNonNegativeComponents(); - [AttributeUsage(AttributeTargets.Property)] - private class IgnoreAttribute : Attribute + Version assemblyVersion; + + if (versionOptions?.AssemblyVersion?.Version is not null) + { + // When specified explicitly, use the assembly version as the user defines it. + assemblyVersion = versionOptions.AssemblyVersion.Version; + } + else { + // Otherwise consider precision to base the assembly version off of the main computed version. + VersionOptions.VersionPrecision precision = versionOptions?.AssemblyVersion?.Precision ?? VersionOptions.DefaultVersionPrecision; + assemblyVersion = ApplyVersionPrecision(version, precision); } + + return assemblyVersion.EnsureNonNegativeComponents(4); + } + + private static Version ApplyVersionPrecision(Version version, VersionOptions.VersionPrecision precision) + { + return new Version( + version.Major, + precision >= VersionOptions.VersionPrecision.Minor ? version.Minor : 0, + precision >= VersionOptions.VersionPrecision.Build ? version.Build : 0, + precision >= VersionOptions.VersionPrecision.Revision ? version.Revision : 0); + } + + /// + /// Replaces any macros found in a prerelease or build metadata string. + /// + /// The prerelease or build metadata. + /// The specified string, with macros substituted for actual values. + private string ReplaceMacros(string prereleaseOrBuildMetadata) => prereleaseOrBuildMetadata.Replace(VersionOptions.VersionHeightPlaceholder, this.VersionHeightWithOffset.ToString(CultureInfo.InvariantCulture)); + + [AttributeUsage(AttributeTargets.Property)] + private class IgnoreAttribute : Attribute + { } } diff --git a/src/NerdBank.GitVersioning/version.schema.json b/src/NerdBank.GitVersioning/version.schema.json index 3d50d72f..44a0bb72 100644 --- a/src/NerdBank.GitVersioning/version.schema.json +++ b/src/NerdBank.GitVersioning/version.schema.json @@ -1,4 +1,4 @@ -{ +{ "$schema": "http://json-schema.org/draft-04/schema", "title": "Nerdbank.GitVersioning version.json schema", "type": "object", @@ -119,7 +119,7 @@ }, "publicReleaseRefSpec": { "type": "array", - "description": "An array of regular expressions that may match a ref (branch or tag) that should be built with PublicRelease=true as the default value. The ref matched against is in its canonical form (e.g. refs/heads/master)", + "description": "An array of regular expressions that may match a ref (branch or tag) that should be built with PublicRelease=true as the default value. The ref matched against is in its canonical form (e.g. refs/heads/master).", "items": { "type": "string", "format": "regex" @@ -128,12 +128,12 @@ }, "cloudBuild": { "type": "object", - "description": "Options that are applicable specifically to cloud builds (e.g. VSTS, AppVeyor, TeamCity)", + "description": "Options that are applicable specifically to cloud builds (e.g. VSTS, AppVeyor, TeamCity).", "properties": { "setAllVariables": { "type": "boolean", "default": false, - "description": "Elevates all build properties to cloud build variables prefaced with \"NBGV_\"" + "description": "Elevates all build properties to cloud build variables prefaced with \"NBGV_\"." }, "setVersionVariables": { "type": "boolean", @@ -172,13 +172,19 @@ } }, "release": { - "description": "Settings for the prepare-release command", + "description": "Settings for the prepare-release and tag commands.", "type": "object", "properties": { + "tagName": { + "description": "Defines the format of tag names. Format must include a placeholder '{version}' for the version.", + "type": "string", + "pattern": "\\{version\\}", + "default": "v{version}" + }, "branchName": { - "description": "Defines the format of release branch names. Format must include a placeholder '{version}' for the version", + "description": "Defines the format of release branch names. Format must include a placeholder '{version}' for the version.", "type": "string", - "pattern": ".*\\{version\\}.*", + "pattern": "\\{version\\}", "default": "v{version}" }, "versionIncrement": { @@ -188,7 +194,7 @@ "default": "minor" }, "firstUnstableTag": { - "description": "Specifies the first/default prerelease tag for new versions", + "description": "Specifies the first/default prerelease tag for new versions.", "type": "string", "default": "alpha" } diff --git a/src/Nerdbank.GitVersioning.Tasks/AssemblyLoader.cs b/src/Nerdbank.GitVersioning.Tasks/AssemblyLoader.cs index 55da8b8b..ab99844e 100644 --- a/src/Nerdbank.GitVersioning.Tasks/AssemblyLoader.cs +++ b/src/Nerdbank.GitVersioning.Tasks/AssemblyLoader.cs @@ -1,4 +1,11 @@ -#if NETFRAMEWORK +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#pragma warning disable SA1402 // File may only contain a single type +#pragma warning disable SA1403 // File may only contain a single namespace +#pragma warning disable SA1649 // File name should match first type name + +#if NETFRAMEWORK using System; using System.IO; @@ -8,14 +15,18 @@ namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public sealed class ModuleInitializerAttribute : Attribute { } + public sealed class ModuleInitializerAttribute : Attribute + { + } } namespace Nerdbank.GitVersioning.Tasks { internal static class AssemblyLoader { +#pragma warning disable CA2255 // The 'ModuleInitializer' attribute should not be used in libraries [ModuleInitializer] +#pragma warning restore CA2255 // The 'ModuleInitializer' attribute should not be used in libraries internal static void LoaderInitializer() { AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; diff --git a/src/Nerdbank.GitVersioning.Tasks/AssemblyVersionInfo.cs b/src/Nerdbank.GitVersioning.Tasks/AssemblyVersionInfo.cs index 2f39ca05..6ff17c7b 100644 --- a/src/Nerdbank.GitVersioning.Tasks/AssemblyVersionInfo.cs +++ b/src/Nerdbank.GitVersioning.Tasks/AssemblyVersionInfo.cs @@ -1,39 +1,31 @@ -namespace Nerdbank.GitVersioning.Tasks +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.CodeDom; +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Validation; + +namespace Nerdbank.GitVersioning.Tasks { - using System; - using System.CodeDom; - using System.CodeDom.Compiler; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Reflection; - using System.Text; - using Microsoft.Build.Framework; - using Microsoft.Build.Utilities; - using Validation; - - public class AssemblyVersionInfo : Task + public class AssemblyVersionInfo : Microsoft.Build.Utilities.Task { + public static readonly string GeneratorName = ThisAssembly.AssemblyName; + public static readonly string GeneratorVersion = ThisAssembly.AssemblyVersion; + /// /// The #if expression that surrounds a to avoid a compilation failure when targeting the nano framework. /// - /// - /// See https://github.com/dotnet/Nerdbank.GitVersioning/issues/346 - /// + /// private const string CompilerDefinesAroundGeneratedCodeAttribute = "NETSTANDARD || NETFRAMEWORK || NETCOREAPP"; - private const string CompilerDefinesAroundExcludeFromCodeCoverageAttribute = "NETFRAMEWORK || NETCOREAPP || NETSTANDARD2_0 || NETSTANDARD2_1"; - - public static readonly string GeneratorName = ThisAssembly.AssemblyName; - public static readonly string GeneratorVersion = ThisAssembly.AssemblyVersion; -#if NET461 - private static readonly CodeGeneratorOptions codeGeneratorOptions = new CodeGeneratorOptions - { - BlankLinesBetweenMembers = false, - IndentString = " ", - }; - - private CodeCompileUnit generatedFile; -#endif + private const string CompilerDefinesAroundExcludeFromCodeCoverageAttribute = "NET40_OR_GREATER || NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER"; private const string FileHeaderComment = @"------------------------------------------------------------------------------ This code was generated by a tool. @@ -45,6 +37,16 @@ the code is regenerated. ------------------------------------------------------------------------------ "; +#if NET462 + private static readonly CodeGeneratorOptions CodeGeneratorOptions = new CodeGeneratorOptions + { + BlankLinesBetweenMembers = false, + IndentString = " ", + }; + + private CodeCompileUnit generatedFile; +#endif + private CodeGenerator generator; [Required] @@ -65,6 +67,8 @@ the code is regenerated. public string RootNamespace { get; set; } + public string ThisAssemblyNamespace { get; set; } + public string AssemblyOriginatorKeyFile { get; set; } public string AssemblyKeyContainerName { get; set; } @@ -90,7 +94,7 @@ the code is regenerated. public bool EmitThisAssemblyClass { get; set; } = true; /// - /// Specify additional fields to be added to the ThisAssembly class. + /// Gets or sets the additional fields to be added to the ThisAssembly class. /// /// /// Field name is given by %(Identity). Provide the field value by specifying exactly one metadata value that is %(String), %(Boolean) or %(Ticks) (for UTC DateTime). @@ -108,7 +112,32 @@ the code is regenerated. /// public ITaskItem[] AdditionalThisAssemblyFields { get; set; } -#if NET461 + public string BuildCode() + { + this.generator = this.CreateGenerator(this.ThisAssemblyNamespace, this.RootNamespace); + + if (this.generator is object) + { + this.generator.AddComment(FileHeaderComment); + this.generator.AddBlankLine(); + this.generator.AddAnalysisSuppressions(); + this.generator.AddBlankLine(); + + this.GenerateAssemblyAttributes(); + + if (this.EmitThisAssemblyClass) + { + this.GenerateThisAssemblyClass(); + } + + return this.generator.GetCode(); + } + + return null; + } + +#if NET462 + /// public override bool Execute() { // attempt to use local codegen @@ -140,7 +169,7 @@ public override bool Execute() { using (var fileWriter = new StreamWriter(file, new UTF8Encoding(true), 4096, leaveOpen: true)) { - codeDomProvider.GenerateCodeFromCompileUnit(this.generatedFile, fileWriter, codeGeneratorOptions); + codeDomProvider.GenerateCodeFromCompileUnit(this.generatedFile, fileWriter, CodeGeneratorOptions); } // truncate to new size. @@ -155,6 +184,104 @@ public override bool Execute() return !this.Log.HasLoggedErrors; } +#endif + +#if !NET462 + /// + public override bool Execute() + { + string fileContent = this.BuildCode(); + if (fileContent is object) + { + Directory.CreateDirectory(Path.GetDirectoryName(this.OutputFile)); + Utilities.FileOperationWithRetry(() => File.WriteAllText(this.OutputFile, fileContent)); + } + else + { + this.Log.LogError("CodeDomProvider not available for language: {0}. No version info will be embedded into assembly.", this.CodeLanguage); + } + + return !this.Log.HasLoggedErrors; + } +#endif + + private static string ToHex(byte[] data) + { + return BitConverter.ToString(data).Replace("-", string.Empty).ToLowerInvariant(); + } + + /// + /// Gets the public key from a key container. + /// + /// The name of the container. + /// The public key. + private static byte[] GetPublicKeyFromKeyContainer(string containerName) + { + throw new NotImplementedException(); + } + + private static byte[] GetPublicKeyFromKeyPair(byte[] keyPair) + { + byte[] publicKey; + if (CryptoBlobParser.TryGetPublicKeyFromPrivateKeyBlob(keyPair, out publicKey)) + { + return publicKey; + } + else + { + throw new ArgumentException("Invalid keypair"); + } + } + +#if NET462 + private static CodeMemberField CreateField(string name, T value) + { + return new CodeMemberField(typeof(T), name) + { + Attributes = MemberAttributes.Const | MemberAttributes.Assembly, + InitExpression = new CodePrimitiveExpression(value), + }; + } + + private static IEnumerable CreateDateTimeField(string name, DateTime value) + { + Requires.NotNullOrEmpty(name, nameof(name)); + + ////internal static System.DateTime GitCommitDate => new System.DateTime({ticks}, System.DateTimeKind.Utc);"); + + var property = new CodeMemberProperty() + { + Attributes = MemberAttributes.Assembly | MemberAttributes.Static | MemberAttributes.Final, + Type = new CodeTypeReference(typeof(DateTime)), + Name = name, + HasGet = true, + HasSet = false, + }; + + property.GetStatements.Add( + new CodeMethodReturnStatement( + new CodeObjectCreateExpression( + typeof(DateTime), + new CodePrimitiveExpression(value.Ticks), + new CodePropertyReferenceExpression( + new CodeTypeReferenceExpression(typeof(DateTimeKind)), + nameof(DateTimeKind.Utc))))); + + yield return property; + } + + private static CodeAttributeDeclaration DeclareAttribute(Type attributeType, params CodeAttributeArgument[] arguments) + { + var assemblyTypeReference = new CodeTypeReference(attributeType); + return new CodeAttributeDeclaration(assemblyTypeReference, arguments); + } + + private static CodeAttributeDeclaration DeclareAttribute(Type attributeType, params string[] arguments) + { + return DeclareAttribute( + attributeType, + arguments.Select(a => new CodeAttributeArgument(new CodePrimitiveExpression(a))).ToArray()); + } private CodeTypeDeclaration CreateThisAssemblyClass() { @@ -166,7 +293,8 @@ private CodeTypeDeclaration CreateThisAssemblyClass() }; var codeAttributeDeclarationCollection = new CodeAttributeDeclarationCollection(); - codeAttributeDeclarationCollection.Add(new CodeAttributeDeclaration("System.CodeDom.Compiler.GeneratedCode", + codeAttributeDeclarationCollection.Add(new CodeAttributeDeclaration( + "System.CodeDom.Compiler.GeneratedCode", new CodeAttributeArgument(new CodePrimitiveExpression(GeneratorName)), new CodeAttributeArgument(new CodePrimitiveExpression(GeneratorVersion)))); thisAssembly.CustomAttributes = codeAttributeDeclarationCollection; @@ -174,9 +302,9 @@ private CodeTypeDeclaration CreateThisAssemblyClass() // CodeDOM doesn't support static classes, so hide the constructor instead. thisAssembly.Members.Add(new CodeConstructor { Attributes = MemberAttributes.Private }); - var fields = this.GetFieldsForThisAssembly(); + List> fields = this.GetFieldsForThisAssembly(); - foreach (var pair in fields) + foreach (KeyValuePair pair in fields) { switch (pair.Value.Value) { @@ -192,6 +320,7 @@ private CodeTypeDeclaration CreateThisAssemblyClass() { thisAssembly.Members.Add(CreateField(pair.Key, stringValue)); } + break; case bool boolValue: @@ -238,101 +367,11 @@ private IEnumerable CreateAssemblyAttributes() } } } - - private static CodeMemberField CreateField(string name, T value) - { - return new CodeMemberField(typeof(T), name) - { - Attributes = MemberAttributes.Const | MemberAttributes.Assembly, - InitExpression = new CodePrimitiveExpression(value), - }; - } - - private static IEnumerable CreateDateTimeField(string name, DateTime value) - { - Requires.NotNullOrEmpty(name, nameof(name)); - - // internal static System.DateTime GitCommitDate => new System.DateTime({ticks}, System.DateTimeKind.Utc);"); - - var property = new CodeMemberProperty() - { - Attributes = MemberAttributes.Assembly | MemberAttributes.Static | MemberAttributes.Final, - Type = new CodeTypeReference(typeof(DateTime)), - Name = name, - HasGet = true, - HasSet = false, - }; - - property.GetStatements.Add( - new CodeMethodReturnStatement( - new CodeObjectCreateExpression( - typeof(DateTime), - new CodePrimitiveExpression(value.Ticks), - new CodePropertyReferenceExpression( - new CodeTypeReferenceExpression(typeof(DateTimeKind)), - nameof(DateTimeKind.Utc))))); - - yield return property; - } - - private static CodeAttributeDeclaration DeclareAttribute(Type attributeType, params CodeAttributeArgument[] arguments) - { - var assemblyTypeReference = new CodeTypeReference(attributeType); - return new CodeAttributeDeclaration(assemblyTypeReference, arguments); - } - - private static CodeAttributeDeclaration DeclareAttribute(Type attributeType, params string[] arguments) - { - return DeclareAttribute( - attributeType, - arguments.Select(a => new CodeAttributeArgument(new CodePrimitiveExpression(a))).ToArray()); - } - -#else - - public override bool Execute() - { - string fileContent = this.BuildCode(); - if (fileContent is object) - { - Directory.CreateDirectory(Path.GetDirectoryName(this.OutputFile)); - Utilities.FileOperationWithRetry(() => File.WriteAllText(this.OutputFile, fileContent)); - } - else - { - this.Log.LogError("CodeDomProvider not available for language: {0}. No version info will be embedded into assembly.", this.CodeLanguage); - } - - return !this.Log.HasLoggedErrors; - } - #endif - public string BuildCode() - { - this.generator = this.CreateGenerator(); - if (this.generator is object) - { - this.generator.AddComment(FileHeaderComment); - this.generator.AddBlankLine(); - this.generator.AddAnalysisSuppressions(); - this.generator.AddBlankLine(); - this.generator.EmitNamespaceIfRequired(this.RootNamespace ?? "AssemblyInfo"); - this.GenerateAssemblyAttributes(); - - if (this.EmitThisAssemblyClass) - { - this.GenerateThisAssemblyClass(); - } - - return this.generator.GetCode(); - } - - return null; - } - private void GenerateAssemblyAttributes() { + this.generator.StartAssemblyAttributes(); this.generator.DeclareAttribute(typeof(AssemblyVersionAttribute), this.AssemblyVersion); this.generator.DeclareAttribute(typeof(AssemblyFileVersionAttribute), this.AssemblyFileVersion); this.generator.DeclareAttribute(typeof(AssemblyInformationalVersionAttribute), this.AssemblyInformationalVersion); @@ -379,8 +418,10 @@ private void GenerateAssemblyAttributes() { "AssemblyCompany", (this.AssemblyCompany, false) }, { "AssemblyConfiguration", (this.AssemblyConfiguration, false) }, { "GitCommitId", (this.GitCommitId, false) }, + // These properties should be defined even if they are empty strings: { "RootNamespace", (this.RootNamespace, true) }, + // These non-string properties are always emitted: { "IsPublicRelease", (this.PublicRelease, true) }, { "IsPrerelease", (!string.IsNullOrEmpty(this.PrereleaseVersion), true) }, @@ -399,9 +440,9 @@ private void GenerateAssemblyAttributes() if (this.AdditionalThisAssemblyFields is object) { - foreach (var item in this.AdditionalThisAssemblyFields) + foreach (ITaskItem item in this.AdditionalThisAssemblyFields) { - var name = item.ItemSpec.Trim(); + string name = item.ItemSpec.Trim(); var meta = new Dictionary(item.MetadataCount, StringComparer.OrdinalIgnoreCase); foreach (string metadataName in item.MetadataNames) { @@ -485,9 +526,9 @@ private void GenerateThisAssemblyClass() { this.generator.StartThisAssemblyClass(); - var fields = this.GetFieldsForThisAssembly(); + List> fields = this.GetFieldsForThisAssembly(); - foreach (var pair in fields) + foreach (KeyValuePair pair in fields) { switch (pair.Value.Value) { @@ -496,6 +537,7 @@ private void GenerateThisAssemblyClass() { this.generator.AddThisAssemblyMember(pair.Key, string.Empty); } + break; case string stringValue: @@ -503,6 +545,7 @@ private void GenerateThisAssemblyClass() { this.generator.AddThisAssemblyMember(pair.Key, stringValue); } + break; case bool boolValue: @@ -521,32 +564,87 @@ private void GenerateThisAssemblyClass() this.generator.EndThisAssemblyClass(); } - private CodeGenerator CreateGenerator() + private CodeGenerator CreateGenerator(string thisAssemblyNamespace, string rootNamespace) { + // The C#/VB generators did not emit namespaces in past versions of NB.GV, so for compatibility, only check the + // new ThisAssemblyNamespace property for these. + var userNs = !string.IsNullOrEmpty(thisAssemblyNamespace) ? thisAssemblyNamespace : null; + switch (this.CodeLanguage.ToLowerInvariant()) { case "c#": - return new CSharpCodeGenerator(); + return new CSharpCodeGenerator(userNs); case "visual basic": case "visualbasic": case "vb": - return new VisualBasicCodeGenerator(); + return new VisualBasicCodeGenerator(userNs); case "f#": - return new FSharpCodeGenerator(); + // The F# generator must emit a namespace, so it respects both ThisAssemblyNamespace and RootNamespace. + return new FSharpCodeGenerator(userNs ?? (!string.IsNullOrEmpty(rootNamespace) ? rootNamespace : "AssemblyInfo")); default: return null; } } - private abstract class CodeGenerator + private bool TryReadKeyInfo(out string publicKey, out string publicKeyToken) { - protected readonly StringBuilder codeBuilder; + try + { + byte[] publicKeyBytes = null; + if (!string.IsNullOrEmpty(this.AssemblyOriginatorKeyFile) && File.Exists(this.AssemblyOriginatorKeyFile)) + { + if (Path.GetExtension(this.AssemblyOriginatorKeyFile).Equals(".snk", StringComparison.OrdinalIgnoreCase)) + { + byte[] keyBytes = File.ReadAllBytes(this.AssemblyOriginatorKeyFile); + bool publicKeyOnly = keyBytes[0] != 0x07; + publicKeyBytes = publicKeyOnly ? keyBytes : GetPublicKeyFromKeyPair(keyBytes); + } + } + else if (!string.IsNullOrEmpty(this.AssemblyKeyContainerName)) + { + publicKeyBytes = GetPublicKeyFromKeyContainer(this.AssemblyKeyContainerName); + } + + // If .NET 2.0 isn't installed, we get byte[0] back. + if (publicKeyBytes is object && publicKeyBytes.Length > 0) + { + publicKey = ToHex(publicKeyBytes); + publicKeyToken = ToHex(CryptoBlobParser.GetStrongNameTokenFromPublicKey(publicKeyBytes)); + } + else + { + if (publicKeyBytes is object) + { + this.Log.LogWarning("Unable to emit public key fields in ThisAssembly class because .NET 2.0 isn't installed."); + } - internal CodeGenerator() + publicKey = null; + publicKeyToken = null; + return false; + } + + return true; + } + catch (NotImplementedException) + { + publicKey = null; + publicKeyToken = null; + return false; + } + } + + private abstract class CodeGenerator + { + internal CodeGenerator(string ns) { - this.codeBuilder = new StringBuilder(); + this.CodeBuilder = new StringBuilder(); + this.Namespace = ns; } + protected StringBuilder CodeBuilder { get; } + + protected string Namespace { get; } + protected virtual IEnumerable WarningCodesToSuppress { get; } = new string[] { "CA2243", // Attribute string literals should parse correctly @@ -556,6 +654,10 @@ internal CodeGenerator() internal abstract void AddComment(string comment); + internal virtual void StartAssemblyAttributes() + { + } + internal abstract void DeclareAttribute(Type type, string arg); internal abstract void StartThisAssemblyClass(); @@ -568,17 +670,11 @@ internal CodeGenerator() internal abstract void EndThisAssemblyClass(); - /// - /// Gives languages that *require* a namespace a chance to emit such. - /// - /// The RootNamespace of the project. - internal virtual void EmitNamespaceIfRequired(string ns) { } - - internal string GetCode() => this.codeBuilder.ToString(); + internal string GetCode() => this.CodeBuilder.ToString(); internal void AddBlankLine() { - this.codeBuilder.AppendLine(); + this.CodeBuilder.AppendLine(); } protected void AddCodeComment(string comment, string token) @@ -587,17 +683,22 @@ protected void AddCodeComment(string comment, string token) string line; while ((line = sr.ReadLine()) is object) { - this.codeBuilder.Append(token); - this.codeBuilder.AppendLine(line); + this.CodeBuilder.Append(token); + this.CodeBuilder.AppendLine(line); } } } private class FSharpCodeGenerator : CodeGenerator { + public FSharpCodeGenerator(string ns) + : base(ns) + { + } + internal override void AddAnalysisSuppressions() { - this.codeBuilder.AppendLine($"#nowarn {string.Join(" ", this.WarningCodesToSuppress.Select(c => $"\"{c}\""))}"); + this.CodeBuilder.AppendLine($"#nowarn {string.Join(" ", this.WarningCodesToSuppress.Select(c => $"\"{c}\""))}"); } internal override void AddComment(string comment) @@ -607,52 +708,57 @@ internal override void AddComment(string comment) internal override void AddThisAssemblyMember(string name, string value) { - this.codeBuilder.AppendLine($" static member internal {name} = \"{value}\""); + this.CodeBuilder.AppendLine($" static member internal {name} = \"{value}\""); } internal override void AddThisAssemblyMember(string name, bool value) { - this.codeBuilder.AppendLine($" static member internal {name} = {(value ? "true" : "false")}"); + this.CodeBuilder.AppendLine($" static member internal {name} = {(value ? "true" : "false")}"); } internal override void AddThisAssemblyMember(string name, DateTime value) { - this.codeBuilder.AppendLine($" static member internal {name} = new System.DateTime({value.Ticks}L, System.DateTimeKind.Utc)"); + this.CodeBuilder.AppendLine($" static member internal {name} = new System.DateTime({value.Ticks}L, System.DateTimeKind.Utc)"); } - internal override void EmitNamespaceIfRequired(string ns) + internal override void StartAssemblyAttributes() { - this.codeBuilder.AppendLine($"namespace {ns}"); + this.CodeBuilder.AppendLine($"namespace {this.Namespace}"); } internal override void DeclareAttribute(Type type, string arg) { - this.codeBuilder.AppendLine($"[]"); + this.CodeBuilder.AppendLine($"[]"); } internal override void EndThisAssemblyClass() { - this.codeBuilder.AppendLine("do()"); + this.CodeBuilder.AppendLine("do()"); } internal override void StartThisAssemblyClass() { - this.codeBuilder.AppendLine("do()"); - this.codeBuilder.AppendLine($"#if {CompilerDefinesAroundGeneratedCodeAttribute}"); - this.codeBuilder.AppendLine($"[]"); - this.codeBuilder.AppendLine("#endif"); - this.codeBuilder.AppendLine($"#if {CompilerDefinesAroundExcludeFromCodeCoverageAttribute}"); - this.codeBuilder.AppendLine("[]"); - this.codeBuilder.AppendLine("#endif"); - this.codeBuilder.AppendLine("type internal ThisAssembly() ="); + this.CodeBuilder.AppendLine("do()"); + this.CodeBuilder.AppendLine($"#if {CompilerDefinesAroundGeneratedCodeAttribute}"); + this.CodeBuilder.AppendLine($"[]"); + this.CodeBuilder.AppendLine("#endif"); + this.CodeBuilder.AppendLine($"#if {CompilerDefinesAroundExcludeFromCodeCoverageAttribute}"); + this.CodeBuilder.AppendLine("[]"); + this.CodeBuilder.AppendLine("#endif"); + this.CodeBuilder.AppendLine("type internal ThisAssembly() ="); } } private class CSharpCodeGenerator : CodeGenerator { + public CSharpCodeGenerator(string ns) + : base(ns) + { + } + internal override void AddAnalysisSuppressions() { - this.codeBuilder.AppendLine($"#pragma warning disable {string.Join(", ", this.WarningCodesToSuppress)}"); + this.CodeBuilder.AppendLine($"#pragma warning disable {string.Join(", ", this.WarningCodesToSuppress)}"); } internal override void AddComment(string comment) @@ -662,46 +768,61 @@ internal override void AddComment(string comment) internal override void DeclareAttribute(Type type, string arg) { - this.codeBuilder.AppendLine($"[assembly: {type.FullName}(\"{arg}\")]"); + this.CodeBuilder.AppendLine($"[assembly: {type.FullName}(\"{arg}\")]"); } internal override void StartThisAssemblyClass() { - this.codeBuilder.AppendLine($"#if {CompilerDefinesAroundGeneratedCodeAttribute}"); - this.codeBuilder.AppendLine($"[System.CodeDom.Compiler.GeneratedCode(\"{GeneratorName}\",\"{GeneratorVersion}\")]"); - this.codeBuilder.AppendLine("#endif"); - this.codeBuilder.AppendLine($"#if {CompilerDefinesAroundExcludeFromCodeCoverageAttribute}"); - this.codeBuilder.AppendLine("[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]"); - this.codeBuilder.AppendLine("#endif"); - this.codeBuilder.AppendLine("internal static partial class ThisAssembly {"); + if (this.Namespace is { } ns) + { + this.CodeBuilder.AppendLine($"namespace {ns} {{"); + } + + this.CodeBuilder.AppendLine($"#if {CompilerDefinesAroundGeneratedCodeAttribute}"); + this.CodeBuilder.AppendLine($"[System.CodeDom.Compiler.GeneratedCode(\"{GeneratorName}\",\"{GeneratorVersion}\")]"); + this.CodeBuilder.AppendLine("#endif"); + this.CodeBuilder.AppendLine($"#if {CompilerDefinesAroundExcludeFromCodeCoverageAttribute}"); + this.CodeBuilder.AppendLine("[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]"); + this.CodeBuilder.AppendLine("#endif"); + this.CodeBuilder.AppendLine("internal static partial class ThisAssembly {"); } internal override void AddThisAssemblyMember(string name, string value) { - this.codeBuilder.AppendLine($" internal const string {name} = \"{value}\";"); + this.CodeBuilder.AppendLine($" internal const string {name} = \"{value}\";"); } internal override void AddThisAssemblyMember(string name, bool value) { - this.codeBuilder.AppendLine($" internal const bool {name} = {(value ? "true" : "false")};"); + this.CodeBuilder.AppendLine($" internal const bool {name} = {(value ? "true" : "false")};"); } internal override void AddThisAssemblyMember(string name, DateTime value) { - this.codeBuilder.AppendLine($" internal static readonly System.DateTime {name} = new System.DateTime({value.Ticks}L, System.DateTimeKind.Utc);"); + this.CodeBuilder.AppendLine($" internal static readonly System.DateTime {name} = new System.DateTime({value.Ticks}L, System.DateTimeKind.Utc);"); } internal override void EndThisAssemblyClass() { - this.codeBuilder.AppendLine("}"); + this.CodeBuilder.AppendLine("}"); + + if (this.Namespace is not null) + { + this.CodeBuilder.AppendLine("}"); + } } } private class VisualBasicCodeGenerator : CodeGenerator { + public VisualBasicCodeGenerator(string ns) + : base(ns) + { + } + internal override void AddAnalysisSuppressions() { - this.codeBuilder.AppendLine($"#Disable Warning {string.Join(", ", this.WarningCodesToSuppress)}"); + this.CodeBuilder.AppendLine($"#Disable Warning {string.Join(", ", this.WarningCodesToSuppress)}"); } internal override void AddComment(string comment) @@ -711,115 +832,51 @@ internal override void AddComment(string comment) internal override void DeclareAttribute(Type type, string arg) { - this.codeBuilder.AppendLine($""); + this.CodeBuilder.AppendLine($""); } internal override void StartThisAssemblyClass() { - this.codeBuilder.AppendLine($"#If {CompilerDefinesAroundExcludeFromCodeCoverageAttribute.Replace("||", " Or ")} Then"); - this.codeBuilder.AppendLine($""); - this.codeBuilder.AppendLine(""); - this.codeBuilder.AppendLine("Partial Friend NotInheritable Class ThisAssembly"); - this.codeBuilder.AppendLine($"#ElseIf {CompilerDefinesAroundGeneratedCodeAttribute.Replace("||", " Or ")} Then"); - this.codeBuilder.AppendLine($""); - this.codeBuilder.AppendLine("Partial Friend NotInheritable Class ThisAssembly"); - this.codeBuilder.AppendLine("#Else"); - this.codeBuilder.AppendLine("Partial Friend NotInheritable Class ThisAssembly"); - this.codeBuilder.AppendLine("#End If"); + if (this.Namespace is { } ns) + { + this.CodeBuilder.AppendLine($"Namespace {ns}"); + } + + this.CodeBuilder.AppendLine($"#If {CompilerDefinesAroundExcludeFromCodeCoverageAttribute.Replace("||", " Or ")} Then"); + this.CodeBuilder.AppendLine($""); + this.CodeBuilder.AppendLine(""); + this.CodeBuilder.AppendLine("Partial Friend NotInheritable Class ThisAssembly"); + this.CodeBuilder.AppendLine($"#ElseIf {CompilerDefinesAroundGeneratedCodeAttribute.Replace("||", " Or ")} Then"); + this.CodeBuilder.AppendLine($""); + this.CodeBuilder.AppendLine("Partial Friend NotInheritable Class ThisAssembly"); + this.CodeBuilder.AppendLine("#Else"); + this.CodeBuilder.AppendLine("Partial Friend NotInheritable Class ThisAssembly"); + this.CodeBuilder.AppendLine("#End If"); } internal override void AddThisAssemblyMember(string name, string value) { - this.codeBuilder.AppendLine($" Friend Const {name} As String = \"{value}\""); + this.CodeBuilder.AppendLine($" Friend Const {name} As String = \"{value}\""); } internal override void AddThisAssemblyMember(string name, bool value) { - this.codeBuilder.AppendLine($" Friend Const {name} As Boolean = {(value ? "True" : "False")}"); + this.CodeBuilder.AppendLine($" Friend Const {name} As Boolean = {(value ? "True" : "False")}"); } internal override void AddThisAssemblyMember(string name, DateTime value) { - this.codeBuilder.AppendLine($" Friend Shared ReadOnly {name} As System.DateTime = New System.DateTime({value.Ticks}L, System.DateTimeKind.Utc)"); + this.CodeBuilder.AppendLine($" Friend Shared ReadOnly {name} As System.DateTime = New System.DateTime({value.Ticks}L, System.DateTimeKind.Utc)"); } internal override void EndThisAssemblyClass() { - this.codeBuilder.AppendLine("End Class"); - } - } - - private static string ToHex(byte[] data) - { - return BitConverter.ToString(data).Replace("-", "").ToLowerInvariant(); - } + this.CodeBuilder.AppendLine("End Class"); - /// - /// Gets the public key from a key container. - /// - /// The name of the container. - /// The public key. - private static byte[] GetPublicKeyFromKeyContainer(string containerName) - { - throw new NotImplementedException(); - } - - private static byte[] GetPublicKeyFromKeyPair(byte[] keyPair) - { - byte[] publicKey; - if (CryptoBlobParser.TryGetPublicKeyFromPrivateKeyBlob(keyPair, out publicKey)) - { - return publicKey; - } - else - { - throw new ArgumentException("Invalid keypair"); - } - } - - private bool TryReadKeyInfo(out string publicKey, out string publicKeyToken) - { - try - { - byte[] publicKeyBytes = null; - if (!string.IsNullOrEmpty(this.AssemblyOriginatorKeyFile) && File.Exists(this.AssemblyOriginatorKeyFile)) - { - if (Path.GetExtension(this.AssemblyOriginatorKeyFile).Equals(".snk", StringComparison.OrdinalIgnoreCase)) - { - byte[] keyBytes = File.ReadAllBytes(this.AssemblyOriginatorKeyFile); - bool publicKeyOnly = keyBytes[0] != 0x07; - publicKeyBytes = publicKeyOnly ? keyBytes : GetPublicKeyFromKeyPair(keyBytes); - } - } - else if (!string.IsNullOrEmpty(this.AssemblyKeyContainerName)) - { - publicKeyBytes = GetPublicKeyFromKeyContainer(this.AssemblyKeyContainerName); - } - - if (publicKeyBytes is object && publicKeyBytes.Length > 0) // If .NET 2.0 isn't installed, we get byte[0] back. + if (this.Namespace is not null) { - publicKey = ToHex(publicKeyBytes); - publicKeyToken = ToHex(CryptoBlobParser.GetStrongNameTokenFromPublicKey(publicKeyBytes)); + this.CodeBuilder.AppendLine("End Namespace"); } - else - { - if (publicKeyBytes is object) - { - this.Log.LogWarning("Unable to emit public key fields in ThisAssembly class because .NET 2.0 isn't installed."); - } - - publicKey = null; - publicKeyToken = null; - return false; - } - - return true; - } - catch (NotImplementedException) - { - publicKey = null; - publicKeyToken = null; - return false; } } } diff --git a/src/Nerdbank.GitVersioning.Tasks/CompareFiles.cs b/src/Nerdbank.GitVersioning.Tasks/CompareFiles.cs index f32d5485..1e81b372 100644 --- a/src/Nerdbank.GitVersioning.Tasks/CompareFiles.cs +++ b/src/Nerdbank.GitVersioning.Tasks/CompareFiles.cs @@ -1,46 +1,82 @@ -namespace Nerdbank.GitVersioning.Tasks +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Nerdbank.GitVersioning.Tasks { - using System; - using System.Collections.Generic; - using System.IO; - using System.IO.MemoryMappedFiles; - using System.Linq; - using System.Text; - using Microsoft.Build.Framework; - using Microsoft.Build.Utilities; - - public class CompareFiles : Task + public class CompareFiles : Microsoft.Build.Utilities.Task { /// - /// One set of items to compare. + /// Gets or sets one set of items to compare. /// [Required] public ITaskItem[] OriginalItems { get; set; } /// - /// The other set of items to compare. + /// Gets or sets the other set of items to compare. /// [Required] public ITaskItem[] NewItems { get; set; } /// - /// Gets whether the items lists contain items that are identical going down the list. + /// Gets a value indicating whether the items lists contain items that are identical going down the list. /// [Output] public bool AreSame { get; private set; } /// - /// Same as , but opposite. + /// Gets a value indicating whether the item lists contain items that are not identical, going down the list. /// [Output] - public bool AreChanged { get { return !this.AreSame; } } + public bool AreChanged + { + get { return !this.AreSame; } + } + /// public override bool Execute() { this.AreSame = this.AreFilesIdentical(); return true; } + /// + /// Tests whether a file is up to date with respect to another, + /// based on existence, last write time and file size. + /// + /// The source path. + /// The dest path. + /// if the files are the same; if the files are different. + internal static bool FastFileEqualityCheck(string sourcePath, string destPath) + { + FileInfo sourceInfo = new FileInfo(sourcePath); + FileInfo destInfo = new FileInfo(destPath); + + if (sourceInfo.Exists ^ destInfo.Exists) + { + // Either the source file or the destination file is missing. + return false; + } + + if (!sourceInfo.Exists) + { + // Neither file exists. + return true; + } + + // We'll say the files are the same if their modification date and length are the same. + return + sourceInfo.LastWriteTimeUtc == destInfo.LastWriteTimeUtc && + sourceInfo.Length == destInfo.Length; + } + private bool AreFilesIdentical() { if (this.OriginalItems.Length != this.NewItems.Length) @@ -62,11 +98,21 @@ private bool AreFilesIdentical() private bool IsContentOfFilesTheSame(string file1, string file2) { // If exactly one file is missing, that's different. - if (File.Exists(file1) ^ File.Exists(file2)) return false; + if (File.Exists(file1) ^ File.Exists(file2)) + { + return false; + } + // If both are missing, that's the same. - if (!File.Exists(file1)) return true; + if (!File.Exists(file1)) + { + return true; + } - if (new FileInfo(file1).Length != new FileInfo(file2).Length) return false; + if (new FileInfo(file1).Length != new FileInfo(file2).Length) + { + return false; + } // If both are present, we need to do a content comparison. // Keep our comparison simple by loading both in memory. @@ -74,11 +120,17 @@ private bool IsContentOfFilesTheSame(string file1, string file2) byte[] file2Content = File.ReadAllBytes(file2); // One more sanity check. - if (file1Content.Length != file2Content.Length) return false; + if (file1Content.Length != file2Content.Length) + { + return false; + } for (int i = 0; i < file1Content.Length; i++) { - if (file1Content[i] != file2Content[i]) return false; + if (file1Content[i] != file2Content[i]) + { + return false; + } } return true; diff --git a/src/Nerdbank.GitVersioning.Tasks/ContextAwareTask.cs b/src/Nerdbank.GitVersioning.Tasks/ContextAwareTask.cs index 7ba03cb6..c49bc387 100644 --- a/src/Nerdbank.GitVersioning.Tasks/ContextAwareTask.cs +++ b/src/Nerdbank.GitVersioning.Tasks/ContextAwareTask.cs @@ -1,30 +1,34 @@ -namespace MSBuildExtensionTask -{ - using System; - using System.IO; - using System.Linq; - using System.Reflection; -#if NETCOREAPP - using System.Runtime.Loader; -#endif - using Microsoft.Build.Framework; - using Microsoft.Build.Utilities; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Linq; +using System.Reflection; #if NETCOREAPP - using Nerdbank.GitVersioning; +using System.Runtime.Loader; +using Nerdbank.GitVersioning; #endif +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Nerdbank.GitVersioning.LibGit2; - public abstract class ContextAwareTask : Task +namespace MSBuildExtensionTask +{ + public abstract class ContextAwareTask : Microsoft.Build.Utilities.Task { - protected virtual string ManagedDllDirectory => Path.GetDirectoryName(new Uri(this.GetType().GetTypeInfo().Assembly.CodeBase).LocalPath); + protected virtual string ManagedDllDirectory => Path.GetDirectoryName(this.GetType().GetTypeInfo().Assembly.Location); protected virtual string UnmanagedDllDirectory => null; + /// public override bool Execute() { + string taskAssemblyPath = this.GetType().GetTypeInfo().Assembly.Location; + string unmanagedBaseDirectory = Path.GetDirectoryName(Path.GetDirectoryName(taskAssemblyPath)); #if NETCOREAPP - string taskAssemblyPath = new Uri(this.GetType().GetTypeInfo().Assembly.CodeBase).LocalPath; - - Assembly inContextAssembly = GitLoaderContext.Instance.LoadFromAssemblyPath(taskAssemblyPath); + GitLoaderContext loaderContext = new(unmanagedBaseDirectory); + Assembly inContextAssembly = loaderContext.LoadFromAssemblyPath(taskAssemblyPath); Type innerTaskType = inContextAssembly.GetType(this.GetType().FullName); object innerTask = Activator.CreateInstance(innerTaskType); @@ -43,7 +47,7 @@ where outerProperty.SetMethod is not null && outerProperty.GetMethod is not null propertyPair.innerProperty.SetValue(innerTask, outerPropertyValue); } - var executeInnerMethod = innerTaskType.GetMethod(nameof(ExecuteInner), BindingFlags.Instance | BindingFlags.NonPublic); + MethodInfo executeInnerMethod = innerTaskType.GetMethod(nameof(this.ExecuteInner), BindingFlags.Instance | BindingFlags.NonPublic); bool result = (bool)executeInnerMethod.Invoke(innerTask, new object[0]); foreach (var propertyPair in outputPropertiesMap) @@ -53,18 +57,7 @@ where outerProperty.SetMethod is not null && outerProperty.GetMethod is not null return result; #else - // On .NET Framework (on Windows), we find native binaries by adding them to our PATH. - if (this.UnmanagedDllDirectory is not null) - { - string pathEnvVar = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; - string[] searchPaths = pathEnvVar.Split(Path.PathSeparator); - if (!searchPaths.Contains(this.UnmanagedDllDirectory, StringComparer.OrdinalIgnoreCase)) - { - pathEnvVar += Path.PathSeparator + this.UnmanagedDllDirectory; - Environment.SetEnvironmentVariable("PATH", pathEnvVar); - } - } - + LibGit2GitExtensions.LoadNativeBinary(unmanagedBaseDirectory); return this.ExecuteInner(); #endif } diff --git a/src/Nerdbank.GitVersioning.Tasks/CryptoBlobParser+BlobHeader.cs b/src/Nerdbank.GitVersioning.Tasks/CryptoBlobParser+BlobHeader.cs index 912a0c3b..5dccb210 100644 --- a/src/Nerdbank.GitVersioning.Tasks/CryptoBlobParser+BlobHeader.cs +++ b/src/Nerdbank.GitVersioning.Tasks/CryptoBlobParser+BlobHeader.cs @@ -1,11 +1,14 @@ -namespace Nerdbank.GitVersioning.Tasks -{ - using System.Runtime.InteropServices; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; +namespace Nerdbank.GitVersioning.Tasks +{ /// /// The struct. /// - partial class CryptoBlobParser + internal partial class CryptoBlobParser { [StructLayout(LayoutKind.Sequential)] private struct BlobHeader @@ -16,17 +19,17 @@ private struct BlobHeader public byte Type; /// - /// Blob format version + /// Blob format version. /// public byte Version; /// - /// Must be 0 + /// Must be 0. /// public ushort Reserved; /// - /// Algorithm ID. Must be one of ALG_ID specified in wincrypto.h + /// Algorithm ID. Must be one of ALG_ID specified in wincrypto.h. /// public uint AlgId; } diff --git a/src/Nerdbank.GitVersioning.Tasks/CryptoBlobParser+RsaPubKey.cs b/src/Nerdbank.GitVersioning.Tasks/CryptoBlobParser+RsaPubKey.cs index 30f2067b..62b1908d 100644 --- a/src/Nerdbank.GitVersioning.Tasks/CryptoBlobParser+RsaPubKey.cs +++ b/src/Nerdbank.GitVersioning.Tasks/CryptoBlobParser+RsaPubKey.cs @@ -1,17 +1,20 @@ -namespace Nerdbank.GitVersioning.Tasks -{ - using System.Runtime.InteropServices; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; - partial class CryptoBlobParser +namespace Nerdbank.GitVersioning.Tasks +{ + internal partial class CryptoBlobParser { /// - /// RSAPUBKEY struct from wincrypt.h + /// RSAPUBKEY struct from wincrypt.h. /// [StructLayout(LayoutKind.Sequential)] private struct RsaPubKey { /// - /// Indicates RSA1 or RSA2 + /// Indicates RSA1 or RSA2. /// public uint Magic; @@ -21,7 +24,7 @@ private struct RsaPubKey public uint BitLen; /// - /// The public exponent + /// The public exponent. /// public uint PubExp; } diff --git a/src/Nerdbank.GitVersioning.Tasks/CryptoBlobParser+SnPublicKeyBlob.cs b/src/Nerdbank.GitVersioning.Tasks/CryptoBlobParser+SnPublicKeyBlob.cs index 2d3884a5..0c01fdd3 100644 --- a/src/Nerdbank.GitVersioning.Tasks/CryptoBlobParser+SnPublicKeyBlob.cs +++ b/src/Nerdbank.GitVersioning.Tasks/CryptoBlobParser+SnPublicKeyBlob.cs @@ -1,39 +1,42 @@ -namespace Nerdbank.GitVersioning.Tasks -{ - using System.Runtime.InteropServices; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; +namespace Nerdbank.GitVersioning.Tasks +{ /// /// The struct. /// - partial class CryptoBlobParser + internal partial class CryptoBlobParser { /// /// The strong name public key blob binary format. /// - /// + /// [StructLayout(LayoutKind.Sequential, Pack = 1)] private unsafe struct SnPublicKeyBlob { /// - /// Signature algorithm ID + /// Signature algorithm ID. /// public uint SigAlgId; /// - /// Hash algorithm ID + /// Hash algorithm ID. /// public uint HashAlgId; /// - /// Size of public key data in bytes, not including the header + /// Size of public key data in bytes, not including the header. /// public uint PublicKeySize; /// - /// PublicKeySize bytes of public key data + /// PublicKeySize bytes of public key data. /// /// - /// Note: PublicKey is variable sized + /// Note: PublicKey is variable sized. /// public fixed byte PublicKey[1]; } diff --git a/src/Nerdbank.GitVersioning.Tasks/CryptoBlobParser.cs b/src/Nerdbank.GitVersioning.Tasks/CryptoBlobParser.cs index edac5036..b947c1e2 100644 --- a/src/Nerdbank.GitVersioning.Tasks/CryptoBlobParser.cs +++ b/src/Nerdbank.GitVersioning.Tasks/CryptoBlobParser.cs @@ -1,29 +1,31 @@ -// Liberally copied from (with slight modifications) https://github.com/dotnet/roslyn/blob/6181abfdf59da26da27f0dbedae2978df2f83768/src/Compilers/Core/Portable/StrongName/CryptoBlobParser.cs +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +// Liberally copied from (with slight modifications) https://github.com/dotnet/roslyn/blob/6181abfdf59da26da27f0dbedae2978df2f83768/src/Compilers/Core/Portable/StrongName/CryptoBlobParser.cs // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See https://github.com/dotnet/roslyn/blob/6181abfdf59da26da27f0dbedae2978df2f83768/License.txt for license information. +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; +using Validation; namespace Nerdbank.GitVersioning.Tasks { - using System; - using System.Collections.Generic; - using System.IO; - using System.Runtime.InteropServices; - using System.Security.Cryptography; - using System.Text; - using Validation; - internal static partial class CryptoBlobParser { /// /// The size of a public key token, in bytes. /// - private const int SN_SIZEOF_TOKEN = 8; + private const int PublicKeyTokenSize = 8; /// /// The length of a SHA1 hash, in bytes. /// - private const int SHA1_HASH_SIZE = 20; + private const int Sha1HashSize = 20; - private const UInt32 RSA1 = 0x31415352; + private const uint RSA1 = 0x31415352; // From wincrypt.h private const byte PublicKeyBlobId = 0x06; @@ -31,7 +33,7 @@ internal static partial class CryptoBlobParser // In wincrypt.h both public and private key blobs start with a // PUBLICKEYSTRUC and RSAPUBKEY and then start the key data - private unsafe static readonly int s_offsetToKeyData = sizeof(BlobHeader) + sizeof(RsaPubKey); + private static unsafe readonly int OffsetToKeyData = sizeof(BlobHeader) + sizeof(RsaPubKey); /// /// Derives the public key token from a full public key. @@ -39,27 +41,27 @@ internal static partial class CryptoBlobParser /// The public key. /// The public key token. /// - /// Heavily inspired by https://github.com/dotnet/coreclr/blob/2efbb9282c059eb9742ba5a59b8a1d52ac4dfa4c/src/strongname/api/strongname.cpp#L270 + /// Heavily inspired by this code. /// internal static unsafe byte[] GetStrongNameTokenFromPublicKey(byte[] publicKey) { Requires.NotNull(publicKey, nameof(publicKey)); - byte[] strongNameToken = new byte[SN_SIZEOF_TOKEN]; + byte[] strongNameToken = new byte[PublicKeyTokenSize]; fixed (byte* publicKeyPtr = publicKey) { - //var publicKeyBlob = (SnPublicKeyBlob*)publicKeyPtr; + ////var publicKeyBlob = (SnPublicKeyBlob*)publicKeyPtr; using (var sha1 = SHA1.Create()) { byte[] hash = sha1.ComputeHash(publicKey); - int hashLenMinusTokenSize = SHA1_HASH_SIZE - SN_SIZEOF_TOKEN; + int hashLenMinusTokenSize = Sha1HashSize - PublicKeyTokenSize; // Take the last few bytes of the hash value for our token. (These are the // low order bytes from a network byte order point of view). Reverse the // order of these bytes in the output buffer to get host byte order. - for (int i = 0; i < SN_SIZEOF_TOKEN; i++) + for (int i = 0; i < PublicKeyTokenSize; i++) { - strongNameToken[SN_SIZEOF_TOKEN - (i + 1)] = hash[i + hashLenMinusTokenSize]; + strongNameToken[PublicKeyTokenSize - (i + 1)] = hash[i + hashLenMinusTokenSize]; } } } @@ -67,70 +69,39 @@ internal static unsafe byte[] GetStrongNameTokenFromPublicKey(byte[] publicKey) return strongNameToken; } - private static byte[] CreateSnPublicKeyBlob(BlobHeader header, RsaPubKey rsa, byte[] pubKeyData) - { - var snPubKey = new SnPublicKeyBlob() - { - SigAlgId = AlgorithmId.RsaSign, - HashAlgId = AlgorithmId.Sha, - PublicKeySize = (UInt32)(s_offsetToKeyData + pubKeyData.Length) - }; - - using (var ms = new MemoryStream(160)) - using (var binaryWriter = new BinaryWriter(ms)) - { - binaryWriter.Write(snPubKey.SigAlgId); - binaryWriter.Write(snPubKey.HashAlgId); - binaryWriter.Write(snPubKey.PublicKeySize); - - binaryWriter.Write(header.Type); - binaryWriter.Write(header.Version); - binaryWriter.Write(header.Reserved); - binaryWriter.Write(header.AlgId); - - binaryWriter.Write(rsa.Magic); - binaryWriter.Write(rsa.BitLen); - binaryWriter.Write(rsa.PubExp); - - binaryWriter.Write(pubKeyData); - - return ms.ToArray(); - } - } - - internal unsafe static bool TryGetPublicKeyFromPrivateKeyBlob(byte[] blob, out byte[] publicKey) + internal static unsafe bool TryGetPublicKeyFromPrivateKeyBlob(byte[] blob, out byte[] publicKey) { fixed (byte* blobPtr = blob) { var header = (BlobHeader*)blobPtr; var rsa = (RsaPubKey*)(blobPtr + sizeof(BlobHeader)); - var version = header->Version; - var modulusBitLength = rsa->BitLen; - var exponent = rsa->PubExp; - var modulus = new byte[modulusBitLength >> 3]; + byte version = header->Version; + uint modulusBitLength = rsa->BitLen; + uint exponent = rsa->PubExp; + byte[] modulus = new byte[modulusBitLength >> 3]; - if (blob.Length - s_offsetToKeyData < modulus.Length) + if (blob.Length - OffsetToKeyData < modulus.Length) { publicKey = null; return false; } - Marshal.Copy((IntPtr)(blobPtr + s_offsetToKeyData), modulus, 0, modulus.Length); + Marshal.Copy((IntPtr)(blobPtr + OffsetToKeyData), modulus, 0, modulus.Length); var newHeader = new BlobHeader() { Type = PublicKeyBlobId, Version = version, Reserved = 0, - AlgId = AlgorithmId.RsaSign + AlgId = AlgorithmId.RsaSign, }; var newRsaKey = new RsaPubKey() { Magic = RSA1, // Public key BitLen = modulusBitLength, - PubExp = exponent + PubExp = exponent, }; publicKey = CreateSnPublicKeyBlob(newHeader, newRsaKey, modulus); @@ -138,27 +109,58 @@ internal unsafe static bool TryGetPublicKeyFromPrivateKeyBlob(byte[] blob, out b } } + private static byte[] CreateSnPublicKeyBlob(BlobHeader header, RsaPubKey rsa, byte[] pubKeyData) + { + var snPubKey = new SnPublicKeyBlob() + { + SigAlgId = AlgorithmId.RsaSign, + HashAlgId = AlgorithmId.Sha, + PublicKeySize = (uint)(OffsetToKeyData + pubKeyData.Length), + }; + + using (var ms = new MemoryStream(160)) + using (var binaryWriter = new BinaryWriter(ms)) + { + binaryWriter.Write(snPubKey.SigAlgId); + binaryWriter.Write(snPubKey.HashAlgId); + binaryWriter.Write(snPubKey.PublicKeySize); + + binaryWriter.Write(header.Type); + binaryWriter.Write(header.Version); + binaryWriter.Write(header.Reserved); + binaryWriter.Write(header.AlgId); + + binaryWriter.Write(rsa.Magic); + binaryWriter.Write(rsa.BitLen); + binaryWriter.Write(rsa.PubExp); + + binaryWriter.Write(pubKeyData); + + return ms.ToArray(); + } + } + private struct AlgorithmId { + public const int RsaSign = 0x00002400; + public const int Sha = 0x00008004; + // From wincrypt.h private const int AlgorithmClassOffset = 13; private const int AlgorithmClassMask = 0x7; private const int AlgorithmSubIdOffset = 0; private const int AlgorithmSubIdMask = 0x1ff; - private readonly uint _flags; - - public const int RsaSign = 0x00002400; - public const int Sha = 0x00008004; + private readonly uint flags; - public bool IsSet + public AlgorithmId(uint flags) { - get { return this._flags != 0; } + this.flags = flags; } - public AlgorithmId(uint flags) + public bool IsSet { - this._flags = flags; + get { return this.flags != 0; } } } } diff --git a/src/Nerdbank.GitVersioning.Tasks/GetBuildVersion.cs b/src/Nerdbank.GitVersioning.Tasks/GetBuildVersion.cs index 77b79342..f38aeb25 100644 --- a/src/Nerdbank.GitVersioning.Tasks/GetBuildVersion.cs +++ b/src/Nerdbank.GitVersioning.Tasks/GetBuildVersion.cs @@ -1,16 +1,19 @@ -namespace Nerdbank.GitVersioning.Tasks +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using MSBuildExtensionTask; +using Validation; + +namespace Nerdbank.GitVersioning.Tasks { - using System; - using System.Collections.Generic; - using System.Globalization; - using System.IO; - using System.Linq; - using System.Reflection; - using Microsoft.Build.Framework; - using Microsoft.Build.Utilities; - using MSBuildExtensionTask; - using Validation; - public class GetBuildVersion : ContextAwareTask { /// @@ -83,7 +86,7 @@ public GetBuildVersion() public ITaskItem[] OutputProperties { get; set; } /// - /// Gets or sets a value indicating whether the project is building + /// Gets a value indicating whether the project is building /// in PublicRelease mode. /// [Output] @@ -200,8 +203,10 @@ public GetBuildVersion() [Output] public ITaskItem[] CloudBuildVersionVars { get; private set; } + /// protected override string UnmanagedDllDirectory => LibGit2.LibGit2GitExtensions.FindLibGit2NativeBinaries(this.TargetsPath); + /// protected override bool ExecuteInner() { try @@ -209,28 +214,29 @@ protected override bool ExecuteInner() if (!string.IsNullOrEmpty(this.ProjectPathRelativeToGitRepoRoot)) { Requires.Argument(!Path.IsPathRooted(this.ProjectPathRelativeToGitRepoRoot), nameof(this.ProjectPathRelativeToGitRepoRoot), "Path must be relative."); - Requires.Argument(!( - this.ProjectPathRelativeToGitRepoRoot.Contains(".." + Path.DirectorySeparatorChar) || - this.ProjectPathRelativeToGitRepoRoot.Contains(".." + Path.AltDirectorySeparatorChar)), - nameof(this.ProjectPathRelativeToGitRepoRoot), - "Path must not use ..\\"); + bool containsDotDotSlash = this.ProjectPathRelativeToGitRepoRoot.Contains(".." + Path.DirectorySeparatorChar) || + this.ProjectPathRelativeToGitRepoRoot.Contains(".." + Path.AltDirectorySeparatorChar); + Requires.Argument(!containsDotDotSlash, nameof(this.ProjectPathRelativeToGitRepoRoot), "Path must not use ..\\"); } - bool useLibGit2 = false; + GitContext.Engine engine = GitContext.Engine.ReadOnly; if (!string.IsNullOrWhiteSpace(this.GitEngine)) { - useLibGit2 = - this.GitEngine == "Managed" ? false : - this.GitEngine == "LibGit2" ? true : - throw new ArgumentException("GitEngine property must be set to either \"Managed\" or \"LibGit2\" or left empty."); + engine = this.GitEngine switch + { + "Managed" => GitContext.Engine.ReadOnly, + "LibGit2" => GitContext.Engine.ReadWrite, + "Disabled" => GitContext.Engine.Disabled, + _ => throw new ArgumentException("GitEngine property must be set to either \"Disabled\", \"Managed\" or \"LibGit2\" or left empty."), + }; } - var cloudBuild = CloudBuild.Active; - var overrideBuildNumberOffset = (this.OverrideBuildNumberOffset == int.MaxValue) ? (int?)null : this.OverrideBuildNumberOffset; + ICloudBuild cloudBuild = CloudBuild.Active; + int? overrideBuildNumberOffset = (this.OverrideBuildNumberOffset == int.MaxValue) ? (int?)null : this.OverrideBuildNumberOffset; string projectDirectory = this.ProjectPathRelativeToGitRepoRoot is object && this.GitRepoRoot is object ? Path.Combine(this.GitRepoRoot, this.ProjectPathRelativeToGitRepoRoot) : this.ProjectDirectory; - using var context = GitContext.Create(projectDirectory, writable: useLibGit2); + using var context = GitContext.Create(projectDirectory, engine: engine); var oracle = new VersionOracle(context, cloudBuild, overrideBuildNumberOffset); if (!string.IsNullOrEmpty(this.DefaultPublicRelease)) { @@ -276,7 +282,7 @@ protected override bool ExecuteInner() if (oracle.CloudBuildAllVarsEnabled) { - var allVariables = oracle.CloudBuildAllVars + IEnumerable allVariables = oracle.CloudBuildAllVars .Select(item => new TaskItem(item.Key, new Dictionary { { "Value", item.Value } })); if (cloudBuildVersionVars is not null) diff --git a/src/Nerdbank.GitVersioning.Tasks/GitLoaderContext.cs b/src/Nerdbank.GitVersioning.Tasks/GitLoaderContext.cs index 609329e9..9ce0c39b 100644 --- a/src/Nerdbank.GitVersioning.Tasks/GitLoaderContext.cs +++ b/src/Nerdbank.GitVersioning.Tasks/GitLoaderContext.cs @@ -1,5 +1,10 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + // This code originally copied from https://github.com/dotnet/sourcelink/tree/c092238370e0437eb95722f28c79273244dc7f1a/src/Microsoft.Build.Tasks.Git // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See license information at https://github.com/dotnet/sourcelink/blob/c092238370e0437eb95722f28c79273244dc7f1a/License.txt. +#nullable enable + #if NETCOREAPP using System; @@ -7,23 +12,68 @@ using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.Loader; +using Nerdbank.GitVersioning.LibGit2; using RuntimeEnvironment = Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment; namespace Nerdbank.GitVersioning { public class GitLoaderContext : AssemblyLoadContext { - public static readonly GitLoaderContext Instance = new GitLoaderContext(); - public const string RuntimePath = "./runtimes"; + private readonly string nativeDependencyBasePath; + + private (string?, IntPtr) lastLoadedLibrary; + /// + /// Initializes a new instance of the class. + /// + /// The path to the directory that contains the "runtimes" folder. + public GitLoaderContext(string nativeDependencyBasePath) + { + this.nativeDependencyBasePath = nativeDependencyBasePath; + } + + /// protected override Assembly Load(AssemblyName assemblyName) { - var path = Path.Combine(Path.GetDirectoryName(typeof(GitLoaderContext).Assembly.Location), assemblyName.Name + ".dll"); + string path = Path.Combine(Path.GetDirectoryName(typeof(GitLoaderContext).Assembly.Location)!, assemblyName.Name + ".dll"); return File.Exists(path) ? this.LoadFromAssemblyPath(path) : Default.LoadFromAssemblyName(assemblyName); } + + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + IntPtr p = base.LoadUnmanagedDll(unmanagedDllName); + + if (p == IntPtr.Zero) + { + if (unmanagedDllName == this.lastLoadedLibrary.Item1) + { + return this.lastLoadedLibrary.Item2; + } + + string prefix = + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? string.Empty : + "lib"; + + string? extension = + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".dll" : + RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? ".so" : + RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? ".dylib" : + null; + + string fileName = $"{prefix}{unmanagedDllName}{extension}"; + string? directoryPath = LibGit2GitExtensions.FindLibGit2NativeBinaries(this.nativeDependencyBasePath); + if (directoryPath is not null && NativeLibrary.TryLoad(Path.Combine(directoryPath, fileName), out p)) + { + // Cache this to make us a little faster next time. + this.lastLoadedLibrary = (unmanagedDllName, p); + } + } + + return p; + } } } #endif diff --git a/src/Nerdbank.GitVersioning.Tasks/NativeVersionInfo.cs b/src/Nerdbank.GitVersioning.Tasks/NativeVersionInfo.cs index 8153a545..e6d9530f 100644 --- a/src/Nerdbank.GitVersioning.Tasks/NativeVersionInfo.cs +++ b/src/Nerdbank.GitVersioning.Tasks/NativeVersionInfo.cs @@ -1,17 +1,22 @@ -namespace Nerdbank.GitVersioning.Tasks +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Nerdbank.GitVersioning.Tasks { - using System; - using System.Collections.Generic; - using System.Globalization; - using System.IO; - using System.Text; - using Microsoft.Build.Framework; - using Microsoft.Build.Utilities; - - public class NativeVersionInfo : Task + public class NativeVersionInfo : Microsoft.Build.Utilities.Task { +#pragma warning disable SA1310 // Field names should not contain underscore private const int VFT_APP = 0x1; private const int VFT_DLL = 0x2; +#pragma warning restore SA1310 // Field names should not contain underscore private const string FileHeaderComment = @"------------------------------------------------------------------------------ @@ -103,6 +108,7 @@ BLOCK NBGV_VERSION_BLOCK public string TargetFileName { get; set; } + /// public override bool Execute() { this.generator = this.CreateGenerator(); @@ -130,9 +136,14 @@ public override bool Execute() return !this.Log.HasLoggedErrors; } + private static string DefaultIfEmpty(string value, string defaultValue) + { + return string.IsNullOrWhiteSpace(value) ? defaultValue : value; + } + private void CreateDefines() { - var fileType = 0; + int fileType = 0; switch (this.ConfigurationType.ToUpperInvariant()) { @@ -149,24 +160,24 @@ private void CreateDefines() return; } - if (!Version.TryParse(this.AssemblyFileVersion, out var fileVersion)) + if (!Version.TryParse(this.AssemblyFileVersion, out Version fileVersion)) { this.Log.LogError("Cannot process AssemblyFileVersion '{0}' into a valid four part version.", this.AssemblyFileVersion); return; } - if (!Version.TryParse(this.AssemblyVersion, out var productVersion)) + if (!Version.TryParse(this.AssemblyVersion, out Version productVersion)) { productVersion = fileVersion; } - var lcid = 0; + int lcid = 0; if (!string.IsNullOrWhiteSpace(this.AssemblyLanguage)) { if (!int.TryParse(this.AssemblyLanguage, out lcid)) { -#if NET461 +#if NET462 try { var cultureInfo = new CultureInfo(this.AssemblyLanguage); @@ -184,7 +195,7 @@ private void CreateDefines() } } - if (!int.TryParse(this.AssemblyCodepage, out var codepage)) + if (!int.TryParse(this.AssemblyCodepage, out int codepage)) { codepage = 0; } @@ -218,12 +229,12 @@ private void CreateDefines() { "NBGV_VERSION_BLOCK", (lcid << 16 | codepage).ToString("X8") }, }; - foreach (var pair in numericFields) + foreach (KeyValuePair pair in numericFields) { this.generator.AddDefine(pair.Key, pair.Value); } - foreach (var pair in stringFields) + foreach (KeyValuePair pair in stringFields) { if (!string.IsNullOrWhiteSpace(pair.Value)) { @@ -251,13 +262,13 @@ private CodeGenerator CreateGenerator() private class CodeGenerator { - protected readonly StringBuilder codeBuilder; - internal CodeGenerator() { - this.codeBuilder = new StringBuilder(); + this.CodeBuilder = new StringBuilder(); } + protected StringBuilder CodeBuilder { get; } + internal void AddComment(string comment) { this.AddCodeComment(comment, "//"); @@ -265,35 +276,35 @@ internal void AddComment(string comment) internal void StartFile() { - this.codeBuilder.AppendLine("#pragma once"); + this.CodeBuilder.AppendLine("#pragma once"); } internal void AddContent(string content) { - this.codeBuilder.AppendLine(content); + this.CodeBuilder.AppendLine(content); } internal void AddDefine(string name, int value) { - this.codeBuilder.AppendLine($"#define {name} {value}"); + this.CodeBuilder.AppendLine($"#define {name} {value}"); } internal void AddDefine(string name, string value) { - var escapedValue = value.Replace("\\", "\\\\"); + string escapedValue = value.Replace("\\", "\\\\"); - this.codeBuilder.AppendLine($"#define {name} NBGV_VERSION_STRING(\"{escapedValue}\")"); + this.CodeBuilder.AppendLine($"#define {name} NBGV_VERSION_STRING(\"{escapedValue}\")"); } internal void EndFile() { } - internal string GetCode() => this.codeBuilder.ToString(); + internal string GetCode() => this.CodeBuilder.ToString(); internal void AddBlankLine() { - this.codeBuilder.AppendLine(); + this.CodeBuilder.AppendLine(); } protected void AddCodeComment(string comment, string token) @@ -302,15 +313,10 @@ protected void AddCodeComment(string comment, string token) string line; while ((line = sr.ReadLine()) is not null) { - this.codeBuilder.Append(token); - this.codeBuilder.AppendLine(line); + this.CodeBuilder.Append(token); + this.CodeBuilder.AppendLine(line); } } } - - private static string DefaultIfEmpty(string value, string defaultValue) - { - return string.IsNullOrWhiteSpace(value) ? defaultValue : value; - } } } diff --git a/src/Nerdbank.GitVersioning.Tasks/Nerdbank.GitVersioning.Tasks.csproj b/src/Nerdbank.GitVersioning.Tasks/Nerdbank.GitVersioning.Tasks.csproj index da481512..da3e1629 100644 --- a/src/Nerdbank.GitVersioning.Tasks/Nerdbank.GitVersioning.Tasks.csproj +++ b/src/Nerdbank.GitVersioning.Tasks/Nerdbank.GitVersioning.Tasks.csproj @@ -1,6 +1,6 @@  - net461;netcoreapp3.1 + net462;net6.0 true Nerdbank.GitVersioning.nuspec @@ -16,40 +16,6 @@ true - - - $(NuGetPackageRoot)libgit2sharp.nativebinaries\$(LibGit2SharpNativeVersion)\ - $(NuspecProperties);Version=$(Version);commit=$(GitCommitId);BaseOutputPath=$(OutputPath);LibGit2SharpNativeBinaries=$(LibGit2SharpNativeBinaries) - $(NuspecProperties);LKGSuffix=.LKG - - - - - - MSBuildCore\ - MSBuildFull\ - - - - - build\$(BuildSubDir) - - - - build\$(BuildSubDir)%(ContentWithTargetPath.TargetPath) - - - - true @@ -75,19 +41,21 @@ - + - + - - + + - - - + + + + + diff --git a/src/Nerdbank.GitVersioning.Tasks/Nerdbank.GitVersioning.Tasks.targets b/src/Nerdbank.GitVersioning.Tasks/Nerdbank.GitVersioning.Tasks.targets new file mode 100644 index 00000000..f61154df --- /dev/null +++ b/src/Nerdbank.GitVersioning.Tasks/Nerdbank.GitVersioning.Tasks.targets @@ -0,0 +1,35 @@ + + + + $(NuGetPackageRoot)libgit2sharp.nativebinaries\$(LibGit2SharpNativeVersion)\ + $(NuspecProperties);Version=$(Version);commit=$(GitCommitId);BaseOutputPath=$(OutputPath);LibGit2SharpNativeBinaries=$(LibGit2SharpNativeBinaries) + $(NuspecProperties);LKGSuffix=.LKG + + + + + + MSBuildCore\ + MSBuildFull\ + + + + + build\$(BuildSubDir) + + + + build\$(BuildSubDir)%(ContentWithTargetPath.TargetPath) + + + + diff --git a/src/Nerdbank.GitVersioning.Tasks/Nerdbank.GitVersioning.nuspec b/src/Nerdbank.GitVersioning.Tasks/Nerdbank.GitVersioning.nuspec index 28b573e0..7cbd1117 100644 --- a/src/Nerdbank.GitVersioning.Tasks/Nerdbank.GitVersioning.nuspec +++ b/src/Nerdbank.GitVersioning.Tasks/Nerdbank.GitVersioning.nuspec @@ -17,18 +17,18 @@ IMPORTANT: The 3.x release may produce a different version height than prior maj - - - - - - - - - - - - + + + + + + + + + + + + @@ -36,18 +36,13 @@ IMPORTANT: The 3.x release may produce a different version height than prior maj - - - - - - - - - - - + + + + + + @@ -56,6 +51,7 @@ IMPORTANT: The 3.x release may produce a different version height than prior maj + diff --git a/src/Nerdbank.GitVersioning.Tasks/Properties/AssemblyInfo.cs b/src/Nerdbank.GitVersioning.Tasks/Properties/AssemblyInfo.cs deleted file mode 100644 index 6ca0b3d2..00000000 --- a/src/Nerdbank.GitVersioning.Tasks/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -[assembly: AssemblyCopyright("Copyright © 2015")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: ComVisible(false)] diff --git a/src/Nerdbank.GitVersioning.Tasks/SetCloudBuildVariables.cs b/src/Nerdbank.GitVersioning.Tasks/SetCloudBuildVariables.cs index b721152c..6480f605 100644 --- a/src/Nerdbank.GitVersioning.Tasks/SetCloudBuildVariables.cs +++ b/src/Nerdbank.GitVersioning.Tasks/SetCloudBuildVariables.cs @@ -1,14 +1,17 @@ -namespace Nerdbank.GitVersioning.Tasks -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Text; - using Microsoft.Build.Framework; - using Microsoft.Build.Utilities; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; - public class SetCloudBuildVariables : Task +namespace Nerdbank.GitVersioning.Tasks +{ + public class SetCloudBuildVariables : Microsoft.Build.Utilities.Task { public ITaskItem[] CloudBuildVersionVars { get; set; } @@ -17,9 +20,10 @@ public class SetCloudBuildVariables : Task public string CloudBuildNumber { get; set; } + /// public override bool Execute() { - var cloudBuild = CloudBuild.Active; + ICloudBuild cloudBuild = CloudBuild.Active; if (cloudBuild is not null) { var envVars = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -35,8 +39,8 @@ public override bool Execute() if (!string.IsNullOrWhiteSpace(this.CloudBuildNumber)) { - var newVars = cloudBuild.SetCloudBuildNumber(this.CloudBuildNumber, stdout, stderr); - foreach (var item in newVars) + IReadOnlyDictionary newVars = cloudBuild.SetCloudBuildNumber(this.CloudBuildNumber, stdout, stderr); + foreach (KeyValuePair item in newVars) { envVars[item.Key] = item.Value; } @@ -44,10 +48,10 @@ public override bool Execute() if (this.CloudBuildVersionVars is not null) { - foreach (var variable in this.CloudBuildVersionVars) + foreach (ITaskItem variable in this.CloudBuildVersionVars) { - var newVars = cloudBuild.SetCloudBuildVariable(variable.ItemSpec, variable.GetMetadata("Value"), stdout, stderr); - foreach (var item in newVars) + IReadOnlyDictionary newVars = cloudBuild.SetCloudBuildVariable(variable.ItemSpec, variable.GetMetadata("Value"), stdout, stderr); + foreach (KeyValuePair item in newVars) { envVars[item.Key] = item.Value; } @@ -58,7 +62,7 @@ public override bool Execute() let metadata = new Dictionary { { "Value", envVar.Value } } select new TaskItem(envVar.Key, metadata)).ToArray(); - foreach (var item in envVars) + foreach (KeyValuePair item in envVars) { Environment.SetEnvironmentVariable(item.Key, item.Value); } diff --git a/src/Nerdbank.GitVersioning.Tasks/build/MSBuildTargetCaching.targets b/src/Nerdbank.GitVersioning.Tasks/build/MSBuildTargetCaching.targets index 8f864fc5..2a9a7daa 100644 --- a/src/Nerdbank.GitVersioning.Tasks/build/MSBuildTargetCaching.targets +++ b/src/Nerdbank.GitVersioning.Tasks/build/MSBuildTargetCaching.targets @@ -7,6 +7,7 @@ $(NBGV_InnerGlobalProperties)GitVersionBaseDirectory=$(GitVersionBaseDirectory); $(NBGV_InnerGlobalProperties)OverrideBuildNumberOffset=$(OverrideBuildNumberOffset); $(NBGV_InnerGlobalProperties)NBGV_PrivateP2PAuxTargets=$(NBGV_PrivateP2PAuxTargets); + $(NBGV_InnerGlobalProperties)NBGV_GitEngine=$(NBGV_GitEngine); @@ -18,8 +19,7 @@ - - + GetBuildVersion_Properties;GetBuildVersion_CloudBuildVersionVars $(NBGV_InnerGlobalProperties)BuildMetadata=@(BuildMetadata, ','); Configuration=Release @@ -31,8 +31,14 @@ false true false - true all + + + + + true @@ -41,19 +47,17 @@ - - diff --git a/src/Nerdbank.GitVersioning.Tasks/build/Nerdbank.GitVersioning.targets b/src/Nerdbank.GitVersioning.Tasks/build/Nerdbank.GitVersioning.targets index a5c5fe58..f617e51a 100644 --- a/src/Nerdbank.GitVersioning.Tasks/build/Nerdbank.GitVersioning.targets +++ b/src/Nerdbank.GitVersioning.Tasks/build/Nerdbank.GitVersioning.targets @@ -40,6 +40,9 @@ false false + + false + GenerateNativeNBGVVersionInfo; @@ -112,11 +115,10 @@ - + $([MSBuild]::NormalizePath('$(IntermediateOutputPath)', '$(AssemblyName).Version$(DefaultLanguageSourceExtension)')) $(VersionSourceFile).new - UltimateResourceFallbackLocation.MainAssembly + + + + + + + + + <_NBGV_Major_Shifted>$([MSBuild]::Multiply($([System.Version]::Parse('$(BuildVersion)').Major), 16777216)) + <_NBGV_Minor_Shifted>$([MSBuild]::Multiply($([System.Version]::Parse('$(BuildVersion)').Minor), 65536)) + + $([MSBuild]::Add($([MSBuild]::Add($(_NBGV_Major_Shifted), $(_NBGV_Minor_Shifted))), $(BuildNumber))) + $(Version) + + + + + + + $(BuildVersionSimple) + + $(BuildVersionSimple) + + + + + + + + + $(BuildVersion) + + + + diff --git a/src/Shared/SemanticVersionExtensions.cs b/src/Shared/SemanticVersionExtensions.cs index 4f58a58b..95b87467 100644 --- a/src/Shared/SemanticVersionExtensions.cs +++ b/src/Shared/SemanticVersionExtensions.cs @@ -1,144 +1,151 @@ -namespace Nerdbank.GitVersioning +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Globalization; +using System.Text.RegularExpressions; +using Validation; + +namespace Nerdbank.GitVersioning; + +/// +/// Extension methods for . +/// +internal static class SemanticVersionExtensions { - using System; - using System.Globalization; - using System.Text.RegularExpressions; - using Validation; + /// + /// A regex that matches on numeric identifiers for prerelease or build metadata. + /// + private static readonly Regex NumericIdentifierRegex = new Regex(@"(? - /// Extension methods for + /// Gets a new semantic with the specified version component (major/minor) incremented. /// - internal static class SemanticVersionExtensions + /// The version to increment. + /// Specifies whether to increment the major or minor version. + /// Returns a new object with either the major or minor version incremented by 1. + internal static SemanticVersion Increment(this SemanticVersion currentVersion, VersionOptions.ReleaseVersionIncrement increment) { - /// - /// A regex that matches on numeric identifiers for prerelease or build metadata. - /// - private static readonly Regex NumericIdentifierRegex = new Regex(@"(? - /// Gets a new semantic with the specified version component (major/minor) incremented. - /// - /// The version to increment. - /// Specifies whether to increment the major or minor version. - /// Returns a new object with either the major or minor version incremented by 1. - internal static SemanticVersion Increment(this SemanticVersion currentVersion, VersionOptions.ReleaseVersionIncrement increment) + Requires.NotNull(currentVersion, nameof(currentVersion)); + Requires.Argument( + increment != VersionOptions.ReleaseVersionIncrement.Build || currentVersion.Version.Build >= 0, + nameof(increment), + "Cannot apply version increment '{0}' to version '{1}'", + increment, + currentVersion); + + int major = currentVersion.Version.Major; + int minor = currentVersion.Version.Minor; + int build = currentVersion.Version.Build; + + switch (increment) { - Requires.NotNull(currentVersion, nameof(currentVersion)); - Requires.Argument(increment != VersionOptions.ReleaseVersionIncrement.Build || currentVersion.Version.Build >= 0, nameof(increment), - "Cannot apply version increment '{0}' to version '{1}'", increment, currentVersion); - - var major = currentVersion.Version.Major; - var minor = currentVersion.Version.Minor; - var build = currentVersion.Version.Build; - - switch (increment) - { - case VersionOptions.ReleaseVersionIncrement.Major: - major += 1; - minor = 0; - build = 0; - break; - case VersionOptions.ReleaseVersionIncrement.Minor: - minor += 1; - build = 0; - break; - - case VersionOptions.ReleaseVersionIncrement.Build: - build += 1; - break; - - default: - throw new ArgumentOutOfRangeException(nameof(increment)); - } - - // use the appropriate constructor for the new version object - // depending on whether the current versions has 2, 3 or 4 segments - Version newVersion; - if (currentVersion.Version.Build >= 0 && currentVersion.Version.Revision > 0) - { - // 4 segment version - newVersion = new Version(major, minor, build, 0); - } - else if (currentVersion.Version.Build >= 0) - { - // 3 segment version - newVersion = new Version(major, minor, build); - } - else - { - // 2 segment version - newVersion = new Version(major, minor); - } - - return new SemanticVersion(newVersion, currentVersion.Prerelease, currentVersion.BuildMetadata); + case VersionOptions.ReleaseVersionIncrement.Major: + major += 1; + minor = 0; + build = 0; + break; + case VersionOptions.ReleaseVersionIncrement.Minor: + minor += 1; + build = 0; + break; + + case VersionOptions.ReleaseVersionIncrement.Build: + build += 1; + break; + + default: + throw new ArgumentOutOfRangeException(nameof(increment)); } - /// - /// Sets the first prerelease tag of the specified semantic version to the specified value. - /// - /// The version which's prerelease tag to modify. - /// The new prerelease tag. The leading hyphen may be specified or omitted. - /// Returns a new instance of with the updated prerelease tag - internal static SemanticVersion SetFirstPrereleaseTag(this SemanticVersion version, string newFirstTag) + // use the appropriate constructor for the new version object + // depending on whether the current versions has 2, 3 or 4 segments + Version newVersion; + if (currentVersion.Version.Build >= 0 && currentVersion.Version.Revision > 0) + { + // 4 segment version + newVersion = new Version(major, minor, build, 0); + } + else if (currentVersion.Version.Build >= 0) + { + // 3 segment version + newVersion = new Version(major, minor, build); + } + else { - Requires.NotNull(version, nameof(version)); - - newFirstTag = newFirstTag ?? ""; - - string preRelease; - if (string.IsNullOrEmpty(version.Prerelease)) - { - preRelease = newFirstTag; - } - else if (version.Prerelease.Contains(".")) - { - preRelease = newFirstTag + version.Prerelease.Substring(version.Prerelease.IndexOf(".")); - } - else - { - preRelease = newFirstTag; - } - - if (!string.IsNullOrEmpty(preRelease) && !preRelease.StartsWith("-")) - preRelease = "-" + preRelease; - - return new SemanticVersion(version.Version, preRelease, version.BuildMetadata); + // 2 segment version + newVersion = new Version(major, minor); } - /// - /// Removes all prerelease tags from the semantic version. - /// - /// The version to remove the prerelease tags from. - /// Returns a new instance which does not contain any prerelease tags. - internal static SemanticVersion WithoutPrepreleaseTags(this SemanticVersion version) + return new SemanticVersion(newVersion, currentVersion.Prerelease, currentVersion.BuildMetadata); + } + + /// + /// Sets the first prerelease tag of the specified semantic version to the specified value. + /// + /// The version which's prerelease tag to modify. + /// The new prerelease tag. The leading hyphen may be specified or omitted. + /// Returns a new instance of with the updated prerelease tag. + internal static SemanticVersion SetFirstPrereleaseTag(this SemanticVersion version, string newFirstTag) + { + Requires.NotNull(version, nameof(version)); + + newFirstTag = newFirstTag ?? string.Empty; + + string preRelease; + if (string.IsNullOrEmpty(version.Prerelease)) + { + preRelease = newFirstTag; + } + else if (version.Prerelease.Contains(".")) { - return new SemanticVersion(version.Version, null, version.BuildMetadata); + preRelease = newFirstTag + version.Prerelease.Substring(version.Prerelease.IndexOf(".")); + } + else + { + preRelease = newFirstTag; } - /// - /// Converts a semver 2 compliant "-beta.5" prerelease tag to a semver 1 compatible one. - /// - /// The semver 2 prerelease tag, including its leading hyphen. - /// The minimum number of digits to use for any numeric identifier. - /// A semver 1 compliant prerelease tag. For example "-beta-0005". - internal static string MakePrereleaseSemVer1Compliant(string prerelease, int paddingSize) + if (!string.IsNullOrEmpty(preRelease) && !preRelease.StartsWith("-")) { - if (string.IsNullOrEmpty(prerelease)) - { - return prerelease; - } + preRelease = "-" + preRelease; + } - string paddingFormatter = "{0:" + new string('0', paddingSize) + "}"; + return new SemanticVersion(version.Version, preRelease, version.BuildMetadata); + } - string semver1 = prerelease; + /// + /// Removes all prerelease tags from the semantic version. + /// + /// The version to remove the prerelease tags from. + /// Returns a new instance which does not contain any prerelease tags. + internal static SemanticVersion WithoutPrepreleaseTags(this SemanticVersion version) + { + return new SemanticVersion(version.Version, null, version.BuildMetadata); + } - // Identify numeric identifiers and pad them. - Assumes.True(prerelease.StartsWith("-")); - semver1 = "-" + NumericIdentifierRegex.Replace(semver1.Substring(1), m => string.Format(CultureInfo.InvariantCulture, paddingFormatter, int.Parse(m.Groups[1].Value))); + /// + /// Converts a semver 2 compliant "-beta.5" prerelease tag to a semver 1 compatible one. + /// + /// The semver 2 prerelease tag, including its leading hyphen. + /// The minimum number of digits to use for any numeric identifier. + /// A semver 1 compliant prerelease tag. For example "-beta-0005". + internal static string MakePrereleaseSemVer1Compliant(string prerelease, int paddingSize) + { + if (string.IsNullOrEmpty(prerelease)) + { + return prerelease; + } - semver1 = semver1.Replace('.', '-'); + string paddingFormatter = "{0:" + new string('0', paddingSize) + "}"; - return semver1; - } + string semver1 = prerelease; + + // Identify numeric identifiers and pad them. + Assumes.True(prerelease.StartsWith("-")); + semver1 = "-" + NumericIdentifierRegex.Replace(semver1.Substring(1), m => string.Format(CultureInfo.InvariantCulture, paddingFormatter, int.Parse(m.Groups[1].Value))); + + semver1 = semver1.Replace('.', '-'); + + return semver1; } } diff --git a/src/Shared/Utilities.cs b/src/Shared/Utilities.cs index c20f34a0..8207acdf 100644 --- a/src/Shared/Utilities.cs +++ b/src/Shared/Utilities.cs @@ -1,31 +1,29 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + using Validation; -namespace Nerdbank.GitVersioning +namespace Nerdbank.GitVersioning; + +internal static class Utilities { - internal static class Utilities + private const int ProcessCannotAccessFileHR = unchecked((int)0x80070020); + + internal static void FileOperationWithRetry(Action operation) { - private const int ProcessCannotAccessFileHR = unchecked((int)0x80070020); + Requires.NotNull(operation, nameof(operation)); - internal static void FileOperationWithRetry(Action operation) + for (int retriesLeft = 6; retriesLeft > 0; retriesLeft--) { - Requires.NotNull(operation, nameof(operation)); - - for (int retriesLeft = 6; retriesLeft > 0; retriesLeft--) + try + { + operation(); + break; + } + catch (IOException ex) when (ex.HResult == ProcessCannotAccessFileHR && retriesLeft > 0) { - try - { - operation(); - break; - } - catch (IOException ex) when (ex.HResult == ProcessCannotAccessFileHR && retriesLeft > 0) - { - Task.Delay(100).Wait(); - continue; - } + Task.Delay(100).Wait(); + continue; } } } diff --git a/src/nbgv/Program.cs b/src/nbgv/Program.cs index 43da7fa8..960e2062 100644 --- a/src/nbgv/Program.cs +++ b/src/nbgv/Program.cs @@ -1,28 +1,31 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Builder; +using System.CommandLine.Invocation; +using System.CommandLine.Parsing; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Build.Construction; +using Nerdbank.GitVersioning.Commands; +using Nerdbank.GitVersioning.LibGit2; +using Newtonsoft.Json; +using NuGet.Common; +using NuGet.Configuration; +using NuGet.PackageManagement; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using NuGet.Resolver; +using Validation; + namespace Nerdbank.GitVersioning.Tool { - using System; - using System.Collections.Generic; - using System.CommandLine; - using System.CommandLine.Builder; - using System.CommandLine.Invocation; - using System.CommandLine.Parsing; - using System.IO; - using System.Linq; - using System.Reflection; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.Build.Construction; - using Nerdbank.GitVersioning.Commands; - using Nerdbank.GitVersioning.LibGit2; - using Newtonsoft.Json; - using NuGet.Common; - using NuGet.Configuration; - using NuGet.PackageManagement; - using NuGet.Protocol; - using NuGet.Protocol.Core.Types; - using NuGet.Resolver; - using Validation; - internal class Program { private const string DefaultVersionSpec = "1.0-beta"; @@ -35,6 +38,9 @@ internal class Program private const BindingFlags CaseInsensitiveFlags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.IgnoreCase; + private static readonly string[] SupportedFormats = new[] { "text", "json" }; + private static ExitCodes exitCode; + private enum ExitCodes { OK, @@ -62,129 +68,170 @@ private enum ExitCodes PackageIdNotFound, ShallowClone, InternalError, + InvalidTagNameSetting, } - private static readonly string[] SupportedFormats = new[] { "text", "json" }; - private static ExitCodes exitCode; - private static bool AlwaysUseLibGit2 => string.Equals(Environment.GetEnvironmentVariable("NBGV_GitEngine"), "LibGit2", StringComparison.Ordinal); + private static string[] CloudProviderNames => CloudBuild.SupportedCloudBuilds.Select(cb => cb.GetType().Name).ToArray(); + public static int Main(string[] args) { - string thisAssemblyPath = new Uri(typeof(Program).GetTypeInfo().Assembly.CodeBase).LocalPath; + string thisAssemblyPath = typeof(Program).GetTypeInfo().Assembly.Location; - Assembly inContextAssembly = GitLoaderContext.Instance.LoadFromAssemblyPath(thisAssemblyPath); + GitLoaderContext loaderContext = new(Path.GetDirectoryName(thisAssemblyPath)); + Assembly inContextAssembly = loaderContext.LoadFromAssemblyPath(thisAssemblyPath); Type innerProgramType = inContextAssembly.GetType(typeof(Program).FullName); object innerProgram = Activator.CreateInstance(innerProgramType); - var mainInnerMethod = innerProgramType.GetMethod(nameof(MainInner), BindingFlags.Static | BindingFlags.NonPublic); + MethodInfo mainInnerMethod = innerProgramType.GetMethod(nameof(MainInner), BindingFlags.Static | BindingFlags.NonPublic); int result = (int)mainInnerMethod.Invoke(null, new object[] { args }); return result; } private static Parser BuildCommandLine() { - var install = new Command("install", "Prepares a project to have version stamps applied using Nerdbank.GitVersioning.") +#pragma warning disable IDE0008 + Command install; { - new Option(new[] { "--path", "-p" }, "The path to the directory that should contain the version.json file. The default is the root of the git repo.").LegalFilePathsOnly(), - new Option(new[] { "--version", "-v" }, () => DefaultVersionSpec, $"The initial version to set."), - new Option(new[] { "--source", "-s" }, $"The URI(s) of the NuGet package source(s) used to determine the latest stable version of the {PackageId} package. This setting overrides all of the sources specified in the NuGet.Config files.") + var path = new Option(new[] { "--path", "-p" }, "The path to the directory that should contain the version.json file. The default is the root of the git repo.").LegalFilePathsOnly(); + var version = new Option(new[] { "--version", "-v" }, () => DefaultVersionSpec, $"The initial version to set."); + var source = new Option(new[] { "--source", "-s" }, () => Array.Empty(), $"The URI(s) of the NuGet package source(s) used to determine the latest stable version of the {PackageId} package. This setting overrides all of the sources specified in the NuGet.Config files.") { - Argument = new Argument(() => Array.Empty()) - { - Arity = ArgumentArity.OneOrMore, - }, - }, - }; - - install.Handler = CommandHandler.Create>(OnInstallCommand); + Arity = ArgumentArity.OneOrMore, + AllowMultipleArgumentsPerToken = true, + }; + install = new Command("install", "Prepares a project to have version stamps applied using Nerdbank.GitVersioning.") + { + path, + version, + source, + }; + install.SetHandler(OnInstallCommand, path, version, source); + } - var getVersion = new Command("get-version", "Gets the version information for a project.") + Command getVersion; { - new Option(new[] { "--project", "-p" }, "The path to the project or project directory. The default is the current directory.").LegalFilePathsOnly(), - new Option("--metadata", "Adds an identifier to the build metadata part of a semantic version.") + var project = new Option(new[] { "--project", "-p" }, "The path to the project or project directory. The default is the current directory.").LegalFilePathsOnly(); + var metadata = new Option("--metadata", () => Array.Empty(), "Adds an identifier to the build metadata part of a semantic version.") { - Argument = new Argument(() => Array.Empty()) - { - Arity = ArgumentArity.OneOrMore, - }, - }, - new Option(new[] { "--format", "-f" }, $"The format to write the version information. Allowed values are: {string.Join(", ", SupportedFormats)}. The default is {DefaultOutputFormat}.").FromAmong(SupportedFormats), - new Option(new[] { "--variable", "-v" }, "The name of just one version property to print to stdout. When specified, the output is always in raw text. Useful in scripts."), - new Argument("commit-ish", () => DefaultRef, $"The commit/ref to get the version information for.") + Arity = ArgumentArity.OneOrMore, + AllowMultipleArgumentsPerToken = true, + }; + var format = new Option(new[] { "--format", "-f" }, $"The format to write the version information. Allowed values are: {string.Join(", ", SupportedFormats)}. The default is {DefaultOutputFormat}.").FromAmong(SupportedFormats); + var variable = new Option(new[] { "--variable", "-v" }, "The name of just one version property to print to stdout. When specified, the output is always in raw text. Useful in scripts."); + var commit = new Argument("commit-ish", () => DefaultRef, $"The commit/ref to get the version information for.") { Arity = ArgumentArity.ZeroOrOne, - }, - }; + }; + getVersion = new Command("get-version", "Gets the version information for a project.") + { + project, + metadata, + format, + variable, + commit, + }; - getVersion.Handler = CommandHandler.Create, string, string, string>(OnGetVersionCommand); + getVersion.SetHandler(OnGetVersionCommand, project, metadata, format, variable, commit); + } - var setVersion = new Command("set-version", "Updates the version stamp that is applied to a project.") + Command setVersion; { - new Option(new[] { "--project", "-p" }, "The path to the project or project directory. The default is the root directory of the repo that spans the current directory, or an existing version.json file, if applicable.").LegalFilePathsOnly(), - new Argument("version", "The version to set."), - }; + var project = new Option(new[] { "--project", "-p" }, "The path to the project or project directory. The default is the root directory of the repo that spans the current directory, or an existing version.json file, if applicable.").LegalFilePathsOnly(); + var version = new Argument("version", "The version to set."); + setVersion = new Command("set-version", "Updates the version stamp that is applied to a project.") + { + project, + version, + }; - setVersion.Handler = CommandHandler.Create(OnSetVersionCommand); + setVersion.SetHandler(OnSetVersionCommand, project, version); + } - var tag = new Command("tag", "Creates a git tag to mark a version.") + Command tag; { - new Option(new[] { "--project", "-p" }, "The path to the project or project directory. The default is the root directory of the repo that spans the current directory, or an existing version.json file, if applicable.").LegalFilePathsOnly(), - new Argument("versionOrRef", () => DefaultRef, $"The a.b.c[.d] version or git ref to be tagged.") + var project = new Option(new[] { "--project", "-p" }, "The path to the project or project directory. The default is the root directory of the repo that spans the current directory, or an existing version.json file, if applicable.").LegalFilePathsOnly(); + var versionOrRef = new Argument("versionOrRef", () => DefaultRef, $"The a.b.c[.d] version or git ref to be tagged.") { Arity = ArgumentArity.ZeroOrOne, - }, - }; + }; + tag = new Command("tag", "Creates a git tag to mark a version.") + { + project, + versionOrRef, + }; - tag.Handler = CommandHandler.Create(OnTagCommand); + tag.SetHandler(OnTagCommand, project, versionOrRef); + } - var getCommits = new Command("get-commits", "Gets the commit(s) that match a given version.") + Command getCommits; { - new Option(new[] { "--project", "-p" }, "The path to the project or project directory. The default is the root directory of the repo that spans the current directory, or an existing version.json file, if applicable.").LegalFilePathsOnly(), - new Option(new[] { "--quiet", "-q" }, "Use minimal output."), - new Argument("version", "The a.b.c[.d] version to find."), - }; + var project = new Option(new[] { "--project", "-p" }, "The path to the project or project directory. The default is the root directory of the repo that spans the current directory, or an existing version.json file, if applicable.").LegalFilePathsOnly(); + var quiet = new Option(new[] { "--quiet", "-q" }, "Use minimal output."); + var version = new Argument("version", "The a.b.c[.d] version to find."); + getCommits = new Command("get-commits", "Gets the commit(s) that match a given version.") + { + project, + quiet, + version, + }; - getCommits.Handler = CommandHandler.Create(OnGetCommitsCommand); + getCommits.SetHandler(OnGetCommitsCommand, project, quiet, version); + } - var cloud = new Command("cloud", "Communicates with the ambient cloud build to set the build number and/or other cloud build variables.") + Command cloud; { - new Option(new[] { "--project", "-p" }, "The path to the project or project directory used to calculate the version. The default is the current directory. Ignored if the -v option is specified.").LegalFilePathsOnly(), - new Option("--metadata", "Adds an identifier to the build metadata part of a semantic version.") + var project = new Option(new[] { "--project", "-p" }, "The path to the project or project directory used to calculate the version. The default is the current directory. Ignored if the -v option is specified.").LegalFilePathsOnly(); + var metadata = new Option("--metadata", () => Array.Empty(), "Adds an identifier to the build metadata part of a semantic version.") { - Argument = new Argument(() => Array.Empty()) - { - Arity = ArgumentArity.OneOrMore, - }, - }, - new Option(new[] { "--version", "-v" }, "The string to use for the cloud build number. If not specified, the computed version will be used."), - new Option(new[] { "--ci-system", "-s" }, "Force activation for a particular CI system. If not specified, auto-detection will be used. Supported values are: " + string.Join(", ", CloudProviderNames)).FromAmong(CloudProviderNames), - new Option(new[] { "--all-vars", "-a" }, "Defines ALL version variables as cloud build variables, with a \"NBGV_\" prefix."), - new Option(new[] { "--common-vars", "-c" }, "Defines a few common version variables as cloud build variables, with a \"Git\" prefix (e.g. GitBuildVersion, GitBuildVersionSimple, GitAssemblyInformationalVersion)."), - new Option(new[] { "--define", "-d" }, "Additional cloud build variables to define. Each should be in the NAME=VALUE syntax.") + Arity = ArgumentArity.OneOrMore, + AllowMultipleArgumentsPerToken = true, + }; + var version = new Option(new[] { "--version", "-v" }, "The string to use for the cloud build number. If not specified, the computed version will be used."); + var ciSystem = new Option(new[] { "--ci-system", "-s" }, "Force activation for a particular CI system. If not specified, auto-detection will be used. Supported values are: " + string.Join(", ", CloudProviderNames)).FromAmong(CloudProviderNames); + var allVars = new Option(new[] { "--all-vars", "-a" }, "Defines ALL version variables as cloud build variables, with a \"NBGV_\" prefix."); + var commonVars = new Option(new[] { "--common-vars", "-c" }, "Defines a few common version variables as cloud build variables, with a \"Git\" prefix (e.g. GitBuildVersion, GitBuildVersionSimple, GitAssemblyInformationalVersion)."); + var define = new Option(new[] { "--define", "-d" }, () => Array.Empty(), "Additional cloud build variables to define. Each should be in the NAME=VALUE syntax.") { - Argument = new Argument(() => Array.Empty()) - { - Arity = ArgumentArity.OneOrMore, - }, - }, - }; + Arity = ArgumentArity.OneOrMore, + AllowMultipleArgumentsPerToken = true, + }; + cloud = new Command("cloud", "Communicates with the ambient cloud build to set the build number and/or other cloud build variables.") + { + project, + metadata, + version, + ciSystem, + allVars, + commonVars, + define, + }; - cloud.Handler = CommandHandler.Create, string, string, bool, bool, IReadOnlyList>(OnCloudCommand); + cloud.SetHandler(OnCloudCommand, project, metadata, version, ciSystem, allVars, commonVars, define); + } - var prepareRelease = new Command("prepare-release", "Prepares a release by creating a release branch for the current version and adjusting the version on the current branch.") + Command prepareRelease; { - new Option(new[] { "--project", "-p" }, "The path to the project or project directory. The default is the current directory.").LegalFilePathsOnly(), - new Option("--nextVersion", "The version to set for the current branch. If omitted, the next version is determined automatically by incrementing the current version."), - new Option("--versionIncrement", "Overrides the 'versionIncrement' setting set in version.json for determining the next version of the current branch."), - new Option(new[] { "--format", "-f" }, $"The format to write information about the release. Allowed values are: {string.Join(", ", SupportedFormats)}. The default is {DefaultOutputFormat}.").FromAmong(SupportedFormats), - new Argument("tag", "The prerelease tag to apply on the release branch (if any). If not specified, any existing prerelease tag will be removed. The preceding hyphen may be omitted.") + var project = new Option(new[] { "--project", "-p" }, "The path to the project or project directory. The default is the current directory.").LegalFilePathsOnly(); + var nextVersion = new Option("--nextVersion", "The version to set for the current branch. If omitted, the next version is determined automatically by incrementing the current version."); + var versionIncrement = new Option("--versionIncrement", "Overrides the 'versionIncrement' setting set in version.json for determining the next version of the current branch."); + var format = new Option(new[] { "--format", "-f" }, $"The format to write information about the release. Allowed values are: {string.Join(", ", SupportedFormats)}. The default is {DefaultOutputFormat}.").FromAmong(SupportedFormats); + var tagArgument = new Argument("tag", "The prerelease tag to apply on the release branch (if any). If not specified, any existing prerelease tag will be removed. The preceding hyphen may be omitted.") { Arity = ArgumentArity.ZeroOrOne, - }, - }; + }; + prepareRelease = new Command("prepare-release", "Prepares a release by creating a release branch for the current version and adjusting the version on the current branch.") + { + project, + nextVersion, + versionIncrement, + format, + tagArgument, + }; - prepareRelease.Handler = CommandHandler.Create(OnPrepareReleaseCommand); + prepareRelease.SetHandler(OnPrepareReleaseCommand, project, nextVersion, versionIncrement, format, tagArgument); + } var root = new RootCommand($"{ThisAssembly.AssemblyTitle} v{ThisAssembly.AssemblyInformationalVersion}") { @@ -199,25 +246,9 @@ private static Parser BuildCommandLine() return new CommandLineBuilder(root) .UseDefaults() - .UseMiddleware(context => - { - // System.CommandLine 0.1 parsed arguments after optional --. Restore that behavior for compatibility. - // TODO: Remove this middleware when https://github.com/dotnet/command-line-api/issues/1238 is resolved. - if (context.ParseResult.UnparsedTokens.Count > 0) - { - var arguments = context.ParseResult.CommandResult.Command.Arguments; - if (arguments.Count() == context.ParseResult.UnparsedTokens.Count) - { - context.ParseResult = context.Parser.Parse( - context.ParseResult.Tokens - .Where(token => token.Type != TokenType.EndOfArguments) - .Select(token => token.Value) - .ToArray()); - } - } - }, (MiddlewareOrder)(-3000)) // MiddlewareOrderInternal.ExceptionHandler so [parse] directive is accurate. .UseExceptionHandler((ex, context) => PrintException(ex, context)) .Build(); +#pragma warning restore IDE0008 } private static void PrintException(Exception ex, InvocationContext context) @@ -237,7 +268,7 @@ private static int MainInner(string[] args) { try { - var parser = BuildCommandLine(); + Parser parser = BuildCommandLine(); exitCode = (ExitCodes)parser.Invoke(args); } catch (GitException ex) @@ -245,7 +276,7 @@ private static int MainInner(string[] args) Console.Error.WriteLine($"ERROR: {ex.Message}"); exitCode = ex.ErrorCode switch { - GitException.ErrorCodes.ObjectNotFound when ex.iSShallowClone => ExitCodes.ShallowClone, + GitException.ErrorCodes.ObjectNotFound when ex.IsShallowClone => ExitCodes.ShallowClone, _ => ExitCodes.InternalError, }; } @@ -253,9 +284,9 @@ private static int MainInner(string[] args) return (int)exitCode; } - private static int OnInstallCommand(string path, string version, IReadOnlyList source) + private static async Task OnInstallCommand(string path, string version, string[] source) { - if (!SemanticVersion.TryParse(string.IsNullOrEmpty(version) ? DefaultVersionSpec : version, out var semver)) + if (!SemanticVersion.TryParse(string.IsNullOrEmpty(version) ? DefaultVersionSpec : version, out SemanticVersion semver)) { Console.Error.WriteLine($"\"{version}\" is not a semver-compliant version spec."); return (int)ExitCodes.InvalidVersionSpec; @@ -284,7 +315,7 @@ private static int OnInstallCommand(string path, string version, IReadOnlyList to validate argument during parsing. - if (!Uri.TryCreate(src, UriKind.Absolute, out var _)) + if (!Uri.TryCreate(src, UriKind.Absolute, out Uri _)) { Console.Error.WriteLine($"\"{src}\" is not a valid NuGet package source."); return (int)ExitCodes.InvalidNuGetPackageSource; @@ -345,7 +376,7 @@ private static int OnInstallCommand(string path, string version, IReadOnlyList i.ItemType == PackageReferenceItemType && i.Include == PackageId); + ProjectItemElement item = propsFile.Items.FirstOrDefault(i => i.ItemType == PackageReferenceItemType && i.Include == PackageId); if (item is null) { @@ -355,12 +386,12 @@ private static int OnInstallCommand(string path, string version, IReadOnlyList { { PrivateAssetsMetadataName, "all" }, - { VersionMetadataName, packageVersion } + { VersionMetadataName, packageVersion }, }); } else { - var versionMetadata = item.Metadata.Single(m => m.Name == VersionMetadataName); + ProjectMetadataElement versionMetadata = item.Metadata.Single(m => m.Name == VersionMetadataName); versionMetadata.Value = packageVersion; } @@ -372,7 +403,7 @@ private static int OnInstallCommand(string path, string version, IReadOnlyList metadata, string format, string variable, string commitish) + private static Task OnGetVersionCommand(string project, string[] metadata, string format, string variable, string commitish) { if (string.IsNullOrEmpty(format)) { @@ -386,17 +417,17 @@ private static int OnGetVersionCommand(string project, IReadOnlyList met string searchPath = GetSpecifiedOrCurrentDirectoryPath(project); - using var context = GitContext.Create(searchPath, writable: AlwaysUseLibGit2); + using var context = GitContext.Create(searchPath, engine: AlwaysUseLibGit2 ? GitContext.Engine.ReadWrite : GitContext.Engine.ReadOnly); if (!context.IsRepository) { Console.Error.WriteLine("No git repo found at or above: \"{0}\"", searchPath); - return (int)ExitCodes.NoGitRepo; + return Task.FromResult((int)ExitCodes.NoGitRepo); } if (!context.TrySelectCommit(commitish)) { Console.Error.WriteLine("rev-parse produced no commit for {0}", commitish); - return (int)ExitCodes.BadGitRef; + return Task.FromResult((int)ExitCodes.BadGitRef); } var oracle = new VersionOracle(context, CloudBuild.Active); @@ -431,7 +462,7 @@ private static int OnGetVersionCommand(string project, IReadOnlyList met break; default: Console.Error.WriteLine("Unsupported format: {0}", format); - return (int)ExitCodes.UnsupportedFormat; + return Task.FromResult((int)ExitCodes.UnsupportedFormat); } } else @@ -439,28 +470,28 @@ private static int OnGetVersionCommand(string project, IReadOnlyList met if (format != "text") { Console.Error.WriteLine("Format must be \"text\" when querying for an individual variable's value."); - return (int)ExitCodes.UnsupportedFormat; + return Task.FromResult((int)ExitCodes.UnsupportedFormat); } - var property = oracle.GetType().GetProperty(variable, CaseInsensitiveFlags); + PropertyInfo property = oracle.GetType().GetProperty(variable, CaseInsensitiveFlags); if (property is null) { Console.Error.WriteLine("Variable \"{0}\" not a version property.", variable); - return (int)ExitCodes.BadVariable; + return Task.FromResult((int)ExitCodes.BadVariable); } Console.WriteLine(property.GetValue(oracle)); } - return (int)ExitCodes.OK; + return Task.FromResult((int)ExitCodes.OK); } - private static int OnSetVersionCommand(string project, string version) + private static Task OnSetVersionCommand(string project, string version) { - if (!SemanticVersion.TryParse(string.IsNullOrEmpty(version) ? DefaultVersionSpec : version, out var semver)) + if (!SemanticVersion.TryParse(string.IsNullOrEmpty(version) ? DefaultVersionSpec : version, out SemanticVersion semver)) { Console.Error.WriteLine($"\"{version}\" is not a semver-compliant version spec."); - return (int)ExitCodes.InvalidVersionSpec; + return Task.FromResult((int)ExitCodes.InvalidVersionSpec); } var defaultOptions = new VersionOptions @@ -469,8 +500,8 @@ private static int OnSetVersionCommand(string project, string version) }; string searchPath = GetSpecifiedOrCurrentDirectoryPath(project); - using var context = GitContext.Create(searchPath, writable: true); - var existingOptions = context.VersionFile.GetVersion(out string actualDirectory); + using var context = GitContext.Create(searchPath, engine: GitContext.Engine.ReadWrite); + VersionOptions existingOptions = context.VersionFile.GetVersion(out string actualDirectory); string versionJsonPath; if (existingOptions is not null) { @@ -482,7 +513,7 @@ private static int OnSetVersionCommand(string project, string version) if (!context.IsRepository) { Console.Error.WriteLine("No version file and no git repo found at or above: \"{0}\"", searchPath); - return (int)ExitCodes.NoGitRepo; + return Task.FromResult((int)ExitCodes.NoGitRepo); } versionJsonPath = context.VersionFile.SetVersion(context.WorkingTreePath, defaultOptions); @@ -497,10 +528,10 @@ private static int OnSetVersionCommand(string project, string version) context.Stage(versionJsonPath); } - return (int)ExitCodes.OK; + return Task.FromResult((int)ExitCodes.OK); } - private static int OnTagCommand(string project, string versionOrRef) + private static Task OnTagCommand(string project, string versionOrRef) { if (string.IsNullOrEmpty(versionOrRef)) { @@ -509,20 +540,38 @@ private static int OnTagCommand(string project, string versionOrRef) string searchPath = GetSpecifiedOrCurrentDirectoryPath(project); - using var context = (LibGit2Context)GitContext.Create(searchPath, writable: true); + using var context = (LibGit2Context)GitContext.Create(searchPath, engine: GitContext.Engine.ReadWrite); if (context is null) { Console.Error.WriteLine("No git repo found at or above: \"{0}\"", searchPath); - return (int)ExitCodes.NoGitRepo; + return Task.FromResult((int)ExitCodes.NoGitRepo); + } + + // get tag name format + VersionOptions versionOptions = context.VersionFile.GetVersion(); + if (versionOptions is null) + { + Console.Error.WriteLine($"Failed to load version file for directory '{searchPath}'."); + return Task.FromResult((int)ExitCodes.NoVersionJsonFound); + } + + string tagNameFormat = versionOptions.ReleaseOrDefault.TagNameOrDefault; + + // ensure there is a '{version}' placeholder in the tag name + if (string.IsNullOrEmpty(tagNameFormat) || !tagNameFormat.Contains("{version}")) + { + Console.Error.WriteLine($"Invalid 'tagName' setting '{tagNameFormat}'. Missing version placeholder '{{version}}'."); + return Task.FromResult((int)ExitCodes.InvalidTagNameSetting); } - var repository = context.Repository; + // get commit to tag + LibGit2Sharp.Repository repository = context.Repository; if (!context.TrySelectCommit(versionOrRef)) { if (!Version.TryParse(versionOrRef, out Version parsedVersion)) { Console.Error.WriteLine($"\"{versionOrRef}\" is not a simple a.b.c[.d] version spec or git reference."); - return (int)ExitCodes.InvalidVersionSpec; + return Task.FromResult((int)ExitCodes.InvalidVersionSpec); } string repoRelativeProjectDir = GetRepoRelativePath(searchPath, repository); @@ -530,7 +579,7 @@ private static int OnTagCommand(string project, string versionOrRef) if (candidateCommits.Count == 0) { Console.Error.WriteLine("No commit with that version found."); - return (int)ExitCodes.NoMatchingVersion; + return Task.FromResult((int)ExitCodes.NoMatchingVersion); } else if (candidateCommits.Count > 1) { @@ -553,11 +602,15 @@ private static int OnTagCommand(string project, string versionOrRef) if (!oracle.VersionFileFound) { Console.Error.WriteLine("No version.json file found in or above \"{0}\" in commit {1}.", searchPath, context.GitCommitId); - return (int)ExitCodes.NoVersionJsonFound; + return Task.FromResult((int)ExitCodes.NoVersionJsonFound); } - oracle.PublicRelease = true; // assume a public release so we don't get a redundant -gCOMMITID in the tag name - string tagName = $"v{oracle.SemVer2}"; + // assume a public release so we don't get a redundant -gCOMMITID in the tag name + oracle.PublicRelease = true; + + // replace the "{version}" placeholder with the actual version + string tagName = tagNameFormat.Replace("{version}", oracle.SemVer2); + try { context.ApplyTag(tagName); @@ -567,45 +620,45 @@ private static int OnTagCommand(string project, string versionOrRef) var taggedCommit = repository.Tags[tagName].Target as LibGit2Sharp.Commit; bool correctTag = taggedCommit?.Sha == context.GitCommitId; Console.Error.WriteLine("The tag {0} is already defined ({1}).", tagName, correctTag ? "to the right commit" : $"expected {context.GitCommitId} but was on {taggedCommit.Sha}"); - return (int)(correctTag ? ExitCodes.OK : ExitCodes.TagConflict); + return Task.FromResult((int)(correctTag ? ExitCodes.OK : ExitCodes.TagConflict)); } Console.WriteLine("{0} tag created at {1}.", tagName, context.GitCommitId); Console.WriteLine("Remember to push to a remote: git push origin {0}", tagName); - return (int)ExitCodes.OK; + return Task.FromResult((int)ExitCodes.OK); } - private static int OnGetCommitsCommand(string project, bool quiet, string version) + private static Task OnGetCommitsCommand(string project, bool quiet, string version) { if (!Version.TryParse(version, out Version parsedVersion)) { Console.Error.WriteLine($"\"{version}\" is not a simple a.b.c[.d] version spec."); - return (int)ExitCodes.InvalidVersionSpec; + return Task.FromResult((int)ExitCodes.InvalidVersionSpec); } string searchPath = GetSpecifiedOrCurrentDirectoryPath(project); - using var context = (LibGit2Context)GitContext.Create(searchPath, writable: true); + using var context = (LibGit2Context)GitContext.Create(searchPath, engine: GitContext.Engine.ReadWrite); if (!context.IsRepository) { Console.Error.WriteLine("No git repo found at or above: \"{0}\"", searchPath); - return (int)ExitCodes.NoGitRepo; + return Task.FromResult((int)ExitCodes.NoGitRepo); } - var candidateCommits = LibGit2GitExtensions.GetCommitsFromVersion(context, parsedVersion); + IEnumerable candidateCommits = LibGit2GitExtensions.GetCommitsFromVersion(context, parsedVersion); PrintCommits(quiet, context, candidateCommits); - return (int)ExitCodes.OK; + return Task.FromResult((int)ExitCodes.OK); } - private static int OnCloudCommand(string project, IReadOnlyList metadata, string version, string ciSystem, bool allVars, bool commonVars, IReadOnlyList define) + private static Task OnCloudCommand(string project, string[] metadata, string version, string ciSystem, bool allVars, bool commonVars, string[] define) { string searchPath = GetSpecifiedOrCurrentDirectoryPath(project); if (!Directory.Exists(searchPath)) { Console.Error.WriteLine("\"{0}\" is not an existing directory.", searchPath); - return (int)ExitCodes.NoGitRepo; + return Task.FromResult((int)ExitCodes.NoGitRepo); } var additionalVariables = new Dictionary(); @@ -617,13 +670,13 @@ private static int OnCloudCommand(string project, IReadOnlyList metadata if (split.Length < 2) { Console.Error.WriteLine($"\"{def}\" is not in the NAME=VALUE syntax required for cloud variables."); - return (int)ExitCodes.BadCloudVariable; + return Task.FromResult((int)ExitCodes.BadCloudVariable); } if (additionalVariables.ContainsKey(split[0])) { Console.Error.WriteLine($"Cloud build variable \"{split[0]}\" specified more than once."); - return (int)ExitCodes.DuplicateCloudVariable; + return Task.FromResult((int)ExitCodes.DuplicateCloudVariable); } additionalVariables[split[0]] = split[1]; @@ -638,51 +691,52 @@ private static int OnCloudCommand(string project, IReadOnlyList metadata catch (CloudCommand.CloudCommandException ex) { Console.Error.WriteLine(ex.Message); + // map error codes switch (ex.Error) { case CloudCommand.CloudCommandError.NoCloudBuildProviderMatch: - return (int)ExitCodes.NoCloudBuildProviderMatch; + return Task.FromResult((int)ExitCodes.NoCloudBuildProviderMatch); case CloudCommand.CloudCommandError.DuplicateCloudVariable: - return (int)ExitCodes.DuplicateCloudVariable; + return Task.FromResult((int)ExitCodes.DuplicateCloudVariable); case CloudCommand.CloudCommandError.NoCloudBuildEnvDetected: - return (int)ExitCodes.NoCloudBuildEnvDetected; + return Task.FromResult((int)ExitCodes.NoCloudBuildEnvDetected); default: Report.Fail($"{nameof(CloudCommand.CloudCommandError)}: {ex.Error}"); - return -1; + return Task.FromResult(-1); } } - return (int)ExitCodes.OK; - + return Task.FromResult((int)ExitCodes.OK); } - private static int OnPrepareReleaseCommand(string project, string nextVersion, string versionIncrement, string format, string tag) + private static Task OnPrepareReleaseCommand(string project, string nextVersion, string versionIncrement, string format, string tag) { // validate project path property string searchPath = GetSpecifiedOrCurrentDirectoryPath(project); if (!Directory.Exists(searchPath)) { Console.Error.WriteLine($"\"{searchPath}\" is not an existing directory."); - return (int)ExitCodes.NoGitRepo; + return Task.FromResult((int)ExitCodes.NoGitRepo); } // nextVersion and versionIncrement parameters cannot be combined if (!string.IsNullOrEmpty(nextVersion) && !string.IsNullOrEmpty(versionIncrement)) { Console.Error.WriteLine("Options 'nextVersion' and 'versionIncrement' cannot be used at the same time."); - return (int)ExitCodes.InvalidParameters; + return Task.FromResult((int)ExitCodes.InvalidParameters); } // parse versionIncrement if parameter was specified VersionOptions.ReleaseVersionIncrement? versionIncrementParsed = default; if (!string.IsNullOrEmpty(versionIncrement)) { - if (!Enum.TryParse(versionIncrement, true, out var parsed)) + if (!Enum.TryParse(versionIncrement, true, out VersionOptions.ReleaseVersionIncrement parsed)) { Console.Error.WriteLine($"\"{versionIncrement}\" is not a valid version increment"); - return (int)ExitCodes.InvalidVersionIncrement; + return Task.FromResult((int)ExitCodes.InvalidVersionIncrement); } + versionIncrementParsed = parsed; } @@ -693,7 +747,7 @@ private static int OnPrepareReleaseCommand(string project, string nextVersion, s if (!Version.TryParse(nextVersion, out nextVersionParsed)) { Console.Error.WriteLine($"\"{nextVersion}\" is not a valid version spec."); - return (int)ExitCodes.InvalidVersionSpec; + return Task.FromResult((int)ExitCodes.InvalidVersionSpec); } } @@ -702,10 +756,11 @@ private static int OnPrepareReleaseCommand(string project, string nextVersion, s { format = DefaultOutputFormat; } + if (!Enum.TryParse(format, true, out ReleaseManager.ReleaseManagerOutputMode outputMode)) { Console.Error.WriteLine($"Unsupported format: {format}"); - return (int)ExitCodes.UnsupportedFormat; + return Task.FromResult((int)ExitCodes.UnsupportedFormat); } // run prepare-release @@ -713,7 +768,7 @@ private static int OnPrepareReleaseCommand(string project, string nextVersion, s { var releaseManager = new ReleaseManager(Console.Out, Console.Error); releaseManager.PrepareRelease(searchPath, tag, nextVersionParsed, versionIncrementParsed, outputMode); - return (int)ExitCodes.OK; + return Task.FromResult((int)ExitCodes.OK); } catch (ReleaseManager.ReleasePreparationException ex) { @@ -721,34 +776,34 @@ private static int OnPrepareReleaseCommand(string project, string nextVersion, s switch (ex.Error) { case ReleaseManager.ReleasePreparationError.NoGitRepo: - return (int)ExitCodes.NoGitRepo; + return Task.FromResult((int)ExitCodes.NoGitRepo); case ReleaseManager.ReleasePreparationError.UncommittedChanges: - return (int)ExitCodes.UncommittedChanges; + return Task.FromResult((int)ExitCodes.UncommittedChanges); case ReleaseManager.ReleasePreparationError.InvalidBranchNameSetting: - return (int)ExitCodes.InvalidBranchNameSetting; + return Task.FromResult((int)ExitCodes.InvalidBranchNameSetting); case ReleaseManager.ReleasePreparationError.NoVersionFile: - return (int)ExitCodes.NoVersionJsonFound; + return Task.FromResult((int)ExitCodes.NoVersionJsonFound); case ReleaseManager.ReleasePreparationError.VersionDecrement: case ReleaseManager.ReleasePreparationError.NoVersionIncrement: - return (int)ExitCodes.InvalidVersionSpec; + return Task.FromResult((int)ExitCodes.InvalidVersionSpec); case ReleaseManager.ReleasePreparationError.BranchAlreadyExists: - return (int)ExitCodes.BranchAlreadyExists; + return Task.FromResult((int)ExitCodes.BranchAlreadyExists); case ReleaseManager.ReleasePreparationError.UserNotConfigured: - return (int)ExitCodes.UserNotConfigured; + return Task.FromResult((int)ExitCodes.UserNotConfigured); case ReleaseManager.ReleasePreparationError.DetachedHead: - return (int)ExitCodes.DetachedHead; + return Task.FromResult((int)ExitCodes.DetachedHead); case ReleaseManager.ReleasePreparationError.InvalidVersionIncrementSetting: - return (int)ExitCodes.InvalidVersionIncrementSetting; + return Task.FromResult((int)ExitCodes.InvalidVersionIncrementSetting); default: Report.Fail($"{nameof(ReleaseManager.ReleasePreparationError)}: {ex.Error}"); - return -1; + return Task.FromResult(-1); } } } private static async Task GetLatestPackageVersionAsync(string packageId, string root, IReadOnlyList sources, CancellationToken cancellationToken = default) { - var settings = Settings.LoadDefaultSettings(root); + ISettings settings = Settings.LoadDefaultSettings(root); var providers = new List>(); providers.AddRange(Repository.Provider.GetCoreV3()); // Add v3 API support @@ -757,12 +812,12 @@ private static async Task GetLatestPackageVersionAsync(string packageId, // Select package sources based on NuGet.Config files or given options, as 'nuget.exe restore' command does // See also 'DownloadCommandBase.GetPackageSources(ISettings)' at https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Clients/NuGet.CommandLine/Commands/DownloadCommandBase.cs - var availableSources = sourceRepositoryProvider.PackageSourceProvider.LoadPackageSources().Where(s => s.IsEnabled); + IEnumerable availableSources = sourceRepositoryProvider.PackageSourceProvider.LoadPackageSources().Where(s => s.IsEnabled); var packageSources = new List(); - foreach (var source in sources) + foreach (string source in sources) { - var resolvedSource = availableSources.FirstOrDefault(s => s.Source.Equals(source, StringComparison.OrdinalIgnoreCase) || s.Name.Equals(source, StringComparison.OrdinalIgnoreCase)); + PackageSource resolvedSource = availableSources.FirstOrDefault(s => s.Source.Equals(source, StringComparison.OrdinalIgnoreCase) || s.Name.Equals(source, StringComparison.OrdinalIgnoreCase)); packageSources.Add(resolvedSource ?? new PackageSource(source)); } @@ -771,7 +826,7 @@ private static async Task GetLatestPackageVersionAsync(string packageId, packageSources.AddRange(availableSources); } - var sourceRepositories = packageSources.Select(sourceRepositoryProvider.CreateRepository).ToArray(); + SourceRepository[] sourceRepositories = packageSources.Select(sourceRepositoryProvider.CreateRepository).ToArray(); var resolutionContext = new ResolutionContext( DependencyBehavior.Highest, includePrelease: false, @@ -781,7 +836,7 @@ private static async Task GetLatestPackageVersionAsync(string packageId, // The target framework doesn't matter, since our package doesn't depend on this for its target projects. var framework = new NuGet.Frameworks.NuGetFramework("net45"); - var pkg = await NuGetPackageManager.GetLatestVersionAsync( + ResolvedPackage pkg = await NuGetPackageManager.GetLatestVersionAsync( packageId, framework, resolutionContext, @@ -815,7 +870,7 @@ private static string ShouldHaveTrailingDirectorySeparator(string path) private static void PrintCommits(bool quiet, GitContext context, IEnumerable candidateCommits, bool includeOptions = false) { int index = 1; - foreach (var commit in candidateCommits) + foreach (LibGit2Sharp.Commit commit in candidateCommits) { if (includeOptions) { @@ -834,7 +889,5 @@ private static void PrintCommits(bool quiet, GitContext context, IEnumerable CloudBuild.SupportedCloudBuilds.Select(cb => cb.GetType().Name).ToArray(); } } diff --git a/src/nbgv/nbgv.csproj b/src/nbgv/nbgv.csproj index ea5c3399..8dfcf125 100644 --- a/src/nbgv/nbgv.csproj +++ b/src/nbgv/nbgv.csproj @@ -4,17 +4,17 @@ nbgv True Exe - netcoreapp3.1 + net6.0 Nerdbank.GitVersioning.Tool A .NET Core Tool that can install, read and set version information based on git history, using Nerdbank.GitVersioning. - - - - - + + + + + @@ -22,6 +22,6 @@ - + diff --git a/src/nerdbank-gitversioning.npm/gulpfile.js b/src/nerdbank-gitversioning.npm/gulpfile.js index 62c52cbd..8850d780 100644 --- a/src/nerdbank-gitversioning.npm/gulpfile.js +++ b/src/nerdbank-gitversioning.npm/gulpfile.js @@ -5,7 +5,7 @@ var ts = require('gulp-typescript'); var sourcemaps = require('gulp-sourcemaps'); var merge = require('merge2'); // var tslint = require('gulp-tslint'); -var del = require('del'); +var del = import('del'); var path = require('path'); const outDir = 'out'; @@ -15,7 +15,7 @@ var tsProject = ts.createProject('tsconfig.json', { }); gulp.task('tsc', function () { - var tsResult = gulp.src(['*.ts', 'ts/**/*.ts', 'node_modules/@types/**/index.d.ts']) + var tsResult = gulp.src(['*.ts', 'ts/**/*.ts']) // .pipe(tslint()) .pipe(sourcemaps.init()) .pipe(tsProject()); diff --git a/src/nerdbank-gitversioning.npm/package.json b/src/nerdbank-gitversioning.npm/package.json index 29aca42b..b56dcd96 100644 --- a/src/nerdbank-gitversioning.npm/package.json +++ b/src/nerdbank-gitversioning.npm/package.json @@ -25,8 +25,8 @@ ], "devDependencies": { "@types/camel-case": "^1.2.1", - "@types/node": "^17.0.30", - "del": "^6.0.0", + "@types/node": "^18.7.14", + "del": "^7.0.0", "gulp": "^4.0.2", "gulp-cli": "^2.3.0", "gulp-sourcemaps": "3.0.0", @@ -34,7 +34,7 @@ "gulp-util": "^3.0.8", "merge2": "^1.4.1", "path": "^0.12.7", - "typescript": "^4.6.4" + "typescript": "^5.0.4" }, "dependencies": { "camel-case": "^4.1.2" diff --git a/src/nerdbank-gitversioning.npm/ts/core.ts b/src/nerdbank-gitversioning.npm/ts/core.ts index 9fcddeea..10c78675 100644 --- a/src/nerdbank-gitversioning.npm/ts/core.ts +++ b/src/nerdbank-gitversioning.npm/ts/core.ts @@ -5,6 +5,6 @@ const nbgvPath = 'nbgv.cli'; export function getNbgvCommand(dotnetCommand?: string): string { var command = dotnetCommand || 'dotnet'; - const nbgvDll = path.join(__dirname, nbgvPath, "tools", "netcoreapp3.1", "any", "nbgv.dll"); + const nbgvDll = path.join(__dirname, nbgvPath, "tools", "net6.0", "any", "nbgv.dll"); return `${command} "${nbgvDll}"`; } diff --git a/src/nerdbank-gitversioning.npm/yarn.lock b/src/nerdbank-gitversioning.npm/yarn.lock index fc7e348c..1ceb2a9f 100644 --- a/src/nerdbank-gitversioning.npm/yarn.lock +++ b/src/nerdbank-gitversioning.npm/yarn.lock @@ -49,23 +49,23 @@ dependencies: camel-case "*" -"@types/node@^17.0.30": - version "17.0.30" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.30.tgz#2c6e8512acac70815e8176aa30c38025067880ef" - integrity sha512-oNBIZjIqyHYP8VCNAV9uEytXVeXG2oR0w9lgAXro20eugRQfY002qr3CUl6BAe+Yf/z3CRjPdz27Pu6WWtuSRw== +"@types/node@^18.7.14": + version "18.15.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f" + integrity sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q== acorn@^6.4.1: version "6.4.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== -aggregate-error@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.0.1.tgz#db2fe7246e536f40d9b5442a39e117d7dd6a24e0" - integrity sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA== +aggregate-error@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-4.0.1.tgz#25091fe1573b9e0be892aeda15c7c66a545f758e" + integrity sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w== dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" + clean-stack "^4.0.0" + indent-string "^5.0.0" ansi-colors@^1.0.1: version "1.1.0" @@ -189,11 +189,6 @@ array-sort@^1.0.0: get-value "^2.0.6" kind-of "^5.0.2" -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - array-uniq@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" @@ -252,9 +247,9 @@ bach@^1.0.0: now-and-later "^2.0.0" balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== base@^0.11.1: version "0.11.2" @@ -403,10 +398,12 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== +clean-stack@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-4.2.0.tgz#c464e4cde4ac789f4e0735c5d75beb49d7b30b31" + integrity sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg== + dependencies: + escape-string-regexp "5.0.0" cliui@^3.2.0: version "3.2.0" @@ -486,7 +483,7 @@ component-emitter@^1.2.1: concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== concat-stream@^1.6.0: version "1.6.2" @@ -581,9 +578,9 @@ decamelize@^1.1.1: integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== default-compare@^1.0.0: version "1.0.0" @@ -626,19 +623,19 @@ define-property@^2.0.2: is-descriptor "^1.0.2" isobject "^3.0.1" -del@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/del/-/del-6.0.0.tgz#0b40d0332cea743f1614f818be4feb717714c952" - integrity sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ== - dependencies: - globby "^11.0.1" - graceful-fs "^4.2.4" - is-glob "^4.0.1" - is-path-cwd "^2.2.0" - is-path-inside "^3.0.2" - p-map "^4.0.0" +del@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/del/-/del-7.0.0.tgz#79db048bec96f83f344b46c1a66e35d9c09fe8ac" + integrity sha512-tQbV/4u5WVB8HMJr08pgw0b6nG4RGt/tj+7Numvq+zqcvUFeMaIWWOUFltiU+6go8BSO2/ogsB4EasDaj0y68Q== + dependencies: + globby "^13.1.2" + graceful-fs "^4.2.10" + is-glob "^4.0.3" + is-path-cwd "^3.0.0" + is-path-inside "^4.0.0" + p-map "^5.5.0" rimraf "^3.0.2" - slash "^3.0.0" + slash "^4.0.0" detect-file@^1.0.0: version "1.0.0" @@ -732,6 +729,11 @@ es6-weak-map@^2.0.1, es6-weak-map@^2.0.2: es6-iterator "^2.0.3" es6-symbol "^3.1.1" +escape-string-regexp@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" + integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== + escape-string-regexp@^1.0.2: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -816,7 +818,7 @@ fancy-log@^1.1.0, fancy-log@^1.3.2: parse-node-version "^1.0.0" time-stamp "^1.0.0" -fast-glob@^3.2.9: +fast-glob@^3.2.11: version "3.2.11" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== @@ -1038,17 +1040,16 @@ global-prefix@^1.0.1: is-windows "^1.0.1" which "^1.2.14" -globby@^11.0.1: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== +globby@^13.1.2: + version "13.1.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.2.tgz#29047105582427ab6eca4f905200667b056da515" + integrity sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ== dependencies: - array-union "^2.1.0" dir-glob "^3.0.1" - fast-glob "^3.2.9" + fast-glob "^3.2.11" ignore "^5.2.0" merge2 "^1.4.1" - slash "^3.0.0" + slash "^4.0.0" glogg@^1.0.0: version "1.0.2" @@ -1057,12 +1058,7 @@ glogg@^1.0.0: dependencies: sparkles "^1.0.0" -graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6: - version "4.2.3" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" - integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== - -graceful-fs@^4.2.4: +graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.10: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== @@ -1252,10 +1248,10 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== +indent-string@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-5.0.0.tgz#4fd2980fccaf8622d14c64d694f4cf33c81951a5" + integrity sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg== inflight@^1.0.4: version "1.0.6" @@ -1397,10 +1393,10 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" -is-glob@^4.0.0, is-glob@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: is-extglob "^2.1.1" @@ -1426,15 +1422,15 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-path-cwd@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" - integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== +is-path-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-3.0.0.tgz#889b41e55c8588b1eb2a96a61d05740a674521c7" + integrity sha512-kyiNFFLU0Ampr6SDZitD/DwUo4Zs1nSdnygUBqsu3LooL00Qvb5j+UnvApUn/TTj1J3OuE6BTdQ5rudKmU2ZaA== -is-path-inside@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +is-path-inside@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-4.0.0.tgz#805aeb62c47c1b12fc3fd13bfb3ed1e7430071db" + integrity sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA== is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" @@ -1798,9 +1794,9 @@ micromatch@^4.0.4: picomatch "^2.3.1" minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" @@ -2016,12 +2012,12 @@ os-locale@^1.4.0: dependencies: lcid "^1.0.0" -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== +p-map@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-5.5.0.tgz#054ca8ca778dfa4cf3f8db6638ccb5b937266715" + integrity sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg== dependencies: - aggregate-error "^3.0.0" + aggregate-error "^4.0.0" parse-filepath@^1.0.1: version "1.0.2" @@ -2435,10 +2431,10 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slash@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" + integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== snapdragon-node@^2.0.1: version "2.1.1" @@ -2746,10 +2742,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^4.6.4: - version "4.6.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" - integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg== +typescript@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" + integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== unc-path-regex@^0.1.2: version "0.1.2" diff --git a/src/nuget.config b/src/nuget.config deleted file mode 100644 index ede70863..00000000 --- a/src/nuget.config +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - Microsoft;aarnott;xunit;kzu;castleproject;patrickb8man;jamesnk;ethomson;AndreyAkinshin;MarcoRossignoli;cake-build;ericnewton76;0xd4d;manuel.roemer - - - - - - - - - - - - diff --git a/src/strongname.snk b/strongname.snk similarity index 100% rename from src/strongname.snk rename to strongname.snk diff --git a/stylecop.json b/stylecop.json new file mode 100644 index 00000000..840fd358 --- /dev/null +++ b/stylecop.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": ".NET Foundation and Contributors", + "copyrightText": "Copyright (c) {companyName}. All rights reserved.\nLicensed under the {licenseName} license. See {licenseFile} file in the project root for full license information.", + "variables": { + "licenseName": "MIT", + "licenseFile": "LICENSE" + }, + "fileNamingConvention": "metadata", + "xmlHeader": false + }, + "orderingRules": { + "usingDirectivesPlacement": "outsideNamespace" + } + } +} diff --git a/test/.editorconfig b/test/.editorconfig new file mode 100644 index 00000000..a055c537 --- /dev/null +++ b/test/.editorconfig @@ -0,0 +1,58 @@ +[*.cs] + +# SA1600: Elements should be documented +dotnet_diagnostic.SA1600.severity = silent + +# SA1601: Partial elements should be documented +dotnet_diagnostic.SA1601.severity = silent + +# SA1602: Enumeration items should be documented +dotnet_diagnostic.SA1602.severity = silent + +# SA1615: Element return value should be documented +dotnet_diagnostic.SA1615.severity = silent + +# VSTHRD103: Call async methods when in an async method +dotnet_diagnostic.VSTHRD103.severity = silent + +# VSTHRD111: Use .ConfigureAwait(bool) +dotnet_diagnostic.VSTHRD111.severity = none + +# VSTHRD200: Use Async suffix for async methods +dotnet_diagnostic.VSTHRD200.severity = silent + +# CA1014: Mark assemblies with CLSCompliant +dotnet_diagnostic.CA1014.severity = none + +# CA1050: Declare types in namespaces +dotnet_diagnostic.CA1050.severity = none + +# CA1303: Do not pass literals as localized parameters +dotnet_diagnostic.CA1303.severity = none + +# CS1591: Missing XML comment for publicly visible type or member +dotnet_diagnostic.CS1591.severity = silent + +# CA1707: Identifiers should not contain underscores +dotnet_diagnostic.CA1707.severity = silent + +# CA1062: Validate arguments of public methods +dotnet_diagnostic.CA1062.severity = suggestion + +# CA1063: Implement IDisposable Correctly +dotnet_diagnostic.CA1063.severity = silent + +# CA1816: Dispose methods should call SuppressFinalize +dotnet_diagnostic.CA1816.severity = silent + +# CA2007: Consider calling ConfigureAwait on the awaited task +dotnet_diagnostic.CA2007.severity = none + +# SA1401: Fields should be private +dotnet_diagnostic.SA1401.severity = silent + +# SA1133: Do not combine attributes +dotnet_diagnostic.SA1133.severity = silent + +# SA1515: Single-line comment should be preceded by blank line +dotnet_diagnostic.SA1515.severity = suggestion diff --git a/test/Cake.GitVersioning.Tests/Cake.GitVersioning.Tests.csproj b/test/Cake.GitVersioning.Tests/Cake.GitVersioning.Tests.csproj new file mode 100644 index 00000000..e79d4931 --- /dev/null +++ b/test/Cake.GitVersioning.Tests/Cake.GitVersioning.Tests.csproj @@ -0,0 +1,24 @@ + + + + net6.0 + false + true + false + true + true + full + + + + + + + + + + + + + + diff --git a/src/Cake.GitVersioning.Tests/GitVersioningCloudProviderTests.cs b/test/Cake.GitVersioning.Tests/GitVersioningCloudProviderTests.cs similarity index 51% rename from src/Cake.GitVersioning.Tests/GitVersioningCloudProviderTests.cs rename to test/Cake.GitVersioning.Tests/GitVersioningCloudProviderTests.cs index 2e22e39c..918507fd 100644 --- a/src/Cake.GitVersioning.Tests/GitVersioningCloudProviderTests.cs +++ b/test/Cake.GitVersioning.Tests/GitVersioningCloudProviderTests.cs @@ -1,30 +1,29 @@ -using System; -using System.Linq; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + using Cake.GitVersioning; using Nerdbank.GitVersioning; using Xunit; /// -/// Tests to verify the enum (part of the Cake integration) is up-to-date +/// Tests to verify the enum (part of the Cake integration) is up-to-date. /// public class GitVersioningCloudProviderTests { [Fact] public void HasExpectedValues() { - var expectedValues = CloudBuild.SupportedCloudBuilds.Select(cb => cb.GetType().Name); - var actualValues = Enum.GetNames(typeof(GitVersioningCloudProvider)); + IEnumerable expectedValues = CloudBuild.SupportedCloudBuilds.Select(cb => cb.GetType().Name); + string[] actualValues = Enum.GetNames(typeof(GitVersioningCloudProvider)); - var missingValues = expectedValues.Except(actualValues); + IEnumerable missingValues = expectedValues.Except(actualValues); Assert.True( !missingValues.Any(), - $"Enumeration is missing the following values of supported cloud build providers: {string.Join(", ", missingValues)}" - ); + $"Enumeration is missing the following values of supported cloud build providers: {string.Join(", ", missingValues)}"); - var redundantValues = actualValues.Except(expectedValues); + IEnumerable redundantValues = actualValues.Except(expectedValues); Assert.True( !redundantValues.Any(), - $"Enumeration contains values which were not found among supported cloud build providers: {string.Join(",", redundantValues)}" - ); + $"Enumeration contains values which were not found among supported cloud build providers: {string.Join(",", redundantValues)}"); } } diff --git a/test/Cake.GitVersioning.Tests/app.config b/test/Cake.GitVersioning.Tests/app.config new file mode 100644 index 00000000..61890f05 --- /dev/null +++ b/test/Cake.GitVersioning.Tests/app.config @@ -0,0 +1,5 @@ + + + + + diff --git a/test/Directory.Build.props b/test/Directory.Build.props new file mode 100644 index 00000000..ad4a4b6c --- /dev/null +++ b/test/Directory.Build.props @@ -0,0 +1,9 @@ + + + + + false + true + + + diff --git a/test/Directory.Build.targets b/test/Directory.Build.targets new file mode 100644 index 00000000..052fe3ef --- /dev/null +++ b/test/Directory.Build.targets @@ -0,0 +1,3 @@ + + + diff --git a/src/NerdBank.GitVersioning.Benchmarks/GetVersionBenchmarks.cs b/test/Nerdbank.GitVersioning.Benchmarks/GetVersionBenchmarks.cs similarity index 81% rename from src/NerdBank.GitVersioning.Benchmarks/GetVersionBenchmarks.cs rename to test/Nerdbank.GitVersioning.Benchmarks/GetVersionBenchmarks.cs index 8445996c..b1bbda93 100644 --- a/src/NerdBank.GitVersioning.Benchmarks/GetVersionBenchmarks.cs +++ b/test/Nerdbank.GitVersioning.Benchmarks/GetVersionBenchmarks.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; using System.IO; using System.Runtime.InteropServices; using BenchmarkDotNet.Attributes; @@ -6,10 +9,8 @@ namespace Nerdbank.GitVersioning.Benchmarks { - [SimpleJob(RuntimeMoniker.NetCoreApp31, baseline: true)] - [SimpleJob(RuntimeMoniker.NetCoreApp21)] - [SimpleJob(RuntimeMoniker.NetCoreApp50)] - [SimpleJob(RuntimeMoniker.Net461)] + [SimpleJob(RuntimeMoniker.Net70)] + [SimpleJob(RuntimeMoniker.Net462, baseline: true)] public class GetVersionBenchmarks { // You must manually clone these repositories: @@ -19,7 +20,7 @@ public class GetVersionBenchmarks "xunit", "Cuemon", "SuperSocket", - "NerdBank.GitVersioning")] + "Nerdbank.GitVersioning")] public string ProjectDirectory; public Version Version { get; set; } @@ -27,7 +28,7 @@ public class GetVersionBenchmarks [Benchmark(Baseline = true)] public void GetVersionLibGit2() { - using var context = GitContext.Create(GetPath(this.ProjectDirectory), writable: true); + using var context = GitContext.Create(GetPath(this.ProjectDirectory), engine: GitContext.Engine.ReadWrite); var oracle = new VersionOracle(context, cloudBuild: null); this.Version = oracle.Version; } @@ -35,7 +36,7 @@ public void GetVersionLibGit2() [Benchmark] public void GetVersionManaged() { - using var context = GitContext.Create(GetPath(this.ProjectDirectory), writable: false); + using var context = GitContext.Create(GetPath(this.ProjectDirectory), engine: GitContext.Engine.ReadOnly); var oracle = new VersionOracle(context, cloudBuild: null); this.Version = oracle.Version; } diff --git a/src/NerdBank.GitVersioning.Benchmarks/Nerdbank.GitVersioning.Benchmarks.csproj b/test/Nerdbank.GitVersioning.Benchmarks/Nerdbank.GitVersioning.Benchmarks.csproj similarity index 53% rename from src/NerdBank.GitVersioning.Benchmarks/Nerdbank.GitVersioning.Benchmarks.csproj rename to test/Nerdbank.GitVersioning.Benchmarks/Nerdbank.GitVersioning.Benchmarks.csproj index 53f34443..6e0ed52a 100644 --- a/src/NerdBank.GitVersioning.Benchmarks/Nerdbank.GitVersioning.Benchmarks.csproj +++ b/test/Nerdbank.GitVersioning.Benchmarks/Nerdbank.GitVersioning.Benchmarks.csproj @@ -1,8 +1,8 @@  - netcoreapp3.1;net5.0 - $(TargetFrameworks);net461 + net7.0 + $(TargetFrameworks);net462 Exe true AnyCPU @@ -10,13 +10,13 @@ - - - + + + - + diff --git a/test/Nerdbank.GitVersioning.Benchmarks/Program.cs b/test/Nerdbank.GitVersioning.Benchmarks/Program.cs new file mode 100644 index 00000000..9ea48d86 --- /dev/null +++ b/test/Nerdbank.GitVersioning.Benchmarks/Program.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using BenchmarkDotNet.Running; + +namespace Nerdbank.GitVersioning.Benchmarks +{ + internal class Program + { + private static void Main(string[] args) => + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); + } +} diff --git a/test/Nerdbank.GitVersioning.Tests/AssemblyInfoTest.cs b/test/Nerdbank.GitVersioning.Tests/AssemblyInfoTest.cs new file mode 100644 index 00000000..43641569 --- /dev/null +++ b/test/Nerdbank.GitVersioning.Tests/AssemblyInfoTest.cs @@ -0,0 +1,404 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Build.Utilities; +using Nerdbank.GitVersioning.Tasks; +using Xunit; + +public class AssemblyInfoTest : IClassFixture // The MSBuildFixture throws PlatformNotSupportedException when run on mono. +{ + [SkippableTheory(typeof(PlatformNotSupportedException))] + [InlineData(false)] + [InlineData(true)] + [InlineData(null)] + public void FSharpGenerator(bool? thisAssemblyClass) + { + var info = new AssemblyVersionInfo(); + info.AssemblyCompany = "company"; + info.AssemblyFileVersion = "1.3.1.0"; + info.AssemblyVersion = "1.3.0.0"; + info.AdditionalThisAssemblyFields = + new TaskItem[] + { + new TaskItem( + "CustomString1", + new Dictionary() { { "String", "abc" } }), + new TaskItem( + "CustomString2", + new Dictionary() { { "String", string.Empty } }), + new TaskItem( + "CustomString3", + new Dictionary() { { "String", string.Empty }, { "EmitIfEmpty", "true" } }), + new TaskItem( + "CustomBool", + new Dictionary() { { "Boolean", "true" } }), + new TaskItem( + "CustomTicks", + new Dictionary() { { "Ticks", "637509805729817056" } }), + }; + info.CodeLanguage = "f#"; + + if (thisAssemblyClass.HasValue) + { + info.EmitThisAssemblyClass = thisAssemblyClass.GetValueOrDefault(); + } + + string built = info.BuildCode(); + + string expected = $@"//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nowarn ""CA2243"" + +namespace AssemblyInfo +[] +[] +[] +{(thisAssemblyClass.GetValueOrDefault(true) ? $@"do() +#if NETSTANDARD || NETFRAMEWORK || NETCOREAPP +[] +#endif +#if NET40_OR_GREATER || NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER +[] +#endif +type internal ThisAssembly() = + static member internal AssemblyCompany = ""company"" + static member internal AssemblyFileVersion = ""1.3.1.0"" + static member internal AssemblyVersion = ""1.3.0.0"" + static member internal CustomBool = true + static member internal CustomString1 = ""abc"" + static member internal CustomString3 = """" + static member internal CustomTicks = new System.DateTime(637509805729817056L, System.DateTimeKind.Utc) + static member internal IsPrerelease = false + static member internal IsPublicRelease = false + static member internal RootNamespace = """" +do() +" : string.Empty)}"; + + Assert.Equal(expected, built); + } + + [SkippableTheory(typeof(PlatformNotSupportedException))] + [InlineData(null, "MyRootNamespace")] + [InlineData("", "MyRootNamespace")] + [InlineData("MyCustomNamespace", null)] + [InlineData("MyCustomNamespace", "")] + [InlineData("MyCustomNamespace", "MyRootNamespace")] + public void FSharpGeneratorWithNamespace(string thisAssemblyNamespace, string rootNamespace) + { + var info = new AssemblyVersionInfo + { + AssemblyCompany = "company", + AssemblyFileVersion = "1.3.1.0", + AssemblyVersion = "1.3.0.0", + CodeLanguage = "f#", + RootNamespace = rootNamespace, + ThisAssemblyNamespace = thisAssemblyNamespace, + }; + + string built = info.BuildCode(); + + string expected = $@"//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nowarn ""CA2243"" + +namespace {( + !string.IsNullOrWhiteSpace(thisAssemblyNamespace) + ? thisAssemblyNamespace + : !string.IsNullOrWhiteSpace(rootNamespace) + ? rootNamespace + : "AssemblyInfo")} +[] +[] +[] +do() +#if NETSTANDARD || NETFRAMEWORK || NETCOREAPP +[] +#endif +#if NET40_OR_GREATER || NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER +[] +#endif +type internal ThisAssembly() = + static member internal AssemblyCompany = ""company"" + static member internal AssemblyFileVersion = ""1.3.1.0"" + static member internal AssemblyVersion = ""1.3.0.0"" + static member internal IsPrerelease = false + static member internal IsPublicRelease = false + static member internal RootNamespace = ""{rootNamespace}"" +do() +"; + + Assert.Equal(expected, built); + } + + [SkippableTheory(typeof(PlatformNotSupportedException))] + [InlineData(false)] + [InlineData(true)] + [InlineData(null)] + public void CSharpGenerator(bool? thisAssemblyClass) + { + var info = new AssemblyVersionInfo(); + info.AssemblyCompany = "company"; + info.AssemblyFileVersion = "1.3.1.0"; + info.AssemblyVersion = "1.3.0.0"; + info.CodeLanguage = "c#"; + info.AdditionalThisAssemblyFields = + new TaskItem[] + { + new TaskItem( + "CustomString1", + new Dictionary() { { "String", "abc" } }), + new TaskItem( + "CustomString2", + new Dictionary() { { "String", string.Empty } }), + new TaskItem( + "CustomString3", + new Dictionary() { { "String", string.Empty }, { "EmitIfEmpty", "true" } }), + new TaskItem( + "CustomBool", + new Dictionary() { { "Boolean", "true" } }), + new TaskItem( + "CustomTicks", + new Dictionary() { { "Ticks", "637509805729817056" } }), + }; + + if (thisAssemblyClass.HasValue) + { + info.EmitThisAssemblyClass = thisAssemblyClass.GetValueOrDefault(); + } + + string built = info.BuildCode(); + + string expected = $@"//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable CA2243 + +[assembly: System.Reflection.AssemblyVersionAttribute(""1.3.0.0"")] +[assembly: System.Reflection.AssemblyFileVersionAttribute(""1.3.1.0"")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("""")] +{(thisAssemblyClass.GetValueOrDefault(true) ? $@"#if NETSTANDARD || NETFRAMEWORK || NETCOREAPP +[System.CodeDom.Compiler.GeneratedCode(""{AssemblyVersionInfo.GeneratorName}"",""{AssemblyVersionInfo.GeneratorVersion}"")] +#endif +#if NET40_OR_GREATER || NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif +internal static partial class ThisAssembly {{ + internal const string AssemblyCompany = ""company""; + internal const string AssemblyFileVersion = ""1.3.1.0""; + internal const string AssemblyVersion = ""1.3.0.0""; + internal const bool CustomBool = true; + internal const string CustomString1 = ""abc""; + internal const string CustomString3 = """"; + internal static readonly System.DateTime CustomTicks = new System.DateTime(637509805729817056L, System.DateTimeKind.Utc); + internal const bool IsPrerelease = false; + internal const bool IsPublicRelease = false; + internal const string RootNamespace = """"; +}} +" : string.Empty)}"; + + Assert.Equal(expected, built); + } + + [SkippableTheory(typeof(PlatformNotSupportedException))] + [InlineData(null, "MyRootNamespace")] + [InlineData("", "MyRootNamespace")] + [InlineData("MyCustomNamespace", null)] + [InlineData("MyCustomNamespace", "")] + [InlineData("MyCustomNamespace", "MyRootNamespace")] + public void CSharpGeneratorWithNamespace(string thisAssemblyNamespace, string rootNamespace) + { + var info = new AssemblyVersionInfo + { + AssemblyCompany = "company", + AssemblyFileVersion = "1.3.1.0", + AssemblyVersion = "1.3.0.0", + CodeLanguage = "c#", + RootNamespace = rootNamespace, + ThisAssemblyNamespace = thisAssemblyNamespace, + }; + + string built = info.BuildCode(); + + (string nsStart, string nsEnd) = !string.IsNullOrWhiteSpace(thisAssemblyNamespace) + ? ($"{Environment.NewLine}namespace {thisAssemblyNamespace} {{", $"{Environment.NewLine}}}") + : (string.Empty, string.Empty); + + string expected = $@"//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable CA2243 + +[assembly: System.Reflection.AssemblyVersionAttribute(""1.3.0.0"")] +[assembly: System.Reflection.AssemblyFileVersionAttribute(""1.3.1.0"")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("""")]{nsStart} +#if NETSTANDARD || NETFRAMEWORK || NETCOREAPP +[System.CodeDom.Compiler.GeneratedCode(""{AssemblyVersionInfo.GeneratorName}"",""{AssemblyVersionInfo.GeneratorVersion}"")] +#endif +#if NET40_OR_GREATER || NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif +internal static partial class ThisAssembly {{ + internal const string AssemblyCompany = ""company""; + internal const string AssemblyFileVersion = ""1.3.1.0""; + internal const string AssemblyVersion = ""1.3.0.0""; + internal const bool IsPrerelease = false; + internal const bool IsPublicRelease = false; + internal const string RootNamespace = ""{rootNamespace}""; +}}{nsEnd} +"; + + Assert.Equal(expected, built); + } + + [SkippableTheory(typeof(PlatformNotSupportedException))] + [InlineData(false)] + [InlineData(true)] + [InlineData(null)] + public void VisualBasicGenerator(bool? thisAssemblyClass) + { + var info = new AssemblyVersionInfo(); + info.AssemblyCompany = "company"; + info.AssemblyFileVersion = "1.3.1.0"; + info.AssemblyVersion = "1.3.0.0"; + info.CodeLanguage = "vb"; + + if (thisAssemblyClass.HasValue) + { + info.EmitThisAssemblyClass = thisAssemblyClass.GetValueOrDefault(); + } + + string built = info.BuildCode(); + + string expected = $@"'------------------------------------------------------------------------------ +' +' This code was generated by a tool. +' Runtime Version:4.0.30319.42000 +' +' Changes to this file may cause incorrect behavior and will be lost if +' the code is regenerated. +' +'------------------------------------------------------------------------------ + +#Disable Warning CA2243 + + + + +{(thisAssemblyClass.GetValueOrDefault(true) ? $@"#If NET40_OR_GREATER Or NETCOREAPP2_0_OR_GREATER Or NETSTANDARD2_0_OR_GREATER Then + + +Partial Friend NotInheritable Class ThisAssembly +#ElseIf NETSTANDARD Or NETFRAMEWORK Or NETCOREAPP Then + +Partial Friend NotInheritable Class ThisAssembly +#Else +Partial Friend NotInheritable Class ThisAssembly +#End If + Friend Const AssemblyCompany As String = ""company"" + Friend Const AssemblyFileVersion As String = ""1.3.1.0"" + Friend Const AssemblyVersion As String = ""1.3.0.0"" + Friend Const IsPrerelease As Boolean = False + Friend Const IsPublicRelease As Boolean = False + Friend Const RootNamespace As String = """" +End Class +" : string.Empty)}"; + + Assert.Equal(expected, built); + } + + [SkippableTheory(typeof(PlatformNotSupportedException))] + [InlineData(null, "MyRootNamespace")] + [InlineData("", "MyRootNamespace")] + [InlineData("MyCustomNamespace", null)] + [InlineData("MyCustomNamespace", "")] + [InlineData("MyCustomNamespace", "MyRootNamespace")] + public void VisualBasicGeneratorWithNamespace(string thisAssemblyNamespace, string rootNamespace) + { + var info = new AssemblyVersionInfo + { + AssemblyCompany = "company", + AssemblyFileVersion = "1.3.1.0", + AssemblyVersion = "1.3.0.0", + CodeLanguage = "vb", + RootNamespace = rootNamespace, + ThisAssemblyNamespace = thisAssemblyNamespace, + }; + + string built = info.BuildCode(); + + (string nsStart, string nsEnd) = !string.IsNullOrWhiteSpace(thisAssemblyNamespace) + ? ($"{Environment.NewLine}Namespace {thisAssemblyNamespace}", $"{Environment.NewLine}End Namespace") + : (string.Empty, string.Empty); + + string expected = $@"'------------------------------------------------------------------------------ +' +' This code was generated by a tool. +' Runtime Version:4.0.30319.42000 +' +' Changes to this file may cause incorrect behavior and will be lost if +' the code is regenerated. +' +'------------------------------------------------------------------------------ + +#Disable Warning CA2243 + + + +{nsStart} +#If NET40_OR_GREATER Or NETCOREAPP2_0_OR_GREATER Or NETSTANDARD2_0_OR_GREATER Then + + +Partial Friend NotInheritable Class ThisAssembly +#ElseIf NETSTANDARD Or NETFRAMEWORK Or NETCOREAPP Then + +Partial Friend NotInheritable Class ThisAssembly +#Else +Partial Friend NotInheritable Class ThisAssembly +#End If + Friend Const AssemblyCompany As String = ""company"" + Friend Const AssemblyFileVersion As String = ""1.3.1.0"" + Friend Const AssemblyVersion As String = ""1.3.0.0"" + Friend Const IsPrerelease As Boolean = False + Friend Const IsPublicRelease As Boolean = False + Friend Const RootNamespace As String = ""{rootNamespace}"" +End Class{nsEnd} +"; + + Assert.Equal(expected, built); + } +} diff --git a/test/Nerdbank.GitVersioning.Tests/BuildIntegrationDisabledTests.cs b/test/Nerdbank.GitVersioning.Tests/BuildIntegrationDisabledTests.cs new file mode 100644 index 00000000..c9ef482f --- /dev/null +++ b/test/Nerdbank.GitVersioning.Tests/BuildIntegrationDisabledTests.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Nerdbank.GitVersioning; +using Xunit; +using Xunit.Abstractions; + +[Trait("Engine", EngineString)] +[Collection("Build")] // msbuild sets current directory in the process, so we can't have it be concurrent with other build tests. +public class BuildIntegrationDisabledTests : BuildIntegrationTests +{ + private const string EngineString = "Disabled"; + + public BuildIntegrationDisabledTests(ITestOutputHelper logger) + : base(logger) + { + } + + protected override GitContext CreateGitContext(string path, string committish = null) + => GitContext.Create(path, committish, GitContext.Engine.Disabled); + + protected override void ApplyGlobalProperties(IDictionary globalProperties) + => globalProperties["NBGV_GitEngine"] = EngineString; +} diff --git a/test/Nerdbank.GitVersioning.Tests/BuildIntegrationInProjectManagedTests.cs b/test/Nerdbank.GitVersioning.Tests/BuildIntegrationInProjectManagedTests.cs new file mode 100644 index 00000000..ea0e4a47 --- /dev/null +++ b/test/Nerdbank.GitVersioning.Tests/BuildIntegrationInProjectManagedTests.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Xunit; +using Xunit.Abstractions; + +[Trait("Engine", EngineString)] +[Collection("Build")] // msbuild sets current directory in the process, so we can't have it be concurrent with other build tests. +public class BuildIntegrationInProjectManagedTests : BuildIntegrationManagedTests +{ + public BuildIntegrationInProjectManagedTests(ITestOutputHelper logger) + : base(logger) + { + } + + /// + protected override void ApplyGlobalProperties(IDictionary globalProperties) + { + base.ApplyGlobalProperties(globalProperties); + globalProperties["NBGV_CacheMode"] = "None"; + } +} diff --git a/test/Nerdbank.GitVersioning.Tests/BuildIntegrationLibGit2Tests.cs b/test/Nerdbank.GitVersioning.Tests/BuildIntegrationLibGit2Tests.cs new file mode 100644 index 00000000..42884ff9 --- /dev/null +++ b/test/Nerdbank.GitVersioning.Tests/BuildIntegrationLibGit2Tests.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Nerdbank.GitVersioning; +using Xunit; +using Xunit.Abstractions; + +[Trait("Engine", EngineString)] +[Collection("Build")] // msbuild sets current directory in the process, so we can't have it be concurrent with other build tests. +public class BuildIntegrationLibGit2Tests : SomeGitBuildIntegrationTests +{ + private const string EngineString = "LibGit2"; + + public BuildIntegrationLibGit2Tests(ITestOutputHelper logger) + : base(logger) + { + } + + protected override GitContext CreateGitContext(string path, string committish = null) + => GitContext.Create(path, committish, GitContext.Engine.ReadWrite); + + protected override void ApplyGlobalProperties(IDictionary globalProperties) + => globalProperties["NBGV_GitEngine"] = EngineString; +} diff --git a/test/Nerdbank.GitVersioning.Tests/BuildIntegrationManagedTests.cs b/test/Nerdbank.GitVersioning.Tests/BuildIntegrationManagedTests.cs new file mode 100644 index 00000000..f7b839dc --- /dev/null +++ b/test/Nerdbank.GitVersioning.Tests/BuildIntegrationManagedTests.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Nerdbank.GitVersioning; +using Xunit; +using Xunit.Abstractions; + +[Trait("Engine", EngineString)] +[Collection("Build")] // msbuild sets current directory in the process, so we can't have it be concurrent with other build tests. +public class BuildIntegrationManagedTests : SomeGitBuildIntegrationTests +{ + protected const string EngineString = "Managed"; + + public BuildIntegrationManagedTests(ITestOutputHelper logger) + : base(logger) + { + } + + protected override GitContext CreateGitContext(string path, string committish = null) + => GitContext.Create(path, committish, GitContext.Engine.ReadOnly); + + protected override void ApplyGlobalProperties(IDictionary globalProperties) + => globalProperties["NBGV_GitEngine"] = EngineString; +} diff --git a/test/Nerdbank.GitVersioning.Tests/BuildIntegrationTests.cs b/test/Nerdbank.GitVersioning.Tests/BuildIntegrationTests.cs new file mode 100644 index 00000000..c694b991 --- /dev/null +++ b/test/Nerdbank.GitVersioning.Tests/BuildIntegrationTests.cs @@ -0,0 +1,661 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; +using System.Globalization; +using System.Reflection; +using System.Text; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Nerdbank.GitVersioning; +using Validation; +using Xunit; +using Xunit.Abstractions; +using Version = System.Version; + +public abstract class BuildIntegrationTests : RepoTestBase, IClassFixture +{ + protected const string GitVersioningPropsFileName = "Nerdbank.GitVersioning.props"; + protected const string GitVersioningTargetsFileName = "Nerdbank.GitVersioning.targets"; + protected const string UnitTestCloudBuildPrefix = "UnitTest: "; + protected static readonly string[] ToxicEnvironmentVariablePrefixes = new string[] + { + "APPVEYOR", + "SYSTEM_", + "BUILD_", + "NBGV_GitEngine", + }; + + protected BuildManager buildManager; + protected ProjectCollection projectCollection; + protected string projectDirectory; + protected ProjectRootElement testProject; + protected Dictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + // Set global properties to neutralize environment variables + // that might actually be defined by a CI that is building and running these tests. + { "PublicRelease", string.Empty }, + }; + + protected Random random; + + public BuildIntegrationTests(ITestOutputHelper logger) + : base(logger) + { + // MSBuildExtensions.LoadMSBuild will be called as part of the base constructor, because this class + // implements the IClassFixture interface. LoadMSBuild will load the MSBuild assemblies. + // This must happen _before_ any method that directly references types in the Microsoft.Build namespace has been called. + // Net, don't init MSBuild-related fields in the constructor, but in a method that is called by the constructor. + this.Init(); + } + + public static object[][] BuildNumberData + { + get + { + return new object[][] + { + new object[] { BuildNumberVersionOptionsBasis, CloudBuild.VSTS, "##vso[build.updatebuildnumber]{CLOUDBUILDNUMBER}" }, + }; + } + } + + public static object[][] CloudBuildVariablesData + { + get + { + return new object[][] + { + new object[] { CloudBuild.VSTS, "##vso[task.setvariable variable={NAME};]{VALUE}", false }, + new object[] { CloudBuild.VSTS, "##vso[task.setvariable variable={NAME};]{VALUE}", true }, + }; + } + } + + protected static VersionOptions BuildNumberVersionOptionsBasis + { + get + { + return new VersionOptions + { + Version = SemanticVersion.Parse("1.0"), + CloudBuild = new VersionOptions.CloudBuildOptions + { + BuildNumber = new VersionOptions.CloudBuildNumberOptions + { + Enabled = true, + IncludeCommitId = new VersionOptions.CloudBuildNumberCommitIdOptions(), + }, + }, + }; + } + } + + protected string CommitIdShort => this.Context.GitCommitId?.Substring(0, VersionOptions.DefaultGitCommitIdShortFixedLength); + + public static IEnumerable CloudBuildOfBranch(string branchName) + { + return new object[][] + { + new object[] { CloudBuild.AppVeyor.SetItem("APPVEYOR_REPO_BRANCH", branchName) }, + new object[] { CloudBuild.VSTS.SetItem("BUILD_SOURCEBRANCH", $"refs/heads/{branchName}") }, + new object[] { CloudBuild.VSTS.SetItem("BUILD_SOURCEBRANCH", $"refs/tags/{branchName}") }, + new object[] { CloudBuild.Teamcity.SetItem("BUILD_GIT_BRANCH", $"refs/heads/{branchName}") }, + new object[] { CloudBuild.Teamcity.SetItem("BUILD_GIT_BRANCH", $"refs/tags/{branchName}") }, + }; + } + + [Fact] + public async Task GetBuildVersion_Returns_BuildVersion_Property() + { + this.WriteVersionFile(); + this.InitializeSourceControl(); + BuildResults buildResult = await this.BuildAsync(); + Assert.Equal( + buildResult.BuildVersion, + buildResult.BuildResult.ResultsByTarget[Targets.GetBuildVersion].Items.Single().ItemSpec); + } + + [Fact] + public async Task GetBuildVersion_Without_Git() + { + this.WriteVersionFile("3.4"); + BuildResults buildResult = await this.BuildAsync(); + Assert.Equal("3.4", buildResult.BuildVersion); + Assert.Equal("3.4.0", buildResult.AssemblyInformationalVersion); + } + + [Fact] + public async Task GetBuildVersion_Without_Git_HighPrecisionAssemblyVersion() + { + this.WriteVersionFile(new VersionOptions + { + Version = SemanticVersion.Parse("3.4"), + AssemblyVersion = new VersionOptions.AssemblyVersionOptions + { + Precision = VersionOptions.VersionPrecision.Revision, + }, + }); + BuildResults buildResult = await this.BuildAsync(); + Assert.Equal("3.4", buildResult.BuildVersion); + Assert.Equal("3.4.0", buildResult.AssemblyInformationalVersion); + } + + // TODO: add key container test. + [Theory] + [InlineData("keypair.snk", false)] + [InlineData("public.snk", true)] + [InlineData("protectedPair.pfx", true)] + public async Task AssemblyInfo_HasKeyData(string keyFile, bool delaySigned) + { + TestUtilities.ExtractEmbeddedResource($@"Keys\{keyFile}", Path.Combine(this.projectDirectory, keyFile)); + this.testProject.AddProperty("SignAssembly", "true"); + this.testProject.AddProperty("AssemblyOriginatorKeyFile", keyFile); + this.testProject.AddProperty("DelaySign", delaySigned.ToString()); + + this.WriteVersionFile(); + BuildResults result = await this.BuildAsync(Targets.GenerateAssemblyNBGVVersionInfo, logVerbosity: LoggerVerbosity.Minimal); + string versionCsContent = File.ReadAllText( + Path.GetFullPath( + Path.Combine( + this.projectDirectory, + result.BuildResult.ProjectStateAfterBuild.GetPropertyValue("VersionSourceFile")))); + this.Logger.WriteLine(versionCsContent); + + SyntaxTree sourceFile = CSharpSyntaxTree.ParseText(versionCsContent); + SyntaxNode syntaxTree = await sourceFile.GetRootAsync(); + IEnumerable fields = syntaxTree.DescendantNodes().OfType(); + + var publicKeyField = (LiteralExpressionSyntax)fields.SingleOrDefault(f => f.Identifier.ValueText == "PublicKey")?.Initializer.Value; + var publicKeyTokenField = (LiteralExpressionSyntax)fields.SingleOrDefault(f => f.Identifier.ValueText == "PublicKeyToken")?.Initializer.Value; + if (Path.GetExtension(keyFile) == ".pfx") + { + // No support for PFX (yet anyway), since they're encrypted. + // Note for future: I think by this point, the user has typically already decrypted + // the PFX and stored the key pair in a key container. If we knew how to find which one, + // we could perhaps divert to that. + Assert.Null(publicKeyField); + Assert.Null(publicKeyTokenField); + } + else + { + Assert.Equal( + "002400000480000094000000060200000024000052534131000400000100010067cea773679e0ecc114b7e1d442466a90bf77c755811a0d3962a546ed716525b6508abf9f78df132ffd3fb75fe604b3961e39c52d5dfc0e6c1fb233cb4fb56b1a9e3141513b23bea2cd156cb2ef7744e59ba6b663d1f5b2f9449550352248068e85b61c68681a6103cad91b3bf7a4b50d2fabf97e1d97ac34db65b25b58cd0dc", + publicKeyField?.Token.ValueText); + Assert.Equal("ca2d1515679318f5", publicKeyTokenField?.Token.ValueText); + } + } + + /// + /// Emulate a project with an unsupported language, and verify that + /// one warning is emitted because the assembly info file couldn't be generated. + /// + [Fact] + public async Task AssemblyInfo_NotProducedWithoutCodeDomProvider() + { + ProjectPropertyGroupElement propertyGroup = this.testProject.CreatePropertyGroupElement(); + this.testProject.AppendChild(propertyGroup); + propertyGroup.AddProperty("Language", "NoCodeDOMProviderForThisLanguage"); + + this.WriteVersionFile(); + BuildResults result = await this.BuildAsync(Targets.GenerateAssemblyNBGVVersionInfo, logVerbosity: LoggerVerbosity.Minimal, assertSuccessfulBuild: false); + Assert.Equal(BuildResultCode.Failure, result.BuildResult.OverallResult); + string versionCsFilePath = Path.Combine(this.projectDirectory, result.BuildResult.ProjectStateAfterBuild.GetPropertyValue("VersionSourceFile")); + Assert.False(File.Exists(versionCsFilePath)); + Assert.Single(result.LoggedEvents.OfType()); + } + + /// + /// Emulate a project with an unsupported language, and verify that + /// no errors are emitted because the target is skipped. + /// + [Fact] + public async Task AssemblyInfo_Suppressed() + { + ProjectPropertyGroupElement propertyGroup = this.testProject.CreatePropertyGroupElement(); + this.testProject.AppendChild(propertyGroup); + propertyGroup.AddProperty("Language", "NoCodeDOMProviderForThisLanguage"); + propertyGroup.AddProperty(Properties.GenerateAssemblyVersionInfo, "false"); + + this.WriteVersionFile(); + BuildResults result = await this.BuildAsync(Targets.GenerateAssemblyNBGVVersionInfo, logVerbosity: LoggerVerbosity.Minimal); + string versionCsFilePath = Path.Combine(this.projectDirectory, result.BuildResult.ProjectStateAfterBuild.GetPropertyValue("VersionSourceFile")); + Assert.False(File.Exists(versionCsFilePath)); + Assert.Empty(result.LoggedEvents.OfType()); + Assert.Empty(result.LoggedEvents.OfType()); + } + + /// + /// Emulate a project with an unsupported language, and verify that + /// no errors are emitted because the target is skipped. + /// + [Fact] + public async Task AssemblyInfo_SuppressedImplicitlyByTargetExt() + { + ProjectPropertyGroupElement propertyGroup = this.testProject.CreatePropertyGroupElement(); + this.testProject.InsertAfterChild(propertyGroup, this.testProject.Imports.First()); // insert just after the Common.Targets import. + propertyGroup.AddProperty("Language", "NoCodeDOMProviderForThisLanguage"); + propertyGroup.AddProperty("TargetExt", ".notdll"); + + this.WriteVersionFile(); + BuildResults result = await this.BuildAsync(Targets.GenerateAssemblyNBGVVersionInfo, logVerbosity: LoggerVerbosity.Minimal); + string versionCsFilePath = Path.Combine(this.projectDirectory, result.BuildResult.ProjectStateAfterBuild.GetPropertyValue("VersionSourceFile")); + Assert.False(File.Exists(versionCsFilePath)); + Assert.Empty(result.LoggedEvents.OfType()); + Assert.Empty(result.LoggedEvents.OfType()); + } + + protected static Version GetExpectedAssemblyVersion(VersionOptions versionOptions, Version version) + { + // Function should be very similar to VersionOracle.GetAssemblyVersion() + Version assemblyVersion = (versionOptions?.AssemblyVersion?.Version ?? versionOptions.Version.Version).EnsureNonNegativeComponents(); + + if (versionOptions?.AssemblyVersion?.Version is null) + { + VersionOptions.VersionPrecision precision = versionOptions?.AssemblyVersion?.Precision ?? VersionOptions.DefaultVersionPrecision; + assemblyVersion = version; + + assemblyVersion = new Version( + assemblyVersion.Major, + precision >= VersionOptions.VersionPrecision.Minor ? assemblyVersion.Minor : 0, + precision >= VersionOptions.VersionPrecision.Build ? assemblyVersion.Build : 0, + precision >= VersionOptions.VersionPrecision.Revision ? assemblyVersion.Revision : 0); + } + + return assemblyVersion; + } + + protected static RestoreEnvironmentVariables ApplyEnvironmentVariables(IReadOnlyDictionary variables) + { + Requires.NotNull(variables, nameof(variables)); + + var oldValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (KeyValuePair variable in variables) + { + oldValues[variable.Key] = Environment.GetEnvironmentVariable(variable.Key); + Environment.SetEnvironmentVariable(variable.Key, variable.Value); + } + + return new RestoreEnvironmentVariables(oldValues); + } + + protected static string GetSemVerAppropriatePrereleaseTag(VersionOptions versionOptions) + { + return versionOptions.NuGetPackageVersionOrDefault.SemVer == 1 + ? versionOptions.Version.Prerelease?.Replace('.', '-') + : versionOptions.Version.Prerelease; + } + + protected void AssertStandardProperties(VersionOptions versionOptions, BuildResults buildResult, string relativeProjectDirectory = null) + { + int versionHeight = this.GetVersionHeight(relativeProjectDirectory); + Version idAsVersion = this.GetVersion(relativeProjectDirectory); + string commitIdShort = this.CommitIdShort; + Version version = this.GetVersion(relativeProjectDirectory); + Version assemblyVersion = GetExpectedAssemblyVersion(versionOptions, version); + IEnumerable additionalBuildMetadata = from item in buildResult.BuildResult.ProjectStateAfterBuild.GetItems("BuildMetadata") + select item.EvaluatedInclude; + string expectedBuildMetadata = $"+{commitIdShort}"; + if (additionalBuildMetadata.Any()) + { + expectedBuildMetadata += "." + string.Join(".", additionalBuildMetadata); + } + + string expectedBuildMetadataWithoutCommitId = additionalBuildMetadata.Any() ? $"+{string.Join(".", additionalBuildMetadata)}" : string.Empty; + string optionalFourthComponent = versionOptions.VersionHeightPosition == SemanticVersion.Position.Revision ? $".{idAsVersion.Revision}" : string.Empty; + + Assert.Equal($"{version}", buildResult.AssemblyFileVersion); + Assert.Equal($"{idAsVersion.Major}.{idAsVersion.Minor}.{idAsVersion.Build}{optionalFourthComponent}{versionOptions.Version.Prerelease}{expectedBuildMetadata}", buildResult.AssemblyInformationalVersion); + + // The assembly version property should always have four integer components to it, + // per bug https://github.com/dotnet/Nerdbank.GitVersioning/issues/26 + Assert.Equal($"{assemblyVersion.Major}.{assemblyVersion.Minor}.{assemblyVersion.Build}.{assemblyVersion.Revision}", buildResult.AssemblyVersion); + + Assert.Equal(idAsVersion.Build.ToString(), buildResult.BuildNumber); + Assert.Equal($"{version}", buildResult.BuildVersion); + Assert.Equal($"{idAsVersion.Major}.{idAsVersion.Minor}.{idAsVersion.Build}", buildResult.BuildVersion3Components); + Assert.Equal(idAsVersion.Build.ToString(), buildResult.BuildVersionNumberComponent); + Assert.Equal($"{idAsVersion.Major}.{idAsVersion.Minor}.{idAsVersion.Build}", buildResult.BuildVersionSimple); + Assert.Equal(this.LibGit2Repository.Head.Tip.Id.Sha, buildResult.GitCommitId); + Assert.Equal(this.LibGit2Repository.Head.Tip.Author.When.UtcTicks.ToString(CultureInfo.InvariantCulture), buildResult.GitCommitDateTicks); + Assert.Equal(commitIdShort, buildResult.GitCommitIdShort); + Assert.Equal(versionHeight.ToString(), buildResult.GitVersionHeight); + Assert.Equal($"{version.Major}.{version.Minor}", buildResult.MajorMinorVersion); + Assert.Equal(versionOptions.Version.Prerelease, buildResult.PrereleaseVersion); + Assert.Equal(expectedBuildMetadata, buildResult.SemVerBuildSuffix); + + string GetPkgVersionSuffix(bool useSemVer2) + { + string pkgVersionSuffix = buildResult.PublicRelease ? string.Empty : $"-g{commitIdShort}"; + if (useSemVer2) + { + pkgVersionSuffix += expectedBuildMetadataWithoutCommitId; + } + + return pkgVersionSuffix; + } + + // NuGet is now SemVer 2.0 and will pass additional build metadata if provided + string nugetPkgVersionSuffix = GetPkgVersionSuffix(useSemVer2: versionOptions?.NuGetPackageVersionOrDefault.SemVer == 2); + Assert.Equal($"{idAsVersion.Major}.{idAsVersion.Minor}.{idAsVersion.Build}{GetSemVerAppropriatePrereleaseTag(versionOptions)}{nugetPkgVersionSuffix}", buildResult.NuGetPackageVersion); + + // Chocolatey only supports SemVer 1.0 + string chocolateyPkgVersionSuffix = GetPkgVersionSuffix(useSemVer2: false); + Assert.Equal($"{idAsVersion.Major}.{idAsVersion.Minor}.{idAsVersion.Build}{GetSemVerAppropriatePrereleaseTag(versionOptions)}{chocolateyPkgVersionSuffix}", buildResult.ChocolateyPackageVersion); + + VersionOptions.CloudBuildNumberOptions buildNumberOptions = versionOptions.CloudBuildOrDefault.BuildNumberOrDefault; + if (buildNumberOptions.EnabledOrDefault) + { + VersionOptions.CloudBuildNumberCommitIdOptions commitIdOptions = buildNumberOptions.IncludeCommitIdOrDefault; + var buildNumberSemVer = SemanticVersion.Parse(buildResult.CloudBuildNumber); + bool hasCommitData = commitIdOptions.WhenOrDefault == VersionOptions.CloudBuildNumberCommitWhen.Always + || (commitIdOptions.WhenOrDefault == VersionOptions.CloudBuildNumberCommitWhen.NonPublicReleaseOnly && !buildResult.PublicRelease); + Version expectedVersion = hasCommitData && commitIdOptions.WhereOrDefault == VersionOptions.CloudBuildNumberCommitWhere.FourthVersionComponent + ? idAsVersion + : new Version(version.Major, version.Minor, version.Build); + Assert.Equal(expectedVersion, buildNumberSemVer.Version); + Assert.Equal(buildResult.PrereleaseVersion, buildNumberSemVer.Prerelease); + string expectedBuildNumberMetadata = hasCommitData && commitIdOptions.WhereOrDefault == VersionOptions.CloudBuildNumberCommitWhere.BuildMetadata + ? $"+{commitIdShort}" + : string.Empty; + if (additionalBuildMetadata.Any()) + { + expectedBuildNumberMetadata = expectedBuildNumberMetadata.Length == 0 + ? "+" + string.Join(".", additionalBuildMetadata) + : expectedBuildNumberMetadata + "." + string.Join(".", additionalBuildMetadata); + } + + Assert.Equal(expectedBuildNumberMetadata, buildNumberSemVer.BuildMetadata); + } + else + { + Assert.Equal(string.Empty, buildResult.CloudBuildNumber); + } + } + + protected async Task BuildAsync(string target = Targets.GetBuildVersion, LoggerVerbosity logVerbosity = LoggerVerbosity.Detailed, bool assertSuccessfulBuild = true) + { + var eventLogger = new MSBuildLogger { Verbosity = LoggerVerbosity.Minimal }; + var loggers = new ILogger[] { eventLogger }; + this.testProject.Save(); // persist generated project on disk for analysis + this.ApplyGlobalProperties(this.globalProperties); + BuildResult buildResult = await this.buildManager.BuildAsync( + this.Logger, + this.projectCollection, + this.testProject, + target, + this.globalProperties, + logVerbosity, + loggers); + var result = new BuildResults(buildResult, eventLogger.LoggedEvents); + this.Logger.WriteLine(result.ToString()); + if (assertSuccessfulBuild) + { + Assert.Equal(BuildResultCode.Success, buildResult.OverallResult); + } + + return result; + } + + protected ProjectRootElement CreateNativeProjectRootElement(string projectDirectory, string projectName) + { + using (var reader = XmlReader.Create(Assembly.GetExecutingAssembly().GetManifestResourceStream($"{ThisAssembly.RootNamespace}.test.vcprj"))) + { + var pre = ProjectRootElement.Create(reader, this.projectCollection); + pre.FullPath = Path.Combine(projectDirectory, projectName); + pre.InsertAfterChild(pre.CreateImportElement(Path.Combine(this.RepoPath, GitVersioningPropsFileName)), null); + pre.AddImport(Path.Combine(this.RepoPath, GitVersioningTargetsFileName)); + return pre; + } + } + + protected ProjectRootElement CreateProjectRootElement(string projectDirectory, string projectName) + { + using (var reader = XmlReader.Create(Assembly.GetExecutingAssembly().GetManifestResourceStream($"{ThisAssembly.RootNamespace}.test.prj"))) + { + var pre = ProjectRootElement.Create(reader, this.projectCollection); + pre.FullPath = Path.Combine(projectDirectory, projectName); + pre.InsertAfterChild(pre.CreateImportElement(Path.Combine(this.RepoPath, GitVersioningPropsFileName)), null); + pre.AddImport(Path.Combine(this.RepoPath, GitVersioningTargetsFileName)); + return pre; + } + } + + protected void MakeItAVBProject() + { + ProjectImportElement csharpImport = this.testProject.Imports.Single(i => i.Project.Contains("CSharp")); + csharpImport.Project = "$(MSBuildToolsPath)/Microsoft.VisualBasic.targets"; + ProjectPropertyElement isVbProperty = this.testProject.Properties.Single(p => p.Name == "IsVB"); + isVbProperty.Value = "true"; + } + + protected abstract void ApplyGlobalProperties(IDictionary globalProperties); + + /// + protected override void Dispose(bool disposing) + { + Environment.SetEnvironmentVariable("_NBGV_UnitTest", string.Empty); + base.Dispose(disposing); + } + + private void LoadTargetsIntoProjectCollection() + { + string prefix = $"{ThisAssembly.RootNamespace}.Targets."; + + IEnumerable streamNames = from name in Assembly.GetExecutingAssembly().GetManifestResourceNames() + where name.StartsWith(prefix, StringComparison.Ordinal) + select name; + foreach (string name in streamNames) + { + using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(name)) + { + var targetsFile = ProjectRootElement.Create(XmlReader.Create(stream), this.projectCollection); + targetsFile.FullPath = Path.Combine(this.RepoPath, name.Substring(prefix.Length)); + targetsFile.Save(); // persist files on disk + } + } + } + + private void Init() + { + int seed = (int)DateTime.Now.Ticks; + this.random = new Random(seed); + this.Logger.WriteLine("Random seed: {0}", seed); + this.buildManager = new BuildManager(); + this.projectCollection = new ProjectCollection(); + this.projectDirectory = Path.Combine(this.RepoPath, "projdir"); + Directory.CreateDirectory(this.projectDirectory); + this.LoadTargetsIntoProjectCollection(); + this.testProject = this.CreateProjectRootElement(this.projectDirectory, "test.prj"); + this.globalProperties.Add("NerdbankGitVersioningTasksPath", Environment.CurrentDirectory + "\\"); + Environment.SetEnvironmentVariable("_NBGV_UnitTest", "true"); + + // Sterilize the test of any environment variables. + foreach (System.Collections.DictionaryEntry variable in Environment.GetEnvironmentVariables()) + { + string name = (string)variable.Key; + if (ToxicEnvironmentVariablePrefixes.Any(toxic => name.StartsWith(toxic, StringComparison.OrdinalIgnoreCase))) + { + this.globalProperties[name] = string.Empty; + } + } + } + + protected struct RestoreEnvironmentVariables : IDisposable + { + private readonly IReadOnlyDictionary applyVariables; + + internal RestoreEnvironmentVariables(IReadOnlyDictionary applyVariables) + { + this.applyVariables = applyVariables; + } + + public void Dispose() + { + ApplyEnvironmentVariables(this.applyVariables); + } + } + + protected static class CloudBuild + { + public static readonly ImmutableDictionary SuppressEnvironment = ImmutableDictionary.Empty + + // AppVeyor + .Add("APPVEYOR", string.Empty) + .Add("APPVEYOR_REPO_TAG", string.Empty) + .Add("APPVEYOR_REPO_TAG_NAME", string.Empty) + .Add("APPVEYOR_PULL_REQUEST_NUMBER", string.Empty) + + // VSTS + .Add("SYSTEM_TEAMPROJECTID", string.Empty) + .Add("BUILD_SOURCEBRANCH", string.Empty) + + // Teamcity + .Add("BUILD_VCS_NUMBER", string.Empty) + .Add("BUILD_GIT_BRANCH", string.Empty); + + public static readonly ImmutableDictionary VSTS = SuppressEnvironment + .SetItem("SYSTEM_TEAMPROJECTID", "1"); + + public static readonly ImmutableDictionary AppVeyor = SuppressEnvironment + .SetItem("APPVEYOR", "True"); + + public static readonly ImmutableDictionary Teamcity = SuppressEnvironment + .SetItem("BUILD_VCS_NUMBER", "1"); + } + + protected static class Targets + { + internal const string Build = "Build"; + internal const string GetBuildVersion = "GetBuildVersion"; + internal const string GetNuGetPackageVersion = "GetNuGetPackageVersion"; + internal const string GenerateAssemblyNBGVVersionInfo = "GenerateAssemblyNBGVVersionInfo"; + internal const string GenerateNativeNBGVVersionInfo = "GenerateNativeNBGVVersionInfo"; + } + + protected static class Properties + { + internal const string GenerateAssemblyVersionInfo = "GenerateAssemblyVersionInfo"; + } + + protected class BuildResults + { + internal BuildResults(BuildResult buildResult, IReadOnlyList loggedEvents) + { + Requires.NotNull(buildResult, nameof(buildResult)); + this.BuildResult = buildResult; + this.LoggedEvents = loggedEvents; + } + + public BuildResult BuildResult { get; private set; } + + public IReadOnlyList LoggedEvents { get; private set; } + + public bool PublicRelease => string.Equals("true", this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("PublicRelease"), StringComparison.OrdinalIgnoreCase); + + public string BuildNumber => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("BuildNumber"); + + public string GitCommitId => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("GitCommitId"); + + public string BuildVersion => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("BuildVersion"); + + public string BuildVersionSimple => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("BuildVersionSimple"); + + public string PrereleaseVersion => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("PrereleaseVersion"); + + public string MajorMinorVersion => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("MajorMinorVersion"); + + public string BuildVersionNumberComponent => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("BuildVersionNumberComponent"); + + public string GitCommitIdShort => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("GitCommitIdShort"); + + public string GitCommitDateTicks => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("GitCommitDateTicks"); + + public string GitVersionHeight => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("GitVersionHeight"); + + public string SemVerBuildSuffix => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("SemVerBuildSuffix"); + + public string BuildVersion3Components => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("BuildVersion3Components"); + + public string AssemblyInformationalVersion => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("AssemblyInformationalVersion"); + + public string AssemblyFileVersion => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("AssemblyFileVersion"); + + public string AssemblyVersion => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("AssemblyVersion"); + + public string NuGetPackageVersion => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("NuGetPackageVersion"); + + public string ChocolateyPackageVersion => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("ChocolateyPackageVersion"); + + public string CloudBuildNumber => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("CloudBuildNumber"); + + public string AssemblyName => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("AssemblyName"); + + public string AssemblyTitle => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("AssemblyTitle"); + + public string AssemblyProduct => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("AssemblyProduct"); + + public string AssemblyCompany => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("AssemblyCompany"); + + public string AssemblyCopyright => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("AssemblyCopyright"); + + public string AssemblyConfiguration => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("Configuration"); + + public string RootNamespace => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("RootNamespace"); + + public string GitBuildVersion => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("GitBuildVersion"); + + public string GitBuildVersionSimple => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("GitBuildVersionSimple"); + + public string GitAssemblyInformationalVersion => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("GitAssemblyInformationalVersion"); + + // Just a sampling of other properties optionally set in cloud build. + public string NBGV_GitCommitIdShort => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("NBGV_GitCommitIdShort"); + + public string NBGV_NuGetPackageVersion => this.BuildResult.ProjectStateAfterBuild.GetPropertyValue("NBGV_NuGetPackageVersion"); + + public override string ToString() + { + var sb = new StringBuilder(); + + foreach (PropertyInfo property in this.GetType().GetRuntimeProperties().OrderBy(p => p.Name, StringComparer.OrdinalIgnoreCase)) + { + if (property.DeclaringType == this.GetType() && property.Name != nameof(this.BuildResult)) + { + sb.AppendLine($"{property.Name} = {property.GetValue(this)}"); + } + } + + return sb.ToString(); + } + } + + private class MSBuildLogger : ILogger + { + public string Parameters { get; set; } + + public LoggerVerbosity Verbosity { get; set; } + + public List LoggedEvents { get; } = new List(); + + public void Initialize(IEventSource eventSource) + { + eventSource.AnyEventRaised += this.EventSource_AnyEventRaised; + } + + public void Shutdown() + { + } + + private void EventSource_AnyEventRaised(object sender, BuildEventArgs e) + { + this.LoggedEvents.Add(e); + } + } +} diff --git a/src/NerdBank.GitVersioning.Tests/FilterPathTests.cs b/test/Nerdbank.GitVersioning.Tests/FilterPathTests.cs similarity index 94% rename from src/NerdBank.GitVersioning.Tests/FilterPathTests.cs rename to test/Nerdbank.GitVersioning.Tests/FilterPathTests.cs index b6c5a6da..22016971 100644 --- a/src/NerdBank.GitVersioning.Tests/FilterPathTests.cs +++ b/test/Nerdbank.GitVersioning.Tests/FilterPathTests.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; using System.Runtime.InteropServices; using Nerdbank.GitVersioning; using Xunit; @@ -107,10 +110,10 @@ public void NonMatchingPathsAreNotExcludedCaseSensitive(string pathSpec, string [Fact] public void InvalidPathspecsThrow() { - Assert.Throws(() => new FilterPath(null, "")); - Assert.Throws(() => new FilterPath("", "")); - Assert.Throws(() => new FilterPath(":?", "")); - Assert.Throws(() => new FilterPath("../foo.txt", "")); + Assert.Throws(() => new FilterPath(null, string.Empty)); + Assert.Throws(() => new FilterPath(string.Empty, string.Empty)); + Assert.Throws(() => new FilterPath(":?", string.Empty)); + Assert.Throws(() => new FilterPath("../foo.txt", string.Empty)); Assert.Throws(() => new FilterPath(".././a/../../foo.txt", "foo")); } diff --git a/src/NerdBank.GitVersioning.Tests/GitContextTests.cs b/test/Nerdbank.GitVersioning.Tests/GitContextTests.cs similarity index 88% rename from src/NerdBank.GitVersioning.Tests/GitContextTests.cs rename to test/Nerdbank.GitVersioning.Tests/GitContextTests.cs index ccb93142..36093811 100644 --- a/src/NerdBank.GitVersioning.Tests/GitContextTests.cs +++ b/test/Nerdbank.GitVersioning.Tests/GitContextTests.cs @@ -1,9 +1,15 @@ -using System; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; using System.IO; using Nerdbank.GitVersioning; using Xunit; using Xunit.Abstractions; +#pragma warning disable SA1402 // File may only contain a single type +#pragma warning disable SA1649 // File name should match first type name + [Trait("Engine", "Managed")] public class GitContextManagedTests : GitContextTests { @@ -12,8 +18,9 @@ public GitContextManagedTests(ITestOutputHelper logger) { } + /// protected override GitContext CreateGitContext(string path, string committish = null) - => GitContext.Create(path, committish, writable: false); + => GitContext.Create(path, committish, engine: GitContext.Engine.ReadOnly); } [Trait("Engine", "LibGit2")] @@ -24,13 +31,15 @@ public GitContextLibGit2Tests(ITestOutputHelper logger) { } + /// protected override GitContext CreateGitContext(string path, string committish = null) - => GitContext.Create(path, committish, writable: true); + => GitContext.Create(path, committish, engine: GitContext.Engine.ReadWrite); } public abstract class GitContextTests : RepoTestBase { - protected GitContextTests(ITestOutputHelper logger) : base(logger) + protected GitContextTests(ITestOutputHelper logger) + : base(logger) { this.InitializeSourceControl(); this.AddCommits(); @@ -166,7 +175,7 @@ public void SelectDirectory_SubDir() [Fact] public void GetVersion_PackedHead() { - using var expandedRepo = TestUtilities.ExtractRepoArchive("PackedHeadRef"); + using TestUtilities.ExpandedRepo expandedRepo = TestUtilities.ExtractRepoArchive("PackedHeadRef"); this.Context = this.CreateGitContext(Path.Combine(expandedRepo.RepoPath)); var oracle = new VersionOracle(this.Context); Assert.Equal("1.0.1", oracle.SimpleVersion.ToString()); @@ -177,7 +186,7 @@ public void GetVersion_PackedHead() [Fact] public void HeadCanonicalName_PackedHead() { - using var expandedRepo = TestUtilities.ExtractRepoArchive("PackedHeadRef"); + using TestUtilities.ExpandedRepo expandedRepo = TestUtilities.ExtractRepoArchive("PackedHeadRef"); this.Context = this.CreateGitContext(Path.Combine(expandedRepo.RepoPath)); Assert.Equal("refs/heads/main", this.Context.HeadCanonicalName); } diff --git a/src/NerdBank.GitVersioning.Tests/Keys/keypair.snk b/test/Nerdbank.GitVersioning.Tests/Keys/keypair.snk similarity index 100% rename from src/NerdBank.GitVersioning.Tests/Keys/keypair.snk rename to test/Nerdbank.GitVersioning.Tests/Keys/keypair.snk diff --git a/src/NerdBank.GitVersioning.Tests/Keys/protectedPair.pfx b/test/Nerdbank.GitVersioning.Tests/Keys/protectedPair.pfx similarity index 100% rename from src/NerdBank.GitVersioning.Tests/Keys/protectedPair.pfx rename to test/Nerdbank.GitVersioning.Tests/Keys/protectedPair.pfx diff --git a/src/NerdBank.GitVersioning.Tests/Keys/public.snk b/test/Nerdbank.GitVersioning.Tests/Keys/public.snk similarity index 100% rename from src/NerdBank.GitVersioning.Tests/Keys/public.snk rename to test/Nerdbank.GitVersioning.Tests/Keys/public.snk diff --git a/src/NerdBank.GitVersioning.Tests/LibGit2GitExtensionsTests.cs b/test/Nerdbank.GitVersioning.Tests/LibGit2GitExtensionsTests.cs similarity index 90% rename from src/NerdBank.GitVersioning.Tests/LibGit2GitExtensionsTests.cs rename to test/Nerdbank.GitVersioning.Tests/LibGit2GitExtensionsTests.cs index 41d92807..78952c85 100644 --- a/src/NerdBank.GitVersioning.Tests/LibGit2GitExtensionsTests.cs +++ b/test/Nerdbank.GitVersioning.Tests/LibGit2GitExtensionsTests.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; using System.Buffers.Binary; using System.Collections.Generic; using System.Diagnostics; @@ -14,16 +17,14 @@ public class LibGit2GitExtensionsTests : RepoTestBase { - public LibGit2GitExtensionsTests(ITestOutputHelper Logger) - : base(Logger) + public LibGit2GitExtensionsTests(ITestOutputHelper logger) + : base(logger) { this.InitializeSourceControl(); } protected new LibGit2Context Context => (LibGit2Context)base.Context; - protected override GitContext CreateGitContext(string path, string committish = null) => GitContext.Create(path, committish, writable: true); - [Fact] public void GetHeight_EmptyRepo() { @@ -37,9 +38,9 @@ public void GetHeight_EmptyRepo() [Fact] public void GetHeight_SinglePath() { - var first = this.LibGit2Repository.Commit("First", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); - var second = this.LibGit2Repository.Commit("Second", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); - var third = this.LibGit2Repository.Commit("Third", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); + Commit first = this.LibGit2Repository.Commit("First", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); + Commit second = this.LibGit2Repository.Commit("Second", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); + Commit third = this.LibGit2Repository.Commit("Third", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); this.SetContextToHead(); Assert.Equal(3, LibGit2GitExtensions.GetHeight(this.Context)); Assert.Equal(3, LibGit2GitExtensions.GetHeight(this.Context, c => true)); @@ -51,9 +52,9 @@ public void GetHeight_SinglePath() [Fact] public void GetHeight_Merge() { - var firstCommit = this.LibGit2Repository.Commit("First", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); - var anotherBranch = this.LibGit2Repository.CreateBranch("another"); - var secondCommit = this.LibGit2Repository.Commit("Second", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); + Commit firstCommit = this.LibGit2Repository.Commit("First", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); + Branch anotherBranch = this.LibGit2Repository.CreateBranch("another"); + Commit secondCommit = this.LibGit2Repository.Commit("Second", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); Commands.Checkout(this.LibGit2Repository, anotherBranch); Commit[] branchCommits = new Commit[5]; for (int i = 1; i <= branchCommits.Length; i++) @@ -88,18 +89,18 @@ public void GetCommitsFromVersion_WithPathFilters() { new FilterPath("./", relativeDirectory), new FilterPath(":^/some-sub-dir/ignore.txt", relativeDirectory), - new FilterPath(":^excluded-dir", relativeDirectory) + new FilterPath(":^excluded-dir", relativeDirectory), }; commitsAt121.Add(this.WriteVersionFile(versionData, relativeDirectory)); // Commit touching excluded path does not affect version height - var ignoredFilePath = Path.Combine(this.RepoPath, relativeDirectory, "ignore.txt"); + string ignoredFilePath = Path.Combine(this.RepoPath, relativeDirectory, "ignore.txt"); File.WriteAllText(ignoredFilePath, "hello"); Commands.Stage(this.LibGit2Repository, ignoredFilePath); commitsAt121.Add(this.LibGit2Repository.Commit("Add excluded file", this.Signer, this.Signer)); // Commit touching both excluded and included path does affect height - var includedFilePath = Path.Combine(this.RepoPath, relativeDirectory, "another-file.txt"); + string includedFilePath = Path.Combine(this.RepoPath, relativeDirectory, "another-file.txt"); File.WriteAllText(includedFilePath, "hello"); File.WriteAllText(ignoredFilePath, "changed"); Commands.Stage(this.LibGit2Repository, includedFilePath); @@ -107,7 +108,7 @@ public void GetCommitsFromVersion_WithPathFilters() commitsAt122.Add(this.LibGit2Repository.Commit("Change both excluded and included file", this.Signer, this.Signer)); // Commit touching excluded directory does not affect version height - var fileInExcludedDirPath = Path.Combine(this.RepoPath, relativeDirectory, "excluded-dir", "ignore.txt"); + string fileInExcludedDirPath = Path.Combine(this.RepoPath, relativeDirectory, "excluded-dir", "ignore.txt"); Directory.CreateDirectory(Path.GetDirectoryName(fileInExcludedDirPath)); File.WriteAllText(fileInExcludedDirPath, "hello"); Commands.Stage(this.LibGit2Repository, fileInExcludedDirPath); @@ -183,7 +184,7 @@ public void GetVersionHeight_ProgressAndReset(string version1, string version2, public void GetIdAsVersion_ReadsMajorMinorFromVersionTxt() { this.WriteVersionFile("4.8"); - var firstCommit = this.LibGit2Repository.Commits.First(); + Commit firstCommit = this.LibGit2Repository.Commits.First(); Version v1 = this.GetVersion(committish: firstCommit.Sha); Assert.Equal(4, v1.Major); @@ -194,7 +195,7 @@ public void GetIdAsVersion_ReadsMajorMinorFromVersionTxt() public void GetIdAsVersion_ReadsMajorMinorFromVersionTxtInSubdirectory() { this.WriteVersionFile("4.8", relativeDirectory: "foo/bar"); - var firstCommit = this.LibGit2Repository.Commits.First(); + Commit firstCommit = this.LibGit2Repository.Commits.First(); Version v1 = this.GetVersion("foo/bar", firstCommit.Sha); Assert.Equal(4, v1.Major); @@ -205,7 +206,7 @@ public void GetIdAsVersion_ReadsMajorMinorFromVersionTxtInSubdirectory() public void GetIdAsVersion_MissingVersionTxt() { this.AddCommits(); - var firstCommit = this.LibGit2Repository.Commits.First(); + Commit firstCommit = this.LibGit2Repository.Commits.First(); Version v1 = this.GetVersion(committish: firstCommit.Sha); Assert.Equal(0, v1.Major); @@ -247,7 +248,7 @@ public void GetIdAsVersion_VersionFileNeverCheckedIn_2Ints() public void GetIdAsVersion_VersionFileChangedOnDisk() { this.WriteVersionFile(); - var versionChangeCommit = this.LibGit2Repository.Commits.First(); + Commit versionChangeCommit = this.LibGit2Repository.Commits.First(); this.AddCommits(); // Verify that we're seeing the original version. @@ -363,7 +364,7 @@ public void GetIdAsVersion_Roundtrip_UnstableOffset(int startingOffset, int offs this.Logger.WriteLine($"Commit {commits[i + 1].Id.Sha.Substring(0, 8)} as version: {versions[i + 1]}"); // Find the commits we just wrote while they are still at the tip of the branch. - var matchingCommits = LibGit2GitExtensions.GetCommitsFromVersion(this.Context, versions[i]); + IEnumerable matchingCommits = LibGit2GitExtensions.GetCommitsFromVersion(this.Context, versions[i]); Assert.Contains(commits[i], matchingCommits); matchingCommits = LibGit2GitExtensions.GetCommitsFromVersion(this.Context, versions[i + 1]); Assert.Contains(commits[i + 1], matchingCommits); @@ -431,7 +432,7 @@ public void GetIdAsVersion_Roundtrip_WithSubdirectoryVersionFiles() public void GetIdAsVersion_FitsInsideCompilerConstraints() { this.WriteVersionFile("2.5"); - var firstCommit = this.LibGit2Repository.Commits.First(); + Commit firstCommit = this.LibGit2Repository.Commits.First(); Version version = this.GetVersion(committish: firstCommit.Sha); this.Logger.WriteLine(version.ToString()); @@ -445,11 +446,11 @@ public void GetIdAsVersion_FitsInsideCompilerConstraints() [Fact] public void GetIdAsVersion_MigrationFromVersionTxtToJson() { - var txtCommit = this.WriteVersionTxtFile("4.8"); + Commit txtCommit = this.WriteVersionTxtFile("4.8"); // Delete the version.txt file so the system writes the version.json file. File.Delete(Path.Combine(this.RepoPath, "version.txt")); - var jsonCommit = this.WriteVersionFile("4.8"); + Commit jsonCommit = this.WriteVersionFile("4.8"); Assert.True(File.Exists(Path.Combine(this.RepoPath, "version.json"))); Version v1 = this.GetVersion(committish: txtCommit.Sha); @@ -466,20 +467,23 @@ public void GetIdAsVersion_MigrationFromVersionTxtToJson() [SkippableFact(Skip = "It fails already.")] // Skippable, only run test on specific machine public void TestBiggerRepo() { - var testBiggerRepoPath = @"D:\git\NerdBank.GitVersioning"; + string testBiggerRepoPath = @"D:\git\Nerdbank.GitVersioning"; Skip.If(!Directory.Exists(testBiggerRepoPath)); using var largeRepo = new Repository(testBiggerRepoPath); - foreach (var commit in largeRepo.Head.Commits) + foreach (Commit commit in largeRepo.Head.Commits) { - var version = this.GetVersion("src", commit.Sha); + Version version = this.GetVersion("src", commit.Sha); this.Logger.WriteLine($"commit {commit.Id} got version {version}"); using var context = LibGit2Context.Create("src", commit.Sha); - var backAgain = LibGit2GitExtensions.GetCommitFromVersion(context, version); + Commit backAgain = LibGit2GitExtensions.GetCommitFromVersion(context, version); Assert.Equal(commit, backAgain); } } + /// + protected override GitContext CreateGitContext(string path, string committish = null) => GitContext.Create(path, committish, engine: GitContext.Engine.ReadWrite); + private Commit[] CommitsWithVersion(string majorMinorVersion) { this.WriteVersionFile(majorMinorVersion); diff --git a/src/NerdBank.GitVersioning.Tests/MSBuildExtensions.cs b/test/Nerdbank.GitVersioning.Tests/MSBuildExtensions.cs similarity index 72% rename from src/NerdBank.GitVersioning.Tests/MSBuildExtensions.cs rename to test/Nerdbank.GitVersioning.Tests/MSBuildExtensions.cs index 5edbbd07..d77f7ba5 100644 --- a/src/NerdBank.GitVersioning.Tests/MSBuildExtensions.cs +++ b/test/Nerdbank.GitVersioning.Tests/MSBuildExtensions.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + using Microsoft.Build.Construction; using Microsoft.Build.Evaluation; using Microsoft.Build.Execution; @@ -14,16 +12,29 @@ internal static class MSBuildExtensions { - private static readonly object loadLock = new object(); + private static readonly object LoadLock = new object(); private static bool loaded; internal static void LoadMSBuild() { - lock (loadLock) + lock (LoadLock) { if (!loaded) { +#if NET + if (IntPtr.Size == 4) + { + // 32-bit .NET runtime requires special code to find the x86 SDK (where MSBuild is). + MSBuildLocator.RegisterMSBuildPath(@"C:\Program Files (x86)\dotnet\sdk\7.0.203"); + } + else + { + MSBuildLocator.RegisterDefaults(); + } +#else MSBuildLocator.RegisterDefaults(); +#endif + loaded = true; } } @@ -48,7 +59,7 @@ internal static async Task BuildAsync(this BuildManager buildManage buildManager.BeginBuild(parameters); - var result = await buildManager.BuildAsync(brd); + BuildResult result = await buildManager.BuildAsync(brd); buildManager.EndBuild(); @@ -61,7 +72,7 @@ internal static Task BuildAsync(this BuildManager buildManager, Bui Requires.NotNull(buildRequestData, nameof(buildRequestData)); var tcs = new TaskCompletionSource(); - var submission = buildManager.PendBuildRequest(buildRequestData); + BuildSubmission submission = buildManager.PendBuildRequest(buildRequestData); submission.ExecuteAsync(s => tcs.SetResult(s.BuildResult), null); return tcs.Task; } diff --git a/test/Nerdbank.GitVersioning.Tests/MSBuildFixture.cs b/test/Nerdbank.GitVersioning.Tests/MSBuildFixture.cs new file mode 100644 index 00000000..97191ad5 --- /dev/null +++ b/test/Nerdbank.GitVersioning.Tests/MSBuildFixture.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +public class MSBuildFixture +{ + public MSBuildFixture() + { + MSBuildExtensions.LoadMSBuild(); + } +} diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/.gitattributes b/test/Nerdbank.GitVersioning.Tests/ManagedGit/.gitattributes similarity index 100% rename from src/NerdBank.GitVersioning.Tests/ManagedGit/.gitattributes rename to test/Nerdbank.GitVersioning.Tests/ManagedGit/.gitattributes diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/3596ffe59898103a2675547d4597e742e1f2389c.gz b/test/Nerdbank.GitVersioning.Tests/ManagedGit/3596ffe59898103a2675547d4597e742e1f2389c.gz similarity index 100% rename from src/NerdBank.GitVersioning.Tests/ManagedGit/3596ffe59898103a2675547d4597e742e1f2389c.gz rename to test/Nerdbank.GitVersioning.Tests/ManagedGit/3596ffe59898103a2675547d4597e742e1f2389c.gz diff --git a/test/Nerdbank.GitVersioning.Tests/ManagedGit/DeltaStreamReaderTests.cs b/test/Nerdbank.GitVersioning.Tests/ManagedGit/DeltaStreamReaderTests.cs new file mode 100644 index 00000000..b0b7a595 --- /dev/null +++ b/test/Nerdbank.GitVersioning.Tests/ManagedGit/DeltaStreamReaderTests.cs @@ -0,0 +1,180 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.ObjectModel; +using System.IO; +using Nerdbank.GitVersioning.ManagedGit; +using Xunit; + +namespace ManagedGit; + +// Test case borrowed from https://stefan.saasen.me/articles/git-clone-in-haskell-from-the-bottom-up/#format-of-the-delta-representation +public class DeltaStreamReaderTests +{ + [Fact] + public void ReadCopyInstruction() + { + using (Stream stream = new MemoryStream( + new byte[] + { + 0b_10110000, + 0b_11010001, + 0b_00000001, + })) + { + DeltaInstruction instruction = DeltaStreamReader.Read(stream).Value; + + Assert.Equal(DeltaInstructionType.Copy, instruction.InstructionType); + Assert.Equal(0, instruction.Offset); + Assert.Equal(465, instruction.Size); + } + } + + [Fact] + public void ReadCopyInstruction_Memory() + { + byte[] stream = new byte[] + { + 0b_10110000, + 0b_11010001, + 0b_00000001, + }; + var memory = new ReadOnlyMemory(stream); + + DeltaInstruction instruction = DeltaStreamReader.Read(ref memory).Value; + + Assert.Equal(0, memory.Length); + Assert.Equal(DeltaInstructionType.Copy, instruction.InstructionType); + Assert.Equal(0, instruction.Offset); + Assert.Equal(465, instruction.Size); + } + + [Fact] + public void ReadInsertInstruction() + { + using (Stream stream = new MemoryStream(new byte[] { 0b_00010111 })) + { + DeltaInstruction instruction = DeltaStreamReader.Read(stream).Value; + + Assert.Equal(DeltaInstructionType.Insert, instruction.InstructionType); + Assert.Equal(0, instruction.Offset); + Assert.Equal(23, instruction.Size); + } + } + + [Fact] + public void ReadInsertInstruction_Memory() + { + byte[] stream = new byte[] { 0b_00010111 }; + var memory = new ReadOnlyMemory(stream); + + DeltaInstruction instruction = DeltaStreamReader.Read(ref memory).Value; + + Assert.Equal(0, memory.Length); + Assert.Equal(DeltaInstructionType.Insert, instruction.InstructionType); + Assert.Equal(0, instruction.Offset); + Assert.Equal(23, instruction.Size); + } + + [Fact] + public void ReadStreamTest() + { + using (Stream stream = new MemoryStream( + new byte[] + { + 0b_10110011, 0b_11001110, 0b_00000001, 0b_00100111, 0b_00000001, + 0b_10110011, 0b_01011111, 0b_00000011, 0b_01101100, 0b_00010000, 0b_10010011, + 0b_11110101, 0b_00000010, 0b_01101011, 0b_10110011, 0b_11001011, 0b_00010011, + 0b_01000110, 0b_00000011, + })) + { + Collection instructions = new Collection(); + + DeltaInstruction? current; + + while ((current = DeltaStreamReader.Read(stream)) is not null) + { + instructions.Add(current.Value); + } + + Assert.Collection( + instructions, + instruction => + { + Assert.Equal(DeltaInstructionType.Copy, instruction.InstructionType); + Assert.Equal(462, instruction.Offset); + Assert.Equal(295, instruction.Size); + }, + instruction => + { + Assert.Equal(DeltaInstructionType.Copy, instruction.InstructionType); + Assert.Equal(863, instruction.Offset); + Assert.Equal(4204, instruction.Size); + }, + instruction => + { + Assert.Equal(DeltaInstructionType.Copy, instruction.InstructionType); + Assert.Equal(757, instruction.Offset); + Assert.Equal(107, instruction.Size); + }, + instruction => + { + Assert.Equal(DeltaInstructionType.Copy, instruction.InstructionType); + Assert.Equal(5067, instruction.Offset); + Assert.Equal(838, instruction.Size); + }); + } + } + + [Fact] + public void ReadStreamTest_Memory() + { + byte[] stream = + new byte[] + { + 0b_10110011, 0b_11001110, 0b_00000001, 0b_00100111, 0b_00000001, + 0b_10110011, 0b_01011111, 0b_00000011, 0b_01101100, 0b_00010000, 0b_10010011, + 0b_11110101, 0b_00000010, 0b_01101011, 0b_10110011, 0b_11001011, 0b_00010011, + 0b_01000110, 0b_00000011, + }; + var memory = new ReadOnlyMemory(stream); + + Collection instructions = new Collection(); + + DeltaInstruction? current; + + while ((current = DeltaStreamReader.Read(ref memory)) is not null) + { + instructions.Add(current.Value); + } + + Assert.Equal(0, memory.Length); + Assert.Collection( + instructions, + instruction => + { + Assert.Equal(DeltaInstructionType.Copy, instruction.InstructionType); + Assert.Equal(462, instruction.Offset); + Assert.Equal(295, instruction.Size); + }, + instruction => + { + Assert.Equal(DeltaInstructionType.Copy, instruction.InstructionType); + Assert.Equal(863, instruction.Offset); + Assert.Equal(4204, instruction.Size); + }, + instruction => + { + Assert.Equal(DeltaInstructionType.Copy, instruction.InstructionType); + Assert.Equal(757, instruction.Offset); + Assert.Equal(107, instruction.Size); + }, + instruction => + { + Assert.Equal(DeltaInstructionType.Copy, instruction.InstructionType); + Assert.Equal(5067, instruction.Offset); + Assert.Equal(838, instruction.Size); + }); + } +} diff --git a/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitCommitReaderTests.cs b/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitCommitReaderTests.cs new file mode 100644 index 00000000..c4c96d60 --- /dev/null +++ b/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitCommitReaderTests.cs @@ -0,0 +1,55 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Linq; +using Nerdbank.GitVersioning.ManagedGit; +using Xunit; + +namespace ManagedGit; + +public class GitCommitReaderTests +{ + [Fact] + public void ReadTest() + { + using (Stream stream = TestUtilities.GetEmbeddedResource(@"ManagedGit\commit-d56dc3ed179053abef2097d1120b4507769bcf1a")) + { + GitCommit commit = GitCommitReader.Read(stream, GitObjectId.Parse("d56dc3ed179053abef2097d1120b4507769bcf1a"), readAuthor: true); + + Assert.Equal("d56dc3ed179053abef2097d1120b4507769bcf1a", commit.Sha.ToString()); + Assert.Equal("f914b48023c7c804a4f3be780d451f31aef74ac1", commit.Tree.ToString()); + + Assert.Collection( + commit.Parents, + c => Assert.Equal("4497b0eaaa89abf0e6d70961ad5f04fd3a49cbc6", c.ToString()), + c => Assert.Equal("0989e8fe0cd0e0900173b26decdfb24bc0cc8232", c.ToString())); + + GitSignature author = commit.Author.Value; + + Assert.Equal("Andrew Arnott", author.Name); + Assert.Equal(new DateTimeOffset(2020, 10, 6, 13, 40, 09, TimeSpan.FromHours(-6)), author.Date); + Assert.Equal("andrewarnott@gmail.com", author.Email); + + // Committer and commit message are not read + } + } + + [Fact] + public void ReadCommitWithThreeParents() + { + using (Stream stream = TestUtilities.GetEmbeddedResource(@"ManagedGit\commit-ab39e8acac105fa0db88514f259341c9f0201b22")) + { + GitCommit commit = GitCommitReader.Read(stream, GitObjectId.Parse("ab39e8acac105fa0db88514f259341c9f0201b22"), readAuthor: true); + + Assert.Equal(3, commit.Parents.Count()); + + Assert.Collection( + commit.Parents, + c => Assert.Equal("e0b4d66ef7915417e04e88d5fa173185bb940029", c.ToString()), + c => Assert.Equal("10e67ce38fbee44b3f5584d4f9df6de6c5f4cc5c", c.ToString()), + c => Assert.Equal("a7fef320334121af85dce4b9b731f6c9a9127cfd", c.ToString())); + } + } +} diff --git a/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitCommitTests.cs b/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitCommitTests.cs new file mode 100644 index 00000000..d053ba2a --- /dev/null +++ b/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitCommitTests.cs @@ -0,0 +1,100 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Nerdbank.GitVersioning.ManagedGit; +using Xunit; + +namespace ManagedGit; + +public class GitCommitTests +{ + private readonly byte[] shaAsByteArray = new byte[] { 0x4e, 0x91, 0x27, 0x36, 0xc2, 0x7e, 0x40, 0xb3, 0x89, 0x90, 0x4d, 0x04, 0x6d, 0xc6, 0x3d, 0xc9, 0xf5, 0x78, 0x11, 0x7f }; + + [Fact] + public void EqualsObjectTest() + { + var commit = new GitCommit() + { + Sha = GitObjectId.Parse(this.shaAsByteArray), + }; + + var commit2 = new GitCommit() + { + Sha = GitObjectId.Parse(this.shaAsByteArray), + }; + + var emptyCommit = new GitCommit() + { + Sha = GitObjectId.Empty, + }; + + // Must be equal to itself + Assert.True(commit.Equals((object)commit)); + Assert.True(commit.Equals((object)commit2)); + + // Not equal to null + Assert.False(commit.Equals(null)); + + // Not equal to other representations of the commit + Assert.False(commit.Equals(this.shaAsByteArray)); + Assert.False(commit.Equals(commit.Sha)); + + // Not equal to other object ids + Assert.False(commit.Equals((object)emptyCommit)); + } + + [Fact] + public void EqualsCommitTest() + { + var commit = new GitCommit() + { + Sha = GitObjectId.Parse(this.shaAsByteArray), + }; + + var commit2 = new GitCommit() + { + Sha = GitObjectId.Parse(this.shaAsByteArray), + }; + + var emptyCommit = new GitCommit() + { + Sha = GitObjectId.Empty, + }; + + // Must be equal to itself + Assert.True(commit.Equals(commit2)); + Assert.True(commit.Equals(commit2)); + + // Not equal to other object ids + Assert.False(commit.Equals(emptyCommit)); + } + + [Fact] + public void GetHashCodeTest() + { + var commit = new GitCommit() + { + Sha = GitObjectId.Parse(this.shaAsByteArray), + }; + + var emptyCommit = new GitCommit() + { + Sha = GitObjectId.Empty, + }; + + // The hash code is the int32 representation of the first 4 bytes of the SHA hash + Assert.Equal(0x3627914e, commit.GetHashCode()); + Assert.Equal(0, emptyCommit.GetHashCode()); + } + + [Fact] + public void ToStringTest() + { + var commit = new GitCommit() + { + Sha = GitObjectId.Parse(this.shaAsByteArray), + }; + + Assert.Equal("Git Commit: 4e912736c27e40b389904d046dc63dc9f578117f", commit.ToString()); + } +} diff --git a/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitObjectIdTests.cs b/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitObjectIdTests.cs new file mode 100644 index 00000000..273f65ad --- /dev/null +++ b/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitObjectIdTests.cs @@ -0,0 +1,136 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.InteropServices; +using System.Text; +using Nerdbank.GitVersioning.ManagedGit; +using Xunit; +using Xunit.Abstractions; + +namespace ManagedGit; + +public class GitObjectIdTests +{ + private const string ShaAsHexString = "4e912736c27e40b389904d046dc63dc9f578117f"; + private readonly byte[] shaAsByteArray = new byte[] { 0x4e, 0x91, 0x27, 0x36, 0xc2, 0x7e, 0x40, 0xb3, 0x89, 0x90, 0x4d, 0x04, 0x6d, 0xc6, 0x3d, 0xc9, 0xf5, 0x78, 0x11, 0x7f }; + private readonly byte[] shaAsHexAsciiByteArray = Encoding.ASCII.GetBytes(ShaAsHexString); + + [Fact] + public void ParseByteArrayTest() + { + var objectId = GitObjectId.Parse(this.shaAsByteArray); + + Span value = stackalloc byte[20]; + objectId.CopyTo(value); + Assert.True(value.SequenceEqual(this.shaAsByteArray.AsSpan())); + } + + [Fact] + public void ParseStringTest() + { + var objectId = GitObjectId.Parse(ShaAsHexString); + + Span value = stackalloc byte[20]; + objectId.CopyTo(value); + Assert.True(value.SequenceEqual(this.shaAsByteArray.AsSpan())); + } + + [Fact] + public void ParseHexArrayTest() + { + var objectId = GitObjectId.ParseHex(this.shaAsHexAsciiByteArray); + + Span value = stackalloc byte[20]; + objectId.CopyTo(value); + Assert.True(value.SequenceEqual(this.shaAsByteArray.AsSpan())); + } + + [Fact] + public void EqualsObjectTest() + { + var objectId = GitObjectId.ParseHex(this.shaAsHexAsciiByteArray); + var objectId2 = GitObjectId.ParseHex(this.shaAsHexAsciiByteArray); + + // Must be equal to itself + Assert.True(objectId.Equals((object)objectId)); + Assert.True(objectId.Equals((object)objectId2)); + + // Not equal to null + Assert.False(objectId.Equals(null)); + + // Not equal to other representations of the object id + Assert.False(objectId.Equals(this.shaAsHexAsciiByteArray)); + Assert.False(objectId.Equals(this.shaAsByteArray)); + Assert.False(objectId.Equals(ShaAsHexString)); + + // Not equal to other object ids + Assert.False(objectId.Equals((object)GitObjectId.Empty)); + } + + [Fact] + public void EqualsObjectIdTest() + { + var objectId = GitObjectId.ParseHex(this.shaAsHexAsciiByteArray); + var objectId2 = GitObjectId.ParseHex(this.shaAsHexAsciiByteArray); + + // Must be equal to itself + Assert.True(objectId.Equals(objectId)); + Assert.True(objectId.Equals(objectId2)); + + // Not equal to other object ids + Assert.False(objectId.Equals(GitObjectId.Empty)); + } + + [Fact] + public void GetHashCodeTest() + { + // The hash code is the int32 representation of the first 4 bytes + var objectId = GitObjectId.ParseHex(this.shaAsHexAsciiByteArray); + Assert.Equal(0x3627914e, objectId.GetHashCode()); + Assert.Equal(0, GitObjectId.Empty.GetHashCode()); + } + + [Fact] + public void AsUInt16Test() + { + // The hash code is the int32 representation of the first 4 bytes + var objectId = GitObjectId.ParseHex(this.shaAsHexAsciiByteArray); + Assert.Equal(0x4e91, objectId.AsUInt16()); + Assert.Equal(0, GitObjectId.Empty.GetHashCode()); + } + + [Fact] + public void ToStringTest() + { + var objectId = GitObjectId.Parse(this.shaAsByteArray); + Assert.Equal(ShaAsHexString, objectId.ToString()); + } + + [Fact] + public void CopyToUtf16StringTest() + { + // Common use case: create the path to the object in the Git object store, + // e.g. git/objects/[byte 0]/[bytes 1 - 19] + byte[] valueAsBytes = Encoding.Unicode.GetBytes("git/objects/00/01020304050607080910111213141516171819"); + Span valueAsChars = MemoryMarshal.Cast(valueAsBytes); + + var objectId = GitObjectId.ParseHex(this.shaAsHexAsciiByteArray); + objectId.CopyAsHex(0, 1, valueAsChars.Slice(12, 1 * 2)); + objectId.CopyAsHex(1, 19, valueAsChars.Slice(15, 19 * 2)); + + string path = Encoding.Unicode.GetString(valueAsBytes); + Assert.Equal("git/objects/4e/912736c27e40b389904d046dc63dc9f578117f", path); + } + + [Fact] + public void CopyToTest() + { + var objectId = GitObjectId.Parse(this.shaAsByteArray); + + byte[] actual = new byte[20]; + objectId.CopyTo(actual); + + Assert.Equal(this.shaAsByteArray, actual); + } +} diff --git a/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitObjectStreamTests.cs b/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitObjectStreamTests.cs new file mode 100644 index 00000000..2ffeaee9 --- /dev/null +++ b/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitObjectStreamTests.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.IO.Compression; +using System.Security.Cryptography; +using Nerdbank.GitVersioning.ManagedGit; +using Xunit; + +namespace ManagedGit; + +public class GitObjectStreamTests +{ + [Fact] + public void ReadTest() + { + using (Stream rawStream = TestUtilities.GetEmbeddedResource(@"ManagedGit\3596ffe59898103a2675547d4597e742e1f2389c.gz")) + using (GitObjectStream stream = new GitObjectStream(rawStream, "commit")) + using (var sha = SHA1.Create()) + { + Assert.Equal(137, stream.Length); + DeflateStream deflateStream = Assert.IsType(stream.BaseStream); + Assert.Same(rawStream, deflateStream.BaseStream); + Assert.Equal("commit", stream.ObjectType); + Assert.Equal(0, stream.Position); + + byte[] hash = sha.ComputeHash(stream); + Assert.Equal("U1WYLbBP+xD47Y32m+hpCCTpnLA=", Convert.ToBase64String(hash)); + + Assert.Equal(stream.Length, stream.Position); + } + } +} diff --git a/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitPackDeltafiedStreamTests.cs b/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitPackDeltafiedStreamTests.cs new file mode 100644 index 00000000..6f2af74a --- /dev/null +++ b/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitPackDeltafiedStreamTests.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.IO; +using Nerdbank.GitVersioning.ManagedGit; +using Xunit; + +namespace ManagedGit; + +public class GitPackDeltafiedStreamTests +{ + // Reconstructs an object by reading the base stream and the delta stream. + // You can create delta representations of an object by running the + // test tool which is located in the t/helper/ folder of the Git source repository. + // Use with the delta -d [base file,in] [updated file,in] [delta file,out] arguments. + [Theory] + [InlineData(@"ManagedGit\commit-4497b0eaaa89abf0e6d70961ad5f04fd3a49cbc6", @"ManagedGit\commit.delta", @"ManagedGit\commit-d56dc3ed179053abef2097d1120b4507769bcf1a")] + [InlineData(@"ManagedGit\tree-bb36cf0ca445ccc8e5ce9cc88f7cf74128e96dc9", @"ManagedGit\tree.delta", @"ManagedGit\tree-f914b48023c7c804a4f3be780d451f31aef74ac1")] + public void TestDeltaStream(string basePath, string deltaPath, string expectedPath) + { + byte[] expected = null; + + using (Stream expectedStream = TestUtilities.GetEmbeddedResource(expectedPath)) + { + expected = new byte[expectedStream.Length]; + expectedStream.Read(expected); + } + + byte[] actual = new byte[expected.Length]; + + using (Stream baseStream = TestUtilities.GetEmbeddedResource(basePath)) + using (Stream deltaStream = TestUtilities.GetEmbeddedResource(deltaPath)) + using (GitPackDeltafiedStream deltafiedStream = new GitPackDeltafiedStream(baseStream, deltaStream)) + { + ////Assert.Equal(expected.Length, deltafiedStream.Length); + + deltafiedStream.Read(actual); + + Assert.Equal(expected, actual); + } + } +} diff --git a/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitPackIndexMappedReaderTests.cs b/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitPackIndexMappedReaderTests.cs new file mode 100644 index 00000000..d4e96eda --- /dev/null +++ b/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitPackIndexMappedReaderTests.cs @@ -0,0 +1,89 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using Nerdbank.GitVersioning.ManagedGit; +using Xunit; + +namespace ManagedGit; + +public class GitPackIndexMappedReaderTests +{ + [Fact] + public void ConstructorNullTest() + { + Assert.Throws(() => new GitPackIndexMappedReader(null)); + } + + [Fact] + public void GetOffsetTest() + { + string indexFile = Path.GetTempFileName(); + + using (Stream resourceStream = TestUtilities.GetEmbeddedResource(@"ManagedGit\pack-7d6b2c56ffb97eedb92f4e28583c093f7ee4b3d9.idx")) + using (FileStream stream = File.Open(indexFile, FileMode.Open)) + { + resourceStream.CopyTo(stream); + } + + using (FileStream stream = File.OpenRead(indexFile)) + using (GitPackIndexReader reader = new GitPackIndexMappedReader(stream)) + { + // Offset of an object which is present + Assert.Equal(12, reader.GetOffset(GitObjectId.Parse("f5b401f40ad83f13030e946c9ea22cb54cb853cd"))); + Assert.Equal(317, reader.GetOffset(GitObjectId.Parse("d6781552a0a94adbf73ed77696712084754dc274"))); + + // null for an object which is not present + Assert.Null(reader.GetOffset(GitObjectId.Empty)); + } + + try + { + File.Delete(indexFile); + } + catch (UnauthorizedAccessException) + { + // TBD: Figure out what's keeping a lock on the file. Seems to be unique to Windows. + } + } + + [Fact] + public void GetOffsetFromPartialTest() + { + string indexFile = Path.GetTempFileName(); + + using (Stream resourceStream = TestUtilities.GetEmbeddedResource(@"ManagedGit\pack-7d6b2c56ffb97eedb92f4e28583c093f7ee4b3d9.idx")) + using (FileStream stream = File.Open(indexFile, FileMode.Open)) + { + resourceStream.CopyTo(stream); + } + + using (FileStream stream = File.OpenRead(indexFile)) + using (var reader = new GitPackIndexMappedReader(stream)) + { + // Offset of an object which is present + (long? offset, GitObjectId? objectId) = reader.GetOffset(new byte[] { 0xf5, 0xb4, 0x01, 0xf4 }); + Assert.Equal(12, offset); + Assert.Equal(GitObjectId.Parse("f5b401f40ad83f13030e946c9ea22cb54cb853cd"), objectId); + + (offset, objectId) = reader.GetOffset(new byte[] { 0xd6, 0x78, 0x15, 0x52 }); + Assert.Equal(317, offset); + Assert.Equal(GitObjectId.Parse("d6781552a0a94adbf73ed77696712084754dc274"), objectId); + + // null for an object which is not present + (offset, objectId) = reader.GetOffset(new byte[] { 0x00, 0x00, 0x00, 0x00 }); + Assert.Null(offset); + Assert.Null(objectId); + } + + try + { + File.Delete(indexFile); + } + catch (UnauthorizedAccessException) + { + // TBD: Figure out what's keeping a lock on the file. Seems to be unique to Windows. + } + } +} diff --git a/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitPackMemoryCacheTests.cs b/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitPackMemoryCacheTests.cs new file mode 100644 index 00000000..c03ae858 --- /dev/null +++ b/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitPackMemoryCacheTests.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.IO; +using Nerdbank.GitVersioning.ManagedGit; +using Xunit; + +namespace ManagedGit; + +/// +/// Tests the class. +/// +public class GitPackMemoryCacheTests +{ + [Fact] + public void StreamsAreIndependent() + { + using (MemoryStream stream = new MemoryStream( + new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 })) + { + var cache = new GitPackMemoryCache(); + + Stream stream1 = cache.Add(0, stream); + Assert.True(cache.TryOpen(0, out Stream stream2)); + + using (stream1) + using (stream2) + { + stream1.Seek(5, SeekOrigin.Begin); + Assert.Equal(5, stream1.Position); + Assert.Equal(0, stream2.Position); + Assert.Equal(5, stream1.ReadByte()); + + Assert.Equal(6, stream1.Position); + Assert.Equal(0, stream2.Position); + + Assert.Equal(0, stream2.ReadByte()); + Assert.Equal(6, stream1.Position); + Assert.Equal(1, stream2.Position); + } + } + } +} diff --git a/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitPackTests.cs b/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitPackTests.cs new file mode 100644 index 00000000..61e9793f --- /dev/null +++ b/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitPackTests.cs @@ -0,0 +1,177 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.IO.Compression; +using System.Security.Cryptography; +using Nerdbank.GitVersioning; +using Nerdbank.GitVersioning.ManagedGit; +using Xunit; +using ZLibStream = Nerdbank.GitVersioning.ManagedGit.ZLibStream; + +namespace ManagedGit; + +public class GitPackTests : IDisposable +{ + private readonly string indexFile = Path.GetTempFileName(); + private readonly string packFile = Path.GetTempFileName(); + + public GitPackTests() + { + using (Stream resourceStream = TestUtilities.GetEmbeddedResource(@"ManagedGit\pack-7d6b2c56ffb97eedb92f4e28583c093f7ee4b3d9.idx")) + using (FileStream stream = File.Open(this.indexFile, FileMode.Open)) + { + resourceStream.CopyTo(stream); + } + + using (Stream resourceStream = TestUtilities.GetEmbeddedResource(@"ManagedGit\pack-7d6b2c56ffb97eedb92f4e28583c093f7ee4b3d9.pack")) + using (FileStream stream = File.Open(this.packFile, FileMode.Open)) + { + resourceStream.CopyTo(stream); + } + } + + /// + public void Dispose() + { + try + { + File.Delete(this.indexFile); + } + catch (UnauthorizedAccessException) + { + // TBD: Figure out what's keeping a lock on the file. Seems to be unique to Windows. + } + + try + { + File.Delete(this.packFile); + } + catch (UnauthorizedAccessException) + { + // TBD: Figure out what's keeping a lock on the file. Seems to be unique to Windows. + } + } + + [Fact] + public void GetPackedObject() + { + using (var gitPack = new GitPack( + (sha, objectType) => null, + new Lazy(() => File.OpenRead(this.indexFile)), + () => File.OpenRead(this.packFile), + GitPackNullCache.Instance)) + using (Stream commitStream = gitPack.GetObject(12, "commit")) + using (SHA1 sha = SHA1.Create()) + { + // This commit is not deltafied. It is stored as a .gz-compressed stream in the pack file. + ZLibStream zlibStream = Assert.IsType(commitStream); + DeflateStream deflateStream = Assert.IsType(zlibStream.BaseStream); + + if (IntPtr.Size > 4) + { + MemoryMappedStream pooledStream = Assert.IsType(deflateStream.BaseStream); + } + else + { + FileStream pooledStream = Assert.IsType(deflateStream.BaseStream); + } + + Assert.Equal(222, commitStream.Length); + Assert.Equal("/zgldANj+jvgOwlecnOKylZDVQg=", Convert.ToBase64String(sha.ComputeHash(commitStream))); + } + } + + [Fact] + public void GetDeltafiedObject() + { + using (var gitPack = new GitPack( + (sha, objectType) => null, + new Lazy(() => File.OpenRead(this.indexFile)), + () => File.OpenRead(this.packFile), + GitPackNullCache.Instance)) + using (Stream commitStream = gitPack.GetObject(317, "commit")) + using (SHA1 sha = SHA1.Create()) + { + // This commit is not deltafied. It is stored as a .gz-compressed stream in the pack file. + GitPackDeltafiedStream deltaStream = Assert.IsType(commitStream); + ZLibStream zlibStream = Assert.IsType(deltaStream.BaseStream); + DeflateStream deflateStream = Assert.IsType(zlibStream.BaseStream); + + if (IntPtr.Size > 4) + { + MemoryMappedStream pooledStream = Assert.IsType(deflateStream.BaseStream); + } + else + { + FileStream directAccessStream = Assert.IsType(deflateStream.BaseStream); + } + + Assert.Equal(137, commitStream.Length); + Assert.Equal("lZu/7nGb0n1UuO9SlPluFnSvj4o=", Convert.ToBase64String(sha.ComputeHash(commitStream))); + } + } + + [Fact] + public void GetInvalidObject() + { + using (var gitPack = new GitPack( + (sha, objectType) => null, + new Lazy(() => File.OpenRead(this.indexFile)), + () => File.OpenRead(this.packFile), + GitPackNullCache.Instance)) + { + Assert.Throws(() => gitPack.GetObject(12, "invalid")); + Assert.Throws(() => gitPack.GetObject(-1, "commit")); + Assert.Throws(() => gitPack.GetObject(1, "commit")); + Assert.Throws(() => gitPack.GetObject(2, "commit")); + Assert.Throws(() => gitPack.GetObject(int.MaxValue, "commit")); + } + } + + [Fact] + public void TryGetObjectTest() + { + using (var gitPack = new GitPack( + (sha, objectType) => null, + new Lazy(() => File.OpenRead(this.indexFile)), + () => File.OpenRead(this.packFile), + GitPackNullCache.Instance)) + using (SHA1 sha = SHA1.Create()) + { + Assert.True(gitPack.TryGetObject(GitObjectId.Parse("f5b401f40ad83f13030e946c9ea22cb54cb853cd"), "commit", out Stream commitStream)); + using (commitStream) + { + // This commit is not deltafied. It is stored as a .gz-compressed stream in the pack file. + ZLibStream zlibStream = Assert.IsType(commitStream); + DeflateStream deflateStream = Assert.IsType(zlibStream.BaseStream); + + if (IntPtr.Size > 4) + { + MemoryMappedStream pooledStream = Assert.IsType(deflateStream.BaseStream); + } + else + { + FileStream directAccessStream = Assert.IsType(deflateStream.BaseStream); + } + + Assert.Equal(222, commitStream.Length); + Assert.Equal("/zgldANj+jvgOwlecnOKylZDVQg=", Convert.ToBase64String(sha.ComputeHash(commitStream))); + } + } + } + + [Fact] + public void TryGetMissingObjectTest() + { + using (var gitPack = new GitPack( + (sha, objectType) => null, + new Lazy(() => File.OpenRead(this.indexFile)), + () => File.OpenRead(this.packFile), + GitPackNullCache.Instance)) + { + Assert.False(gitPack.TryGetObject(GitObjectId.Empty, "commit", out Stream commitStream)); + } + } +} diff --git a/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitRepositoryTests.cs b/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitRepositoryTests.cs new file mode 100644 index 00000000..582dfbdc --- /dev/null +++ b/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitRepositoryTests.cs @@ -0,0 +1,340 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Linq; +using System.Text; +using LibGit2Sharp; +using Nerdbank.GitVersioning; +using Nerdbank.GitVersioning.ManagedGit; +using Xunit; +using Xunit.Abstractions; + +namespace ManagedGit; + +public class GitRepositoryTests : RepoTestBase +{ + public GitRepositoryTests(ITestOutputHelper logger) + : base(logger) + { + } + + [Fact] + public void CreateTest() + { + this.InitializeSourceControl(); + this.AddCommits(1); + + using (var repository = GitRepository.Create(this.RepoPath)) + { + AssertPath(Path.Combine(this.RepoPath, ".git"), repository.CommonDirectory); + AssertPath(Path.Combine(this.RepoPath, ".git"), repository.GitDirectory); + AssertPath(this.RepoPath, repository.WorkingDirectory); + AssertPath(Path.Combine(this.RepoPath, ".git", "objects"), repository.ObjectDirectory); + } + } + + [Fact] + public void CreateWorkTreeTest() + { + this.InitializeSourceControl(); + this.AddCommits(2); + + string workTreePath = this.CreateDirectoryForNewRepo(); + Directory.Delete(workTreePath); + this.LibGit2Repository.Worktrees.Add("HEAD~1", "myworktree", workTreePath, isLocked: false); + + using (var repository = GitRepository.Create(workTreePath)) + { + AssertPath(Path.Combine(this.RepoPath, ".git"), repository.CommonDirectory); + AssertPath(Path.Combine(this.RepoPath, ".git", "worktrees", "myworktree"), repository.GitDirectory); + AssertPath(workTreePath, repository.WorkingDirectory); + AssertPath(Path.Combine(this.RepoPath, ".git", "objects"), repository.ObjectDirectory); + } + } + + [Fact] + public void CreateNotARepoTest() + { + Assert.Null(GitRepository.Create(null)); + Assert.Null(GitRepository.Create(string.Empty)); + Assert.Null(GitRepository.Create("/A/Path/To/A/Directory/Which/Does/Not/Exist")); + Assert.Null(GitRepository.Create(this.RepoPath)); + } + + // A "normal" repository, where a branch is currently checked out. + [Fact] + public void GetHeadAsReferenceTest() + { + this.InitializeSourceControl(); + this.AddCommits(2); + + var headObjectId = GitObjectId.Parse(this.LibGit2Repository.Head.Tip.Sha); + + using (var repository = GitRepository.Create(this.RepoPath)) + { + object head = repository.GetHeadAsReferenceOrSha(); + string reference = Assert.IsType(head); + Assert.Equal("refs/heads/master", reference); + + Assert.Equal(headObjectId, repository.GetHeadCommitSha()); + + GitCommit? headCommit = repository.GetHeadCommit(); + Assert.NotNull(headCommit); + Assert.Equal(headObjectId, headCommit.Value.Sha); + } + } + + // A repository with a detached HEAD. + [Fact] + public void GetHeadAsShaTest() + { + this.InitializeSourceControl(); + this.AddCommits(2); + + Commit newHead = this.LibGit2Repository.Head.Tip.Parents.Single(); + var newHeadObjectId = GitObjectId.Parse(newHead.Sha); + Commands.Checkout(this.LibGit2Repository, this.LibGit2Repository.Head.Tip.Parents.Single()); + + using (var repository = GitRepository.Create(this.RepoPath)) + { + object detachedHead = repository.GetHeadAsReferenceOrSha(); + GitObjectId reference = Assert.IsType(detachedHead); + Assert.Equal(newHeadObjectId, reference); + + Assert.Equal(newHeadObjectId, repository.GetHeadCommitSha()); + + GitCommit? headCommit = repository.GetHeadCommit(); + Assert.NotNull(headCommit); + Assert.Equal(newHeadObjectId, headCommit.Value.Sha); + } + } + + // A fresh repository with no commits yet. + [Fact] + public void GetHeadMissingTest() + { + this.InitializeSourceControl(withInitialCommit: false); + + using (var repository = GitRepository.Create(this.RepoPath)) + { + object head = repository.GetHeadAsReferenceOrSha(); + string reference = Assert.IsType(head); + Assert.Equal("refs/heads/master", reference); + + Assert.Equal(GitObjectId.Empty, repository.GetHeadCommitSha()); + + Assert.Null(repository.GetHeadCommit()); + } + } + + // Fetch a commit from the object store + [Fact] + public void GetCommitTest() + { + this.InitializeSourceControl(); + this.AddCommits(2); + + var headObjectId = GitObjectId.Parse(this.LibGit2Repository.Head.Tip.Sha); + + using (var repository = GitRepository.Create(this.RepoPath)) + { + GitCommit commit = repository.GetCommit(headObjectId); + Assert.Equal(headObjectId, commit.Sha); + } + } + + [Fact] + public void GetInvalidCommitTest() + { + this.InitializeSourceControl(); + this.AddCommits(2); + + var headObjectId = GitObjectId.Parse(this.LibGit2Repository.Head.Tip.Sha); + + using (var repository = GitRepository.Create(this.RepoPath)) + { + Assert.Throws(() => repository.GetCommit(GitObjectId.Empty)); + } + } + + [Fact] + public void GetTreeEntryTest() + { + this.InitializeSourceControl(); + File.WriteAllText(Path.Combine(this.RepoPath, "hello.txt"), "Hello, World"); + Commands.Stage(this.LibGit2Repository, "hello.txt"); + this.AddCommits(); + + using (var repository = GitRepository.Create(this.RepoPath)) + { + GitCommit? headCommit = repository.GetHeadCommit(); + Assert.NotNull(headCommit); + + GitObjectId helloBlobId = repository.GetTreeEntry(headCommit.Value.Tree, Encoding.UTF8.GetBytes("hello.txt")); + Assert.Equal("1856e9be02756984c385482a07e42f42efd5d2f3", helloBlobId.ToString()); + } + } + + [Fact] + public void GetInvalidTreeEntryTest() + { + this.InitializeSourceControl(); + File.WriteAllText(Path.Combine(this.RepoPath, "hello.txt"), "Hello, World"); + Commands.Stage(this.LibGit2Repository, "hello.txt"); + this.AddCommits(); + + using (var repository = GitRepository.Create(this.RepoPath)) + { + GitCommit? headCommit = repository.GetHeadCommit(); + Assert.NotNull(headCommit); + + Assert.Equal(GitObjectId.Empty, repository.GetTreeEntry(headCommit.Value.Tree, Encoding.UTF8.GetBytes("goodbye.txt"))); + } + } + + [Fact] + public void GetObjectByShaTest() + { + this.InitializeSourceControl(); + this.AddCommits(2); + + var headObjectId = GitObjectId.Parse(this.LibGit2Repository.Head.Tip.Sha); + + using (var repository = GitRepository.Create(this.RepoPath)) + { + Stream commitStream = repository.GetObjectBySha(headObjectId, "commit"); + Assert.NotNull(commitStream); + + GitObjectStream objectStream = Assert.IsType(commitStream); + Assert.Equal("commit", objectStream.ObjectType); + Assert.Equal(186, objectStream.Length); + } + } + + // This test runs on netcoreapp only; netstandard/netfx don't support Path.GetRelativePath +#if NETCOREAPP + [Fact] + public void GetObjectFromAlternateTest() + { + // Add 2 alternates for this repository, each with their own commit. + // Make sure that commits from the current repository and the alternates + // can be found. + // + // Alternate1 Alternate2 + // | | + // +-----+ +-----+ + // | + // Repo + this.InitializeSourceControl(); + + Commit localCommit = this.LibGit2Repository.Commit("Local", this.Signer, this.Signer, new CommitOptions() { AllowEmptyCommit = true }); + + string alternate1Path = this.CreateDirectoryForNewRepo(); + this.InitializeSourceControl(alternate1Path).Dispose(); + var alternate1 = new Repository(alternate1Path); + Commit alternate1Commit = alternate1.Commit("Alternate 1", this.Signer, this.Signer, new CommitOptions() { AllowEmptyCommit = true }); + + string alternate2Path = this.CreateDirectoryForNewRepo(); + this.InitializeSourceControl(alternate2Path).Dispose(); + var alternate2 = new Repository(alternate2Path); + Commit alternate2Commit = alternate2.Commit("Alternate 2", this.Signer, this.Signer, new CommitOptions() { AllowEmptyCommit = true }); + + string objectDatabasePath = Path.Combine(this.RepoPath, ".git", "objects"); + + Directory.CreateDirectory(Path.Combine(this.RepoPath, ".git", "objects", "info")); + File.WriteAllText( + Path.Combine(this.RepoPath, ".git", "objects", "info", "alternates"), + $"{Path.GetRelativePath(objectDatabasePath, Path.Combine(alternate1Path, ".git", "objects"))}:{Path.GetRelativePath(objectDatabasePath, Path.Combine(alternate2Path, ".git", "objects"))}:"); + + using (GitRepository repository = GitRepository.Create(this.RepoPath)) + { + Assert.Equal(localCommit.Sha, repository.GetCommit(GitObjectId.Parse(localCommit.Sha)).Sha.ToString()); + Assert.Equal(alternate1Commit.Sha, repository.GetCommit(GitObjectId.Parse(alternate1Commit.Sha)).Sha.ToString()); + Assert.Equal(alternate2Commit.Sha, repository.GetCommit(GitObjectId.Parse(alternate2Commit.Sha)).Sha.ToString()); + } + } +#endif + + [Fact] + public void GetObjectByShaAndWrongTypeTest() + { + this.InitializeSourceControl(); + this.AddCommits(2); + + var headObjectId = GitObjectId.Parse(this.LibGit2Repository.Head.Tip.Sha); + + using (var repository = GitRepository.Create(this.RepoPath)) + { + Assert.Throws(() => repository.GetObjectBySha(headObjectId, "tree")); + } + } + + [Fact] + public void GetMissingObjectByShaTest() + { + this.InitializeSourceControl(); + this.AddCommits(2); + + var headObjectId = GitObjectId.Parse(this.LibGit2Repository.Head.Tip.Sha); + + using (var repository = GitRepository.Create(this.RepoPath)) + { + Assert.Throws(() => repository.GetObjectBySha(GitObjectId.Parse("7d6b2c56ffb97eedb92f4e28583c093f7ee4b3d9"), "commit")); + Assert.Null(repository.GetObjectBySha(GitObjectId.Empty, "commit")); + } + } + + [Fact] + public void ParseAlternates_SingleValue_Test() + { + List alternates = GitRepository.ParseAlternates(Encoding.UTF8.GetBytes("/home/git/nbgv/.git/objects\n")); + Assert.Collection( + alternates, + a => Assert.Equal("/home/git/nbgv/.git/objects", a)); + } + + [Fact] + public void ParseAlternates_SingleValue_NoTrailingNewline_Test() + { + List alternates = GitRepository.ParseAlternates(Encoding.UTF8.GetBytes("../repo/.git/objects")); + Assert.Collection( + alternates, + a => Assert.Equal("../repo/.git/objects", a)); + } + + [Fact] + public void ParseAlternates_TwoValues_Test() + { + List alternates = GitRepository.ParseAlternates(Encoding.UTF8.GetBytes("/home/git/nbgv/.git/objects:../../clone/.git/objects\n")); + Assert.Collection( + alternates, + a => Assert.Equal("/home/git/nbgv/.git/objects", a), + a => Assert.Equal("../../clone/.git/objects", a)); + } + + [Fact] + public void ParseAlternates_PathWithColon_Test() + { + List alternates = GitRepository.ParseAlternates( + Encoding.UTF8.GetBytes("C:/Users/nbgv/objects:C:/Users/nbgv2/objects/:../../clone/.git/objects\n"), + 2); + Assert.Collection( + alternates, + a => Assert.Equal("C:/Users/nbgv/objects", a), + a => Assert.Equal("C:/Users/nbgv2/objects/", a), + a => Assert.Equal("../../clone/.git/objects", a)); + } + + /// + protected override Nerdbank.GitVersioning.GitContext CreateGitContext(string path, string committish = null) + => Nerdbank.GitVersioning.GitContext.Create(path, committish, engine: GitContext.Engine.ReadOnly); + + private static void AssertPath(string expected, string actual) + { + Assert.Equal( + Path.GetFullPath(expected), + Path.GetFullPath(actual)); + } +} diff --git a/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitTreeStreamingReaderTests.cs b/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitTreeStreamingReaderTests.cs new file mode 100644 index 00000000..9e94fc36 --- /dev/null +++ b/test/Nerdbank.GitVersioning.Tests/ManagedGit/GitTreeStreamingReaderTests.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.IO; +using System.Text; +using Nerdbank.GitVersioning.ManagedGit; +using Xunit; + +namespace ManagedGit; + +public class GitTreeStreamingReaderTests +{ + [Fact] + public void FindBlobTest() + { + using (Stream stream = TestUtilities.GetEmbeddedResource(@"ManagedGit\tree.bin")) + { + GitObjectId blobObjectId = GitTreeStreamingReader.FindNode(stream, Encoding.UTF8.GetBytes("version.json")); + Assert.Equal("59552a5eed6779aa4e5bb4dc96e80f36bb6e7380", blobObjectId.ToString()); + } + } + + [Fact] + public void FindTreeTest() + { + using (Stream stream = TestUtilities.GetEmbeddedResource(@"ManagedGit\tree.bin")) + { + GitObjectId blobObjectId = GitTreeStreamingReader.FindNode(stream, Encoding.UTF8.GetBytes("tools")); + Assert.Equal("ec8e91fc4ad13d6a214584330f26d7a05495c8cc", blobObjectId.ToString()); + } + } +} diff --git a/test/Nerdbank.GitVersioning.Tests/ManagedGit/StreamExtensionsTests.cs b/test/Nerdbank.GitVersioning.Tests/ManagedGit/StreamExtensionsTests.cs new file mode 100644 index 00000000..881e9450 --- /dev/null +++ b/test/Nerdbank.GitVersioning.Tests/ManagedGit/StreamExtensionsTests.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.IO; +using Nerdbank.GitVersioning.ManagedGit; +using Xunit; + +namespace ManagedGit; + +public class StreamExtensionsTests +{ + [Fact] + public void ReadTest() + { + byte[] data = new byte[] { 0b10010001, 0b00101110 }; + + using (MemoryStream stream = new MemoryStream(data)) + { + Assert.Equal(5905, stream.ReadMbsInt()); + } + } +} diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/ZLibStreamTest.cs b/test/Nerdbank.GitVersioning.Tests/ManagedGit/ZLibStreamTest.cs similarity index 80% rename from src/NerdBank.GitVersioning.Tests/ManagedGit/ZLibStreamTest.cs rename to test/Nerdbank.GitVersioning.Tests/ManagedGit/ZLibStreamTest.cs index f71827d9..70d087e9 100644 --- a/src/NerdBank.GitVersioning.Tests/ManagedGit/ZLibStreamTest.cs +++ b/test/Nerdbank.GitVersioning.Tests/ManagedGit/ZLibStreamTest.cs @@ -1,10 +1,14 @@ -using System; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; using System.IO; using System.IO.Compression; using System.Security.Cryptography; using System.Text; using Nerdbank.GitVersioning.ManagedGit; using Xunit; +using ZLibStream = Nerdbank.GitVersioning.ManagedGit.ZLibStream; namespace ManagedGit { @@ -17,11 +21,11 @@ public void ReadTest() using (ZLibStream stream = new ZLibStream(rawStream, -1)) using (var sha = SHA1.Create()) { - var deflateStream = Assert.IsType(stream.BaseStream); + DeflateStream deflateStream = Assert.IsType(stream.BaseStream); Assert.Same(rawStream, deflateStream.BaseStream); Assert.Equal(0, stream.Position); - var hash = sha.ComputeHash(stream); + byte[] hash = sha.ComputeHash(stream); Assert.Equal("NZb/5ZiYEDomdVR9RZfnQuHyOJw=", Convert.ToBase64String(hash)); Assert.Equal(148, stream.Position); @@ -36,7 +40,7 @@ public void SeekTest() { // Seek past the commit 137 header, and make sure we can read the 'tree' word Assert.Equal(11, stream.Seek(11, SeekOrigin.Begin)); - var tree = new byte[4]; + byte[] tree = new byte[4]; stream.Read(tree); Assert.Equal("tree", Encoding.UTF8.GetString(tree)); diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/commit-4497b0eaaa89abf0e6d70961ad5f04fd3a49cbc6 b/test/Nerdbank.GitVersioning.Tests/ManagedGit/commit-4497b0eaaa89abf0e6d70961ad5f04fd3a49cbc6 similarity index 100% rename from src/NerdBank.GitVersioning.Tests/ManagedGit/commit-4497b0eaaa89abf0e6d70961ad5f04fd3a49cbc6 rename to test/Nerdbank.GitVersioning.Tests/ManagedGit/commit-4497b0eaaa89abf0e6d70961ad5f04fd3a49cbc6 diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/commit-ab39e8acac105fa0db88514f259341c9f0201b22 b/test/Nerdbank.GitVersioning.Tests/ManagedGit/commit-ab39e8acac105fa0db88514f259341c9f0201b22 similarity index 100% rename from src/NerdBank.GitVersioning.Tests/ManagedGit/commit-ab39e8acac105fa0db88514f259341c9f0201b22 rename to test/Nerdbank.GitVersioning.Tests/ManagedGit/commit-ab39e8acac105fa0db88514f259341c9f0201b22 diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/commit-d56dc3ed179053abef2097d1120b4507769bcf1a b/test/Nerdbank.GitVersioning.Tests/ManagedGit/commit-d56dc3ed179053abef2097d1120b4507769bcf1a similarity index 100% rename from src/NerdBank.GitVersioning.Tests/ManagedGit/commit-d56dc3ed179053abef2097d1120b4507769bcf1a rename to test/Nerdbank.GitVersioning.Tests/ManagedGit/commit-d56dc3ed179053abef2097d1120b4507769bcf1a diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/commit.delta b/test/Nerdbank.GitVersioning.Tests/ManagedGit/commit.delta similarity index 100% rename from src/NerdBank.GitVersioning.Tests/ManagedGit/commit.delta rename to test/Nerdbank.GitVersioning.Tests/ManagedGit/commit.delta diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/pack-7d6b2c56ffb97eedb92f4e28583c093f7ee4b3d9 .txt b/test/Nerdbank.GitVersioning.Tests/ManagedGit/pack-7d6b2c56ffb97eedb92f4e28583c093f7ee4b3d9 .txt similarity index 100% rename from src/NerdBank.GitVersioning.Tests/ManagedGit/pack-7d6b2c56ffb97eedb92f4e28583c093f7ee4b3d9 .txt rename to test/Nerdbank.GitVersioning.Tests/ManagedGit/pack-7d6b2c56ffb97eedb92f4e28583c093f7ee4b3d9 .txt diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/pack-7d6b2c56ffb97eedb92f4e28583c093f7ee4b3d9.idx b/test/Nerdbank.GitVersioning.Tests/ManagedGit/pack-7d6b2c56ffb97eedb92f4e28583c093f7ee4b3d9.idx similarity index 100% rename from src/NerdBank.GitVersioning.Tests/ManagedGit/pack-7d6b2c56ffb97eedb92f4e28583c093f7ee4b3d9.idx rename to test/Nerdbank.GitVersioning.Tests/ManagedGit/pack-7d6b2c56ffb97eedb92f4e28583c093f7ee4b3d9.idx diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/pack-7d6b2c56ffb97eedb92f4e28583c093f7ee4b3d9.pack b/test/Nerdbank.GitVersioning.Tests/ManagedGit/pack-7d6b2c56ffb97eedb92f4e28583c093f7ee4b3d9.pack similarity index 100% rename from src/NerdBank.GitVersioning.Tests/ManagedGit/pack-7d6b2c56ffb97eedb92f4e28583c093f7ee4b3d9.pack rename to test/Nerdbank.GitVersioning.Tests/ManagedGit/pack-7d6b2c56ffb97eedb92f4e28583c093f7ee4b3d9.pack diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/tree-bb36cf0ca445ccc8e5ce9cc88f7cf74128e96dc9 b/test/Nerdbank.GitVersioning.Tests/ManagedGit/tree-bb36cf0ca445ccc8e5ce9cc88f7cf74128e96dc9 similarity index 100% rename from src/NerdBank.GitVersioning.Tests/ManagedGit/tree-bb36cf0ca445ccc8e5ce9cc88f7cf74128e96dc9 rename to test/Nerdbank.GitVersioning.Tests/ManagedGit/tree-bb36cf0ca445ccc8e5ce9cc88f7cf74128e96dc9 diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/tree-f914b48023c7c804a4f3be780d451f31aef74ac1 b/test/Nerdbank.GitVersioning.Tests/ManagedGit/tree-f914b48023c7c804a4f3be780d451f31aef74ac1 similarity index 100% rename from src/NerdBank.GitVersioning.Tests/ManagedGit/tree-f914b48023c7c804a4f3be780d451f31aef74ac1 rename to test/Nerdbank.GitVersioning.Tests/ManagedGit/tree-f914b48023c7c804a4f3be780d451f31aef74ac1 diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/tree.bin b/test/Nerdbank.GitVersioning.Tests/ManagedGit/tree.bin similarity index 100% rename from src/NerdBank.GitVersioning.Tests/ManagedGit/tree.bin rename to test/Nerdbank.GitVersioning.Tests/ManagedGit/tree.bin diff --git a/src/NerdBank.GitVersioning.Tests/ManagedGit/tree.delta b/test/Nerdbank.GitVersioning.Tests/ManagedGit/tree.delta similarity index 100% rename from src/NerdBank.GitVersioning.Tests/ManagedGit/tree.delta rename to test/Nerdbank.GitVersioning.Tests/ManagedGit/tree.delta diff --git a/test/Nerdbank.GitVersioning.Tests/Nerdbank.GitVersioning.Tests.csproj b/test/Nerdbank.GitVersioning.Tests/Nerdbank.GitVersioning.Tests.csproj new file mode 100644 index 00000000..8c6333fd --- /dev/null +++ b/test/Nerdbank.GitVersioning.Tests/Nerdbank.GitVersioning.Tests.csproj @@ -0,0 +1,52 @@ + + + net7.0 + $(TargetFrameworks);net472 + true + true + full + false + false + true + + + + + + + false + Targets\%(FileName)%(Extension) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/NerdBank.GitVersioning.Tests/ReleaseManagerTests.cs b/test/Nerdbank.GitVersioning.Tests/ReleaseManagerTests.cs similarity index 87% rename from src/NerdBank.GitVersioning.Tests/ReleaseManagerTests.cs rename to test/Nerdbank.GitVersioning.Tests/ReleaseManagerTests.cs index fb3e248c..9ced1c7a 100644 --- a/src/NerdBank.GitVersioning.Tests/ReleaseManagerTests.cs +++ b/test/Nerdbank.GitVersioning.Tests/ReleaseManagerTests.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; using System.IO; using System.Linq; using LibGit2Sharp; @@ -14,6 +17,9 @@ using ReleaseVersionIncrement = Nerdbank.GitVersioning.VersionOptions.ReleaseVersionIncrement; using Version = System.Version; +#pragma warning disable SA1402 // File may only contain a single type +#pragma warning disable SA1649 // File name should match first type name + [Trait("Engine", "Managed")] public class ReleaseManagerManagedTests : ReleaseManagerTests { @@ -22,8 +28,9 @@ public ReleaseManagerManagedTests(ITestOutputHelper logger) { } + /// protected override GitContext CreateGitContext(string path, string committish = null) - => GitContext.Create(path, committish, writable: false); + => GitContext.Create(path, committish, engine: GitContext.Engine.ReadOnly); } [Trait("Engine", "LibGit2")] @@ -34,8 +41,9 @@ public ReleaseManagerLibGit2Tests(ITestOutputHelper logger) { } + /// protected override GitContext CreateGitContext(string path, string committish = null) - => GitContext.Create(path, committish, writable: true); + => GitContext.Create(path, committish, engine: GitContext.Engine.ReadWrite); } public abstract class ReleaseManagerTests : RepoTestBase @@ -59,7 +67,7 @@ public void PrepareRelease_DirtyWorkingDirecotory() this.InitializeSourceControl(); // create a file - File.WriteAllText(Path.Combine(this.RepoPath, "file1.txt"), ""); + File.WriteAllText(Path.Combine(this.RepoPath, "file1.txt"), string.Empty); // running PrepareRelease should result in an error // because there is a new file not under version control @@ -72,8 +80,8 @@ public void PrepareRelease_DirtyIndex() this.InitializeSourceControl(); // create a file and stage it - var filePath = Path.Combine(this.RepoPath, "file1.txt"); - File.WriteAllText(filePath, ""); + string filePath = Path.Combine(this.RepoPath, "file1.txt"); + File.WriteAllText(filePath, string.Empty); Commands.Stage(this.LibGit2Repository, filePath); // running PrepareRelease should result in an error @@ -103,7 +111,7 @@ public void PrepareRelease_InvalidBranchNameSetting() Release = new ReleaseOptions() { BranchName = "nameWithoutPlaceholder", - } + }, }; this.WriteVersionFile(versionOptions); @@ -124,7 +132,7 @@ public void PrepareRelease_ReleaseBranchAlreadyExists() Release = new ReleaseOptions() { BranchName = "release/v{version}", - } + }, }; this.WriteVersionFile(versionOptions); @@ -159,8 +167,8 @@ public void PrepareRelease_ReleaseBranch(string initialVersion, string releaseOp Version = SemanticVersion.Parse(initialVersion), Release = new ReleaseOptions() { - BranchName = releaseOptionsBranchName - } + BranchName = releaseOptionsBranchName, + }, }; var expectedVersionOptions = new VersionOptions() @@ -168,18 +176,18 @@ public void PrepareRelease_ReleaseBranch(string initialVersion, string releaseOp Version = SemanticVersion.Parse(resultingVersion), Release = new ReleaseOptions() { - BranchName = releaseOptionsBranchName - } + BranchName = releaseOptionsBranchName, + }, }; // create version.json this.WriteVersionFile(initialVersionOptions); // switch to release branch - var branchName = releaseBranchName; + string branchName = releaseBranchName; Commands.Checkout(this.LibGit2Repository, this.LibGit2Repository.CreateBranch(branchName)); - var tipBeforePrepareRelease = this.LibGit2Repository.Head.Tip; + Commit tipBeforePrepareRelease = this.LibGit2Repository.Head.Tip; // run PrepareRelease var releaseManager = new ReleaseManager(); @@ -187,7 +195,7 @@ public void PrepareRelease_ReleaseBranch(string initialVersion, string releaseOp // Check if a commit was created { - var updateVersionCommit = this.LibGit2Repository.Head.Tip; + Commit updateVersionCommit = this.LibGit2Repository.Head.Tip; Assert.NotEqual(tipBeforePrepareRelease.Id, updateVersionCommit.Id); Assert.Single(updateVersionCommit.Parents); Assert.Equal(updateVersionCommit.Parents.Single().Id, tipBeforePrepareRelease.Id); @@ -195,7 +203,7 @@ public void PrepareRelease_ReleaseBranch(string initialVersion, string releaseOp // check version on release branch { - var actualVersionOptions = this.GetVersionOptions(committish: this.LibGit2Repository.Branches[branchName].Tip.Sha); + VersionOptions actualVersionOptions = this.GetVersionOptions(committish: this.LibGit2Repository.Branches[branchName].Tip.Sha); Assert.Equal(expectedVersionOptions, actualVersionOptions); } } @@ -220,7 +228,7 @@ public void PrepeareRelease_ReleaseBranchWithVersionDecrement(string initialVers this.AssertError(() => new ReleaseManager().PrepareRelease(this.RepoPath, releaseUnstableTag), ReleasePreparationError.VersionDecrement); } - +#pragma warning disable SA1114 // Parameter list should follow declaration [Theory] // base test cases [InlineData("1.2-beta", null, null, null, null, null, null, "v1.2", "1.2", "1.3-alpha")] @@ -259,7 +267,7 @@ public void PrepeareRelease_ReleaseBranchWithVersionDecrement(string initialVers [InlineData("1.2-beta", null, null, null, null, "4.5", null, "v1.2", "1.2", "4.5-alpha")] [InlineData("1.2-beta.{height}", null, null, null, null, "4.5", null, "v1.2", "1.2", "4.5-alpha.{height}")] [InlineData("1.2-beta.{height}", null, null, "pre", null, "4.5.6", null, "v1.2", "1.2", "4.5.6-pre.{height}")] - // explicitly set version increment overriding the setting from ReleaseOptions + // explicitly set version increment overriding the setting from ReleaseOptions [InlineData("1.2-beta", null, ReleaseVersionIncrement.Minor, null, null, null, ReleaseVersionIncrement.Major, "v1.2", "1.2", "2.0-alpha")] [InlineData("1.2.3-beta", null, ReleaseVersionIncrement.Minor, null, null, null, ReleaseVersionIncrement.Build, "v1.2.3", "1.2.3", "1.2.4-alpha")] public void PrepareRelease_Master( @@ -277,6 +285,7 @@ public void PrepareRelease_Master( string resultingReleaseVersion, string resultingMainVersion) { +#pragma warning restore SA1114 // Parameter list should follow declaration // create and configure repository this.InitializeSourceControl(); @@ -288,8 +297,8 @@ public void PrepareRelease_Master( { VersionIncrement = releaseOptionsVersionIncrement, BranchName = releaseOptionsBranchName, - FirstUnstableTag = releaseOptionsFirstUnstableTag - } + FirstUnstableTag = releaseOptionsFirstUnstableTag, + }, }; this.WriteVersionFile(initialVersionOptions); @@ -300,8 +309,8 @@ public void PrepareRelease_Master( { VersionIncrement = releaseOptionsVersionIncrement, BranchName = releaseOptionsBranchName, - FirstUnstableTag = releaseOptionsFirstUnstableTag - } + FirstUnstableTag = releaseOptionsFirstUnstableTag, + }, }; var expectedVersionOptionsCurrentBrach = new VersionOptions() @@ -311,16 +320,16 @@ public void PrepareRelease_Master( { VersionIncrement = releaseOptionsVersionIncrement, BranchName = releaseOptionsBranchName, - FirstUnstableTag = releaseOptionsFirstUnstableTag - } + FirstUnstableTag = releaseOptionsFirstUnstableTag, + }, }; - var initialBranchName = this.LibGit2Repository.Head.FriendlyName; - var tipBeforePrepareRelease = this.LibGit2Repository.Head.Tip; + string initialBranchName = this.LibGit2Repository.Head.FriendlyName; + Commit tipBeforePrepareRelease = this.LibGit2Repository.Head.Tip; // prepare release var releaseManager = new ReleaseManager(); - releaseManager.PrepareRelease(this.RepoPath, releaseUnstableTag, (nextVersion is null ? null : Version.Parse(nextVersion)), parameterVersionIncrement); + releaseManager.PrepareRelease(this.RepoPath, releaseUnstableTag, nextVersion is null ? null : Version.Parse(nextVersion), parameterVersionIncrement); // check if a branch was created Assert.Contains(this.LibGit2Repository.Branches, branch => branch.FriendlyName == expectedBranchName); @@ -330,7 +339,7 @@ public void PrepareRelease_Master( // check if release branch contains a new commit // parent of new commit must be the commit before preparing the release - var releaseBranch = this.LibGit2Repository.Branches[expectedBranchName]; + Branch releaseBranch = this.LibGit2Repository.Branches[expectedBranchName]; { // If the original branch had no -prerelease tag, the release branch has no commit to author. if (string.IsNullOrEmpty(initialVersionOptions.Version.Prerelease)) @@ -347,7 +356,7 @@ public void PrepareRelease_Master( if (string.IsNullOrEmpty(initialVersionOptions.Version.Prerelease)) { // Verify that one commit was authored. - var incrementCommit = this.LibGit2Repository.Head.Tip; + Commit incrementCommit = this.LibGit2Repository.Head.Tip; Assert.Single(incrementCommit.Parents); Assert.Equal(tipBeforePrepareRelease.Id, incrementCommit.Parents.Single().Id); } @@ -356,24 +365,24 @@ public void PrepareRelease_Master( // check if current branch contains new commits // - one commit that updates the version (parent must be the commit before preparing the release) // - one commit merging the release branch back to master and resolving the conflict - var mergeCommit = this.LibGit2Repository.Head.Tip; + Commit mergeCommit = this.LibGit2Repository.Head.Tip; Assert.Equal(2, mergeCommit.Parents.Count()); Assert.Equal(releaseBranch.Tip.Id, mergeCommit.Parents.Skip(1).First().Id); - var updateVersionCommit = mergeCommit.Parents.First(); + Commit updateVersionCommit = mergeCommit.Parents.First(); Assert.Single(updateVersionCommit.Parents); Assert.Equal(tipBeforePrepareRelease.Id, updateVersionCommit.Parents.First().Id); } // check version on release branch { - var releaseBranchVersion = this.GetVersionOptions(committish: releaseBranch.Tip.Sha); + VersionOptions releaseBranchVersion = this.GetVersionOptions(committish: releaseBranch.Tip.Sha); Assert.Equal(expectedVersionOptionsReleaseBranch, releaseBranchVersion); } // check version on master branch { - var currentBranchVersion = this.GetVersionOptions(committish: this.LibGit2Repository.Head.Tip.Sha); + VersionOptions currentBranchVersion = this.GetVersionOptions(committish: this.LibGit2Repository.Head.Tip.Sha); Assert.Equal(expectedVersionOptionsCurrentBrach, currentBranchVersion); } } @@ -394,7 +403,7 @@ public void PrepareRelease_MasterWithVersionDecrement(string initialVersion, str // running PrepareRelease should result in an error // because we're setting the version on master to a lower version this.AssertError( - () => new ReleaseManager().PrepareRelease(this.RepoPath, releaseUnstableTag, (nextVersion is null ? null : Version.Parse(nextVersion))), + () => new ReleaseManager().PrepareRelease(this.RepoPath, releaseUnstableTag, nextVersion is null ? null : Version.Parse(nextVersion)), ReleasePreparationError.VersionDecrement); } @@ -412,7 +421,7 @@ public void PrepareRelease_MasterWithoutVersionIncrement(string initialVersion, // running PrepareRelease should result in an error // because we're trying to set master to the version it already has this.AssertError( - () => new ReleaseManager().PrepareRelease(this.RepoPath, null, (nextVersion is null ? null : Version.Parse(nextVersion))), + () => new ReleaseManager().PrepareRelease(this.RepoPath, null, nextVersion is null ? null : Version.Parse(nextVersion)), ReleasePreparationError.NoVersionIncrement); } @@ -422,7 +431,7 @@ public void PrepareRelease_DetachedHead() this.InitializeSourceControl(); this.WriteVersionFile("1.0", "-alpha"); Commands.Checkout(this.LibGit2Repository, this.LibGit2Repository.Head.Commits.First()); - var ex = Assert.Throws(() => new ReleaseManager().PrepareRelease(this.RepoPath)); + ReleasePreparationException ex = Assert.Throws(() => new ReleaseManager().PrepareRelease(this.RepoPath)); Assert.Equal(ReleasePreparationError.DetachedHead, ex.Error); } @@ -436,7 +445,7 @@ public void PrepareRelease_InvalidVersionIncrement() var versionOptions = new VersionOptions() { Version = SemanticVersion.Parse("1.2"), - Release = new ReleaseOptions() { VersionIncrement = ReleaseVersionIncrement.Build } + Release = new ReleaseOptions() { VersionIncrement = ReleaseVersionIncrement.Build }, }; this.WriteVersionFile(versionOptions); @@ -476,20 +485,19 @@ public void PrepareRelease_JsonOutput() Release = new ReleaseOptions() { BranchName = "v{version}", - VersionIncrement = ReleaseVersionIncrement.Minor - } + VersionIncrement = ReleaseVersionIncrement.Minor, + }, }; this.WriteVersionFile(versionOptions); - var currentBranchName = this.LibGit2Repository.Head.FriendlyName; - var releaseBranchName = "v1.0"; + string currentBranchName = this.LibGit2Repository.Head.FriendlyName; + string releaseBranchName = "v1.0"; // run release preparation var stdout = new StringWriter(); var releaseManager = new ReleaseManager(stdout); releaseManager.PrepareRelease(this.RepoPath, outputMode: ReleaseManager.ReleaseManagerOutputMode.Json); - // Expected output: // { // "CurrentBranch" : { @@ -503,13 +511,12 @@ public void PrepareRelease_JsonOutput() // "Version" : "", // } // } - var jsonOutput = JObject.Parse(stdout.ToString()); // check "CurrentBranch" output { - var expectedCommitId = this.LibGit2Repository.Branches[currentBranchName].Tip.Sha; - var expectedVersion = this.GetVersionOptions(committish: this.LibGit2Repository.Branches[currentBranchName].Tip.Sha).Version.ToString(); + string expectedCommitId = this.LibGit2Repository.Branches[currentBranchName].Tip.Sha; + string expectedVersion = this.GetVersionOptions(committish: this.LibGit2Repository.Branches[currentBranchName].Tip.Sha).Version.ToString(); var currentBranchOutput = jsonOutput.Property("CurrentBranch")?.Value as JObject; Assert.NotNull(currentBranchOutput); @@ -517,13 +524,12 @@ public void PrepareRelease_JsonOutput() Assert.Equal(currentBranchName, currentBranchOutput.GetValue("Name")?.ToString()); Assert.Equal(expectedCommitId, currentBranchOutput.GetValue("Commit")?.ToString()); Assert.Equal(expectedVersion, currentBranchOutput.GetValue("Version")?.ToString()); - } // Check "NewBranch" output { - var expectedCommitId = this.LibGit2Repository.Branches[releaseBranchName].Tip.Sha; - var expectedVersion = this.GetVersionOptions(committish: this.LibGit2Repository.Branches[releaseBranchName].Tip.Sha).Version.ToString(); + string expectedCommitId = this.LibGit2Repository.Branches[releaseBranchName].Tip.Sha; + string expectedVersion = this.GetVersionOptions(committish: this.LibGit2Repository.Branches[releaseBranchName].Tip.Sha).Version.ToString(); var newBranchOutput = jsonOutput.Property("NewBranch")?.Value as JObject; Assert.NotNull(newBranchOutput); @@ -547,11 +553,11 @@ public void PrepareRelease_JsonOutputWhenUpdatingReleaseBranch() Release = new ReleaseOptions() { BranchName = "v{version}", - VersionIncrement = ReleaseVersionIncrement.Minor - } + VersionIncrement = ReleaseVersionIncrement.Minor, + }, }; this.WriteVersionFile(versionOptions); - var branchName = "v1.0"; + string branchName = "v1.0"; // switch to release branch Commands.Checkout(this.LibGit2Repository, this.LibGit2Repository.CreateBranch(branchName)); @@ -561,7 +567,6 @@ public void PrepareRelease_JsonOutputWhenUpdatingReleaseBranch() var releaseManager = new ReleaseManager(stdout); releaseManager.PrepareRelease(this.RepoPath, outputMode: ReleaseManager.ReleaseManagerOutputMode.Json); - // Expected output: // { // "CurrentBranch" : { @@ -571,13 +576,12 @@ public void PrepareRelease_JsonOutputWhenUpdatingReleaseBranch() // }, // "NewBranch" : null // } - var jsonOutput = JObject.Parse(stdout.ToString()); // check "CurrentBranch" output { - var expectedCommitId = this.LibGit2Repository.Branches[branchName].Tip.Sha; - var expectedVersion = this.GetVersionOptions(committish: this.LibGit2Repository.Branches[branchName].Tip.Sha).Version.ToString(); + string expectedCommitId = this.LibGit2Repository.Branches[branchName].Tip.Sha; + string expectedVersion = this.GetVersionOptions(committish: this.LibGit2Repository.Branches[branchName].Tip.Sha).Version.ToString(); var currentBranchOutput = jsonOutput.Property("CurrentBranch")?.Value as JObject; Assert.NotNull(currentBranchOutput); @@ -585,8 +589,8 @@ public void PrepareRelease_JsonOutputWhenUpdatingReleaseBranch() Assert.Equal(branchName, currentBranchOutput.GetValue("Name")?.ToString()); Assert.Equal(expectedCommitId, currentBranchOutput.GetValue("Commit")?.ToString()); Assert.Equal(expectedVersion, currentBranchOutput.GetValue("Version")?.ToString()); - } + // Check "NewBranch" output { // no new branch was created, so "NewBranch" should be null @@ -621,19 +625,20 @@ public void PrepareRelease_ResetsVersionHeightOffset() // create version.json this.WriteVersionFile(initialVersionOptions); - var tipBeforePrepareRelease = this.LibGit2Repository.Head.Tip; + Commit tipBeforePrepareRelease = this.LibGit2Repository.Head.Tip; var releaseManager = new ReleaseManager(); releaseManager.PrepareRelease(this.RepoPath); this.SetContextToHead(); - var newVersion = this.Context.VersionFile.GetVersion(); + VersionOptions newVersion = this.Context.VersionFile.GetVersion(); Assert.Equal(expectedMainVersionOptions, newVersion); VersionOptions releaseVersion = this.GetVersionOptions(committish: this.LibGit2Repository.Branches["v1.0"].Tip.Sha); Assert.Equal(expectedReleaseVersionOptions, releaseVersion); } + /// protected override void InitializeSourceControl(bool withInitialCommit = true) { base.InitializeSourceControl(withInitialCommit); @@ -642,7 +647,7 @@ protected override void InitializeSourceControl(bool withInitialCommit = true) private void AssertError(Action testCode, ReleasePreparationError expectedError) { - var ex = Assert.Throws(testCode); + ReleasePreparationException ex = Assert.Throws(testCode); Assert.Equal(expectedError, ex.Error); } } diff --git a/src/NerdBank.GitVersioning.Tests/RepoTestBase.Helpers.cs b/test/Nerdbank.GitVersioning.Tests/RepoTestBase.Helpers.cs similarity index 87% rename from src/NerdBank.GitVersioning.Tests/RepoTestBase.Helpers.cs rename to test/Nerdbank.GitVersioning.Tests/RepoTestBase.Helpers.cs index 09ba29d4..8918cd28 100644 --- a/src/NerdBank.GitVersioning.Tests/RepoTestBase.Helpers.cs +++ b/test/Nerdbank.GitVersioning.Tests/RepoTestBase.Helpers.cs @@ -1,4 +1,7 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable using System; using System.Collections.Generic; @@ -11,9 +14,9 @@ public partial class RepoTestBase /// /// Gets the number of commits in the longest single path between /// the specified commit and the most distant ancestor (inclusive) - /// that set the version to the value at . + /// that set the version to the value at . /// - /// The commit, branch or tag to measure the height of. Leave as null to check HEAD. + /// The commit, branch or tag to measure the height of. Leave as null to check HEAD. /// The repo-relative project directory for which to calculate the version. /// The height of the commit. Always a positive integer. protected int GetVersionHeight(string? committish, string? repoRelativeProjectDirectory = null) => this.GetVersionOracle(repoRelativeProjectDirectory, committish).VersionHeight; diff --git a/src/NerdBank.GitVersioning.Tests/RepoTestBase.cs b/test/Nerdbank.GitVersioning.Tests/RepoTestBase.cs similarity index 89% rename from src/NerdBank.GitVersioning.Tests/RepoTestBase.cs rename to test/Nerdbank.GitVersioning.Tests/RepoTestBase.cs index 473f0bd9..d8edefb8 100644 --- a/src/NerdBank.GitVersioning.Tests/RepoTestBase.cs +++ b/test/Nerdbank.GitVersioning.Tests/RepoTestBase.cs @@ -1,4 +1,7 @@ -#nullable enable +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable using System; using System.Collections.Generic; @@ -33,6 +36,7 @@ public RepoTestBase(ITestOutputHelper logger) protected Repository? LibGit2Repository { get; private set; } + /// public void Dispose() { this.Dispose(true); @@ -47,13 +51,13 @@ public void Dispose() { Debug.Assert(path?.Length != 40, "commit passed as path"); - using var context = this.CreateGitContext(path is null ? this.RepoPath : Path.Combine(this.RepoPath, path), committish); + using GitContext? context = this.CreateGitContext(path is null ? this.RepoPath : Path.Combine(this.RepoPath, path), committish); return context.VersionFile.GetVersion(); } protected VersionOracle GetVersionOracle(string? path = null, string? committish = null) { - using var context = this.CreateGitContext(path is null ? this.RepoPath : Path.Combine(this.RepoPath, path), committish); + using GitContext? context = this.CreateGitContext(path is null ? this.RepoPath : Path.Combine(this.RepoPath, path), committish); return new VersionOracle(context); } @@ -65,7 +69,8 @@ protected string CreateDirectoryForNewRepo() do { repoPath = Path.Combine(Path.GetTempPath(), this.GetType().Name + "_" + Path.GetRandomFileName()); - } while (Directory.Exists(repoPath)); + } + while (Directory.Exists(repoPath)); Directory.CreateDirectory(repoPath); this.repoDirectories.Add(repoPath); @@ -112,7 +117,7 @@ protected virtual GitContext InitializeSourceControl(string repoPath, bool withI repo.Config.Set("user.name", this.Signer.Name, ConfigurationLevel.Local); repo.Config.Set("user.email", this.Signer.Email, ConfigurationLevel.Local); - foreach (var file in repo.RetrieveStatus().Untracked) + foreach (StatusEntry? file in repo.RetrieveStatus().Untracked) { if (!Path.GetFileName(file.FilePath).StartsWith("_git2_", StringComparison.Ordinal)) { @@ -176,7 +181,7 @@ protected void AddCommits(int count = 1) } bool localContextCreated = this.Context is null; - var context = this.Context ?? GitContext.Create(this.RepoPath, writable: true); + GitContext? context = this.Context ?? GitContext.Create(this.RepoPath, engine: GitContext.Engine.ReadWrite); try { string versionFilePath = context.VersionFile.SetVersion(Path.Combine(this.RepoPath, relativeDirectory), versionData); @@ -199,7 +204,7 @@ protected void AddCommits(int count = 1) if (this.LibGit2Repository is object) { Assumes.True(versionFilePath.StartsWith(this.RepoPath, StringComparison.OrdinalIgnoreCase)); - var relativeFilePath = versionFilePath.Substring(this.RepoPath.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + string? relativeFilePath = versionFilePath.Substring(this.RepoPath.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); Commands.Stage(this.LibGit2Repository, relativeFilePath); if (Path.GetExtension(relativeFilePath) == ".json") { diff --git a/src/NerdBank.GitVersioning.Tests/SemanticVersionExtensionsTests.cs b/test/Nerdbank.GitVersioning.Tests/SemanticVersionExtensionsTests.cs similarity index 89% rename from src/NerdBank.GitVersioning.Tests/SemanticVersionExtensionsTests.cs rename to test/Nerdbank.GitVersioning.Tests/SemanticVersionExtensionsTests.cs index 1631c65a..2213809b 100644 --- a/src/NerdBank.GitVersioning.Tests/SemanticVersionExtensionsTests.cs +++ b/test/Nerdbank.GitVersioning.Tests/SemanticVersionExtensionsTests.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; using System.Collections.Generic; using System.Reflection; using Nerdbank.GitVersioning; @@ -8,7 +11,7 @@ using static Nerdbank.GitVersioning.VersionOptions; /// -/// Tests for +/// Tests for . /// public class SemanticVersionExtensionsTests { @@ -38,7 +41,7 @@ public void Increment(string currentVersionString, ReleaseVersionIncrement incre var currentVersion = SemanticVersion.Parse(currentVersionString); var expectedVersion = SemanticVersion.Parse(expectedVersionString); - var actualVersion = currentVersion.Increment(increment); + SemanticVersion actualVersion = currentVersion.Increment(increment); Assert.Equal(expectedVersion, actualVersion); } @@ -67,7 +70,7 @@ public void Increment_InvalidIncrement(string currentVersionString, ReleaseVersi // multiple prerelease tags [InlineData("1.2-alpha.preview", "beta", "1.2-beta.preview")] [InlineData("1.2-alpha.preview", "-beta", "1.2-beta.preview")] - [InlineData("1.2-alpha.preview+metadata", "beta", "1.2-beta.preview+metadata")] + [InlineData("1.2-alpha.preview+metadata", "beta", "1.2-beta.preview+metadata")] [InlineData("1.2.3-alpha.preview", "beta", "1.2.3-beta.preview")] [InlineData("1.2-alpha.{height}", "beta", "1.2-beta.{height}")] // remove tag @@ -77,9 +80,8 @@ public void SetFirstPrereleaseTag(string currentVersionString, string newTag, st var currentVersion = SemanticVersion.Parse(currentVersionString); var expectedVersion = SemanticVersion.Parse(expectedVersionString); - var actualVersion = currentVersion.SetFirstPrereleaseTag(newTag); + SemanticVersion actualVersion = currentVersion.SetFirstPrereleaseTag(newTag); Assert.Equal(expectedVersion, actualVersion); } } - diff --git a/src/NerdBank.GitVersioning.Tests/SemanticVersionTests.cs b/test/Nerdbank.GitVersioning.Tests/SemanticVersionTests.cs similarity index 96% rename from src/NerdBank.GitVersioning.Tests/SemanticVersionTests.cs rename to test/Nerdbank.GitVersioning.Tests/SemanticVersionTests.cs index bde7b5d4..a22feb7a 100644 --- a/src/NerdBank.GitVersioning.Tests/SemanticVersionTests.cs +++ b/test/Nerdbank.GitVersioning.Tests/SemanticVersionTests.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -50,7 +53,6 @@ public void TryParse() Assert.Equal(2, result.Version.Minor); Assert.Equal("-pre.5.8-foo", result.Prerelease); Assert.Equal("+build-metadata.id1.2", result.BuildMetadata); - } [Fact] @@ -156,6 +158,5 @@ public void ToStringTests() Assert.Equal("1.2+buildInfo", vb.ToString()); var vpb = new SemanticVersion(new Version(1, 2), "-pre", "+buildInfo"); Assert.Equal("1.2-pre+buildInfo", vpb.ToString()); - } } diff --git a/test/Nerdbank.GitVersioning.Tests/SomeGitBuildIntegrationTests.cs b/test/Nerdbank.GitVersioning.Tests/SomeGitBuildIntegrationTests.cs new file mode 100644 index 00000000..5ced01af --- /dev/null +++ b/test/Nerdbank.GitVersioning.Tests/SomeGitBuildIntegrationTests.cs @@ -0,0 +1,720 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; +using System.Diagnostics; +using System.Globalization; +using System.Reflection; +using System.Xml; +using LibGit2Sharp; +using Microsoft.Build.Construction; +using Microsoft.Build.Framework; +using Microsoft.CodeAnalysis; +using Nerdbank.GitVersioning; +using Validation; +using Xunit; +using Xunit.Abstractions; +using Version = System.Version; + +/// +/// The base class for tests that require some actual git implementation behind it. +/// In other words, NOT the disabled engine implementation. +/// +public abstract class SomeGitBuildIntegrationTests : BuildIntegrationTests +{ + protected SomeGitBuildIntegrationTests(ITestOutputHelper logger) + : base(logger) + { + } + + [Fact] + public async Task GetBuildVersion_WithThreeVersionIntegers() + { + VersionOptions workingCopyVersion = new VersionOptions + { + Version = SemanticVersion.Parse("7.8.9-beta.3"), + SemVer1NumericIdentifierPadding = 1, + }; + this.WriteVersionFile(workingCopyVersion); + this.InitializeSourceControl(); + BuildResults buildResult = await this.BuildAsync(); + this.AssertStandardProperties(workingCopyVersion, buildResult); + } + + [Fact] + public async Task GetBuildVersion_OutsideGit_PointingToGit() + { + // Write a version file to the 'virtualized' repo. + string version = "3.4"; + this.WriteVersionFile(version); + + string repoRelativeProjectPath = this.testProject.FullPath.Substring(this.RepoPath.Length + 1); + + // Update the repo path so we create the 'normal' one elsewhere + this.RepoPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + this.InitializeSourceControl(); + + // Write the same version file to the 'real' repo + this.WriteVersionFile(version); + + // Point the project to the 'real' repo + this.testProject.AddProperty("GitRepoRoot", this.RepoPath); + this.testProject.AddProperty("ProjectPathRelativeToGitRepoRoot", repoRelativeProjectPath); + + BuildResults buildResult = await this.BuildAsync(); + + var workingCopyVersion = VersionOptions.FromVersion(new Version(version)); + + this.AssertStandardProperties(workingCopyVersion, buildResult); + } + + [Fact] + public async Task GetBuildVersion_In_Git_But_Without_Commits() + { + Repository.Init(this.RepoPath); + var repo = new Repository(this.RepoPath); // do not assign Repo property to avoid commits being generated later + this.WriteVersionFile("3.4"); + Assumes.False(repo.Head.Commits.Any()); // verification that the test is doing what it claims + BuildResults buildResult = await this.BuildAsync(); + Assert.Equal("3.4.0.0", buildResult.BuildVersion); + Assert.Equal("3.4.0", buildResult.AssemblyInformationalVersion); + } + + [Fact] + public async Task GetBuildVersion_In_Git_But_Head_Lacks_VersionFile() + { + Repository.Init(this.RepoPath); + var repo = new Repository(this.RepoPath); // do not assign Repo property to avoid commits being generated later + repo.Commit("empty", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); + this.WriteVersionFile("3.4"); + Assumes.True(repo.Index[VersionFile.JsonFileName] is null); + BuildResults buildResult = await this.BuildAsync(); + Assert.Equal("3.4.0." + this.GetVersion().Revision, buildResult.BuildVersion); + Assert.Equal("3.4.0+" + repo.Head.Tip.Id.Sha.Substring(0, VersionOptions.DefaultGitCommitIdShortFixedLength), buildResult.AssemblyInformationalVersion); + } + + [Fact] + public async Task GetBuildVersion_In_Git_But_WorkingCopy_Has_Changes() + { + const string majorMinorVersion = "5.8"; + const string prerelease = ""; + + this.WriteVersionFile(majorMinorVersion, prerelease); + this.InitializeSourceControl(); + var workingCopyVersion = VersionOptions.FromVersion(new Version("6.0")); + this.Context.VersionFile.SetVersion(this.RepoPath, workingCopyVersion); + BuildResults buildResult = await this.BuildAsync(); + this.AssertStandardProperties(workingCopyVersion, buildResult); + } + + [Fact] + public async Task GetBuildVersion_In_Git_No_VersionFile_At_All() + { + Repository.Init(this.RepoPath); + var repo = new Repository(this.RepoPath); // do not assign Repo property to avoid commits being generated later + repo.Commit("empty", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); + BuildResults buildResult = await this.BuildAsync(); + Assert.Equal("0.0.0." + this.GetVersion().Revision, buildResult.BuildVersion); + Assert.Equal("0.0.0+" + repo.Head.Tip.Id.Sha.Substring(0, VersionOptions.DefaultGitCommitIdShortFixedLength), buildResult.AssemblyInformationalVersion); + } + + [Fact] + public async Task GetBuildVersion_In_Git_With_Version_File_In_Subdirectory_Works() + { + const string majorMinorVersion = "5.8"; + const string prerelease = ""; + const string subdirectory = "projdir"; + + this.WriteVersionFile(majorMinorVersion, prerelease, subdirectory); + this.InitializeSourceControl(); + this.AddCommits(this.random.Next(15)); + BuildResults buildResult = await this.BuildAsync(); + this.AssertStandardProperties(VersionOptions.FromVersion(new Version(majorMinorVersion)), buildResult, subdirectory); + } + + [Fact] + public async Task GetBuildVersion_In_Git_With_Version_File_In_Root_And_Subdirectory_Works() + { + var rootVersionSpec = new VersionOptions + { + Version = SemanticVersion.Parse("14.1"), + AssemblyVersion = new VersionOptions.AssemblyVersionOptions(new Version(14, 0)), + }; + var subdirVersionSpec = new VersionOptions { Version = SemanticVersion.Parse("11.0") }; + const string subdirectory = "projdir"; + + this.WriteVersionFile(rootVersionSpec); + this.WriteVersionFile(subdirVersionSpec, subdirectory); + this.InitializeSourceControl(); + this.AddCommits(this.random.Next(15)); + BuildResults buildResult = await this.BuildAsync(); + this.AssertStandardProperties(subdirVersionSpec, buildResult, subdirectory); + } + + [Fact] + public async Task GetBuildVersion_In_Git_With_Version_File_In_Root_And_Project_In_Root_Works() + { + var rootVersionSpec = new VersionOptions + { + Version = SemanticVersion.Parse("14.1"), + AssemblyVersion = new VersionOptions.AssemblyVersionOptions(new Version(14, 0)), + }; + + this.WriteVersionFile(rootVersionSpec); + this.InitializeSourceControl(); + this.AddCommits(this.random.Next(15)); + this.testProject = this.CreateProjectRootElement(this.RepoPath, "root.proj"); + BuildResults buildResult = await this.BuildAsync(); + this.AssertStandardProperties(rootVersionSpec, buildResult); + } + + [Fact] + public async Task GetBuildVersion_StablePreRelease() + { + const string majorMinorVersion = "5.8"; + const string prerelease = ""; + + this.WriteVersionFile(majorMinorVersion, prerelease); + this.InitializeSourceControl(); + this.AddCommits(this.random.Next(15)); + BuildResults buildResult = await this.BuildAsync(); + this.AssertStandardProperties(VersionOptions.FromVersion(new Version(majorMinorVersion)), buildResult); + } + + [Fact] + public async Task GetBuildVersion_StableRelease() + { + const string majorMinorVersion = "5.8"; + const string prerelease = ""; + + this.WriteVersionFile(majorMinorVersion, prerelease); + this.InitializeSourceControl(); + this.AddCommits(this.random.Next(15)); + this.globalProperties["PublicRelease"] = "true"; + BuildResults buildResult = await this.BuildAsync(); + this.AssertStandardProperties(VersionOptions.FromVersion(new Version(majorMinorVersion)), buildResult); + + Version version = this.GetVersion(); + Assert.Equal($"{version.Major}.{version.Minor}.{buildResult.GitVersionHeight}", buildResult.NuGetPackageVersion); + } + + [Fact] + public async Task GetBuildVersion_UnstablePreRelease() + { + const string majorMinorVersion = "5.8"; + const string prerelease = "-beta"; + + this.WriteVersionFile(majorMinorVersion, prerelease); + this.InitializeSourceControl(); + this.AddCommits(this.random.Next(15)); + BuildResults buildResult = await this.BuildAsync(); + this.AssertStandardProperties(VersionOptions.FromVersion(new Version(majorMinorVersion), prerelease), buildResult); + } + + [Fact] + public async Task GetBuildVersion_UnstableRelease() + { + const string majorMinorVersion = "5.8"; + const string prerelease = "-beta"; + + this.WriteVersionFile(majorMinorVersion, prerelease); + this.InitializeSourceControl(); + this.AddCommits(this.random.Next(15)); + this.globalProperties["PublicRelease"] = "true"; + BuildResults buildResult = await this.BuildAsync(); + this.AssertStandardProperties(VersionOptions.FromVersion(new Version(majorMinorVersion), prerelease), buildResult); + } + + [Fact] + public async Task GetBuildVersion_CustomAssemblyVersion() + { + this.WriteVersionFile("14.0"); + this.InitializeSourceControl(); + var versionOptions = new VersionOptions + { + Version = new SemanticVersion(new Version(14, 1)), + AssemblyVersion = new VersionOptions.AssemblyVersionOptions(new Version(14, 0)), + }; + this.WriteVersionFile(versionOptions); + BuildResults buildResult = await this.BuildAsync(); + this.AssertStandardProperties(versionOptions, buildResult); + } + + [Theory] + [InlineData(VersionOptions.VersionPrecision.Major)] + [InlineData(VersionOptions.VersionPrecision.Build)] + [InlineData(VersionOptions.VersionPrecision.Revision)] + public async Task GetBuildVersion_CustomAssemblyVersionWithPrecision(VersionOptions.VersionPrecision precision) + { + var versionOptions = new VersionOptions + { + Version = new SemanticVersion("14.1"), + AssemblyVersion = new VersionOptions.AssemblyVersionOptions + { + Version = new Version("15.2"), + Precision = precision, + }, + }; + this.WriteVersionFile(versionOptions); + this.InitializeSourceControl(); + BuildResults buildResult = await this.BuildAsync(); + this.AssertStandardProperties(versionOptions, buildResult); + } + + [Theory] + [InlineData(VersionOptions.VersionPrecision.Major)] + [InlineData(VersionOptions.VersionPrecision.Build)] + [InlineData(VersionOptions.VersionPrecision.Revision)] + public async Task GetBuildVersion_CustomAssemblyVersionPrecision(VersionOptions.VersionPrecision precision) + { + var versionOptions = new VersionOptions + { + Version = new SemanticVersion("14.1"), + AssemblyVersion = new VersionOptions.AssemblyVersionOptions + { + Precision = precision, + }, + }; + this.WriteVersionFile(versionOptions); + this.InitializeSourceControl(); + BuildResults buildResult = await this.BuildAsync(); + this.AssertStandardProperties(versionOptions, buildResult); + } + + [Fact] + public async Task GetBuildVersion_CustomBuildNumberOffset() + { + this.WriteVersionFile("14.0"); + this.InitializeSourceControl(); + var versionOptions = new VersionOptions + { + Version = new SemanticVersion(new Version(14, 1)), + VersionHeightOffset = 5, + }; + this.WriteVersionFile(versionOptions); + BuildResults buildResult = await this.BuildAsync(); + this.AssertStandardProperties(versionOptions, buildResult); + } + + [Fact] + public async Task GetBuildVersion_OverrideBuildNumberOffset() + { + this.WriteVersionFile("14.0"); + this.InitializeSourceControl(); + var versionOptions = new VersionOptions + { + Version = new SemanticVersion(new Version(14, 1)), + }; + this.WriteVersionFile(versionOptions); + this.testProject.AddProperty("OverrideBuildNumberOffset", "10"); + BuildResults buildResult = await this.BuildAsync(); + Assert.StartsWith("14.1.11.", buildResult.AssemblyFileVersion); + } + + [Fact] + public async Task GetBuildVersion_Minus1BuildOffset_NotYetCommitted() + { + this.WriteVersionFile("14.0"); + this.InitializeSourceControl(); + var versionOptions = new VersionOptions + { + Version = new SemanticVersion(new Version(14, 1)), + VersionHeightOffset = -1, + }; + this.Context.VersionFile.SetVersion(this.RepoPath, versionOptions); + BuildResults buildResult = await this.BuildAsync(); + this.AssertStandardProperties(versionOptions, buildResult); + } + + [Theory] + [InlineData(0)] + [InlineData(21)] + public async Task GetBuildVersion_BuildNumberSpecifiedInVersionJson(int buildNumber) + { + var versionOptions = new VersionOptions + { + Version = SemanticVersion.Parse("14.0." + buildNumber), + }; + this.WriteVersionFile(versionOptions); + this.InitializeSourceControl(); + BuildResults buildResult = await this.BuildAsync(); + this.AssertStandardProperties(versionOptions, buildResult); + } + + [Fact] + public async Task PublicRelease_RegEx_Unsatisfied() + { + var versionOptions = new VersionOptions + { + Version = SemanticVersion.Parse("1.0"), + PublicReleaseRefSpec = new string[] { "^refs/heads/release$" }, + }; + this.WriteVersionFile(versionOptions); + this.InitializeSourceControl(); + + // Just build "master", which doesn't conform to the regex. + BuildResults buildResult = await this.BuildAsync(); + Assert.False(buildResult.PublicRelease); + this.AssertStandardProperties(versionOptions, buildResult); + } + + [Theory] + [MemberData(nameof(CloudBuildOfBranch), "release")] + public async Task PublicRelease_RegEx_SatisfiedByCI(IReadOnlyDictionary serverProperties) + { + var versionOptions = new VersionOptions + { + Version = SemanticVersion.Parse("1.0"), + PublicReleaseRefSpec = new string[] + { + "^refs/heads/release$", + "^refs/tags/release$", + }, + }; + this.WriteVersionFile(versionOptions); + this.InitializeSourceControl(); + + // Don't actually switch the checked out branch in git. CI environment variables + // should take precedence over actual git configuration. (Why? because these variables may + // retain information about which tag was checked out on a detached head). + using (ApplyEnvironmentVariables(serverProperties)) + { + BuildResults buildResult = await this.BuildAsync(); + Assert.True(buildResult.PublicRelease); + this.AssertStandardProperties(versionOptions, buildResult); + } + } + + [Theory] + [Trait("TestCategory", "FailsInCloudTest")] + [MemberData(nameof(CloudBuildVariablesData))] + public async Task CloudBuildVariables_SetInCI(IReadOnlyDictionary properties, string expectedMessage, bool setAllVariables) + { + using (ApplyEnvironmentVariables(properties)) + { + string keyName = "n1"; + string value = "v1"; + this.testProject.AddItem("CloudBuildVersionVars", keyName, new Dictionary { { "Value", value } }); + + string alwaysExpectedMessage = UnitTestCloudBuildPrefix + expectedMessage + .Replace("{NAME}", keyName) + .Replace("{VALUE}", value); + + var versionOptions = new VersionOptions + { + Version = SemanticVersion.Parse("1.0"), + CloudBuild = new VersionOptions.CloudBuildOptions { SetAllVariables = setAllVariables, SetVersionVariables = true }, + }; + this.WriteVersionFile(versionOptions); + this.InitializeSourceControl(); + + BuildResults buildResult = await this.BuildAsync(); + this.AssertStandardProperties(versionOptions, buildResult); + + // Assert GitBuildVersion was set + string conditionallyExpectedMessage = UnitTestCloudBuildPrefix + expectedMessage + .Replace("{NAME}", "GitBuildVersion") + .Replace("{VALUE}", buildResult.BuildVersion); + Assert.Contains(alwaysExpectedMessage, buildResult.LoggedEvents.Select(e => e.Message.TrimEnd())); + Assert.Contains(conditionallyExpectedMessage, buildResult.LoggedEvents.Select(e => e.Message.TrimEnd())); + + // Assert GitBuildVersionSimple was set + conditionallyExpectedMessage = UnitTestCloudBuildPrefix + expectedMessage + .Replace("{NAME}", "GitBuildVersionSimple") + .Replace("{VALUE}", buildResult.BuildVersionSimple); + Assert.Contains(alwaysExpectedMessage, buildResult.LoggedEvents.Select(e => e.Message.TrimEnd())); + Assert.Contains(conditionallyExpectedMessage, buildResult.LoggedEvents.Select(e => e.Message.TrimEnd())); + + // Assert that project properties are also set. + Assert.Equal(buildResult.BuildVersion, buildResult.GitBuildVersion); + Assert.Equal(buildResult.BuildVersionSimple, buildResult.GitBuildVersionSimple); + Assert.Equal(buildResult.AssemblyInformationalVersion, buildResult.GitAssemblyInformationalVersion); + + if (setAllVariables) + { + // Assert that some project properties were set as build properties prefaced with "NBGV_". + Assert.Equal(buildResult.GitCommitIdShort, buildResult.NBGV_GitCommitIdShort); + Assert.Equal(buildResult.NuGetPackageVersion, buildResult.NBGV_NuGetPackageVersion); + } + else + { + // Assert that the NBGV_ prefixed properties are *not* set. + Assert.Equal(string.Empty, buildResult.NBGV_GitCommitIdShort); + Assert.Equal(string.Empty, buildResult.NBGV_NuGetPackageVersion); + } + + // Assert that env variables were also set in context of the build. + Assert.Contains( + buildResult.LoggedEvents, + e => string.Equals(e.Message, $"n1=v1", StringComparison.OrdinalIgnoreCase) || string.Equals(e.Message, $"n1='v1'", StringComparison.OrdinalIgnoreCase)); + + versionOptions.CloudBuild.SetVersionVariables = false; + this.WriteVersionFile(versionOptions); + this.SetContextToHead(); + buildResult = await this.BuildAsync(); + this.AssertStandardProperties(versionOptions, buildResult); + + // Assert GitBuildVersion was not set + conditionallyExpectedMessage = UnitTestCloudBuildPrefix + expectedMessage + .Replace("{NAME}", "GitBuildVersion") + .Replace("{VALUE}", buildResult.BuildVersion); + Assert.Contains(alwaysExpectedMessage, buildResult.LoggedEvents.Select(e => e.Message.TrimEnd())); + Assert.DoesNotContain(conditionallyExpectedMessage, buildResult.LoggedEvents.Select(e => e.Message.TrimEnd())); + Assert.NotEqual(buildResult.BuildVersion, buildResult.BuildResult.ProjectStateAfterBuild.GetPropertyValue("GitBuildVersion")); + + // Assert GitBuildVersionSimple was not set + conditionallyExpectedMessage = UnitTestCloudBuildPrefix + expectedMessage + .Replace("{NAME}", "GitBuildVersionSimple") + .Replace("{VALUE}", buildResult.BuildVersionSimple); + Assert.Contains(alwaysExpectedMessage, buildResult.LoggedEvents.Select(e => e.Message.TrimEnd())); + Assert.DoesNotContain(conditionallyExpectedMessage, buildResult.LoggedEvents.Select(e => e.Message.TrimEnd())); + Assert.NotEqual(buildResult.BuildVersionSimple, buildResult.BuildResult.ProjectStateAfterBuild.GetPropertyValue("GitBuildVersionSimple")); + } + } + + [Theory] + [MemberData(nameof(BuildNumberData))] + public async Task BuildNumber_SetInCI(VersionOptions versionOptions, IReadOnlyDictionary properties, string expectedBuildNumberMessage) + { + this.WriteVersionFile(versionOptions); + this.InitializeSourceControl(); + using (ApplyEnvironmentVariables(properties)) + { + BuildResults buildResult = await this.BuildAsync(); + this.AssertStandardProperties(versionOptions, buildResult); + expectedBuildNumberMessage = expectedBuildNumberMessage.Replace("{CLOUDBUILDNUMBER}", buildResult.CloudBuildNumber); + Assert.Contains(UnitTestCloudBuildPrefix + expectedBuildNumberMessage, buildResult.LoggedEvents.Select(e => e.Message.TrimEnd())); + } + + versionOptions.CloudBuild.BuildNumber.Enabled = false; + this.WriteVersionFile(versionOptions); + using (ApplyEnvironmentVariables(properties)) + { + BuildResults buildResult = await this.BuildAsync(); + this.AssertStandardProperties(versionOptions, buildResult); + expectedBuildNumberMessage = expectedBuildNumberMessage.Replace("{CLOUDBUILDNUMBER}", buildResult.CloudBuildNumber); + Assert.DoesNotContain(UnitTestCloudBuildPrefix + expectedBuildNumberMessage, buildResult.LoggedEvents.Select(e => e.Message.TrimEnd())); + } + } + + [Theory] + [PairwiseData] + public async Task BuildNumber_VariousOptions(bool isPublic, VersionOptions.CloudBuildNumberCommitWhere where, VersionOptions.CloudBuildNumberCommitWhen when, [CombinatorialValues(0, 1, 2)] int extraBuildMetadataCount, [CombinatorialValues(1, 2)] int semVer) + { + VersionOptions versionOptions = BuildNumberVersionOptionsBasis; + versionOptions.CloudBuild.BuildNumber.IncludeCommitId.Where = where; + versionOptions.CloudBuild.BuildNumber.IncludeCommitId.When = when; + versionOptions.NuGetPackageVersion = new VersionOptions.NuGetPackageVersionOptions + { + SemVer = semVer, + }; + this.WriteVersionFile(versionOptions); + this.InitializeSourceControl(); + + this.globalProperties["PublicRelease"] = isPublic.ToString(); + for (int i = 0; i < extraBuildMetadataCount; i++) + { + this.testProject.AddItem("BuildMetadata", $"A{i}"); + } + + BuildResults buildResult = await this.BuildAsync(); + this.AssertStandardProperties(versionOptions, buildResult); + } + + [Fact] + public void GitLab_BuildTag() + { + // Based on the values defined in https://docs.gitlab.com/ee/ci/variables/#syntax-of-environment-variables-in-job-scripts + using (ApplyEnvironmentVariables( + CloudBuild.SuppressEnvironment.SetItems( + new Dictionary() + { + { "CI_COMMIT_TAG", "1.0.0" }, + { "CI_COMMIT_SHA", "1ecfd275763eff1d6b4844ea3168962458c9f27a" }, + { "GITLAB_CI", "true" }, + { "SYSTEM_TEAMPROJECTID", string.Empty }, + }))) + { + ICloudBuild activeCloudBuild = Nerdbank.GitVersioning.CloudBuild.Active; + Assert.NotNull(activeCloudBuild); + Assert.Null(activeCloudBuild.BuildingBranch); + Assert.Equal("refs/tags/1.0.0", activeCloudBuild.BuildingTag); + Assert.Equal("1ecfd275763eff1d6b4844ea3168962458c9f27a", activeCloudBuild.GitCommitId); + Assert.True(activeCloudBuild.IsApplicable); + Assert.False(activeCloudBuild.IsPullRequest); + } + } + + [Fact] + public void GitLab_BuildBranch() + { + // Based on the values defined in https://docs.gitlab.com/ee/ci/variables/#syntax-of-environment-variables-in-job-scripts + using (ApplyEnvironmentVariables( + CloudBuild.SuppressEnvironment.SetItems( + new Dictionary() + { + { "CI_COMMIT_REF_NAME", "master" }, + { "CI_COMMIT_SHA", "1ecfd275763eff1d6b4844ea3168962458c9f27a" }, + { "GITLAB_CI", "true" }, + }))) + { + ICloudBuild activeCloudBuild = Nerdbank.GitVersioning.CloudBuild.Active; + Assert.NotNull(activeCloudBuild); + Assert.Equal("refs/heads/master", activeCloudBuild.BuildingBranch); + Assert.Null(activeCloudBuild.BuildingTag); + Assert.Equal("1ecfd275763eff1d6b4844ea3168962458c9f27a", activeCloudBuild.GitCommitId); + Assert.True(activeCloudBuild.IsApplicable); + Assert.False(activeCloudBuild.IsPullRequest); + } + } + + [Fact] + public async Task PublicRelease_RegEx_SatisfiedByCheckedOutBranch() + { + var versionOptions = new VersionOptions + { + Version = SemanticVersion.Parse("1.0"), + PublicReleaseRefSpec = new string[] { "^refs/heads/release$" }, + }; + this.WriteVersionFile(versionOptions); + this.InitializeSourceControl(); + + using (ApplyEnvironmentVariables(CloudBuild.SuppressEnvironment)) + { + // Check out a branch that conforms. + Branch releaseBranch = this.LibGit2Repository.CreateBranch("release"); + Commands.Checkout(this.LibGit2Repository, releaseBranch); + BuildResults buildResult = await this.BuildAsync(); + Assert.True(buildResult.PublicRelease); + this.AssertStandardProperties(versionOptions, buildResult); + } + } + + // This test builds projects using 'classic' MSBuild projects, which target net45. + // This is not supported on Linux. + [WindowsTheory] + [PairwiseData] + public async Task AssemblyInfo(bool isVB, bool includeNonVersionAttributes, bool gitRepo, bool isPrerelease, bool isPublicRelease) + { + this.WriteVersionFile(prerelease: isPrerelease ? "-beta" : string.Empty); + if (gitRepo) + { + this.InitializeSourceControl(); + } + + if (isVB) + { + this.MakeItAVBProject(); + } + + if (includeNonVersionAttributes) + { + this.testProject.AddProperty("NBGV_EmitNonVersionCustomAttributes", "true"); + } + + this.globalProperties["PublicRelease"] = isPublicRelease ? "true" : "false"; + + BuildResults result = await this.BuildAsync("Build", logVerbosity: LoggerVerbosity.Minimal); + string assemblyPath = result.BuildResult.ProjectStateAfterBuild.GetPropertyValue("TargetPath"); + string versionFileContent = File.ReadAllText(Path.Combine(this.projectDirectory, result.BuildResult.ProjectStateAfterBuild.GetPropertyValue("VersionSourceFile"))); + this.Logger.WriteLine(versionFileContent); + + var assembly = Assembly.LoadFile(assemblyPath); + + AssemblyFileVersionAttribute assemblyFileVersion = assembly.GetCustomAttribute(); + AssemblyInformationalVersionAttribute assemblyInformationalVersion = assembly.GetCustomAttribute(); + AssemblyTitleAttribute assemblyTitle = assembly.GetCustomAttribute(); + AssemblyProductAttribute assemblyProduct = assembly.GetCustomAttribute(); + AssemblyCompanyAttribute assemblyCompany = assembly.GetCustomAttribute(); + AssemblyCopyrightAttribute assemblyCopyright = assembly.GetCustomAttribute(); + Type thisAssemblyClass = assembly.GetType("ThisAssembly") ?? assembly.GetType("TestNamespace.ThisAssembly"); + Assert.NotNull(thisAssemblyClass); + + Assert.Equal(new Version(result.AssemblyVersion), assembly.GetName().Version); + Assert.Equal(result.AssemblyFileVersion, assemblyFileVersion.Version); + Assert.Equal(result.AssemblyInformationalVersion, assemblyInformationalVersion.InformationalVersion); + if (includeNonVersionAttributes) + { + Assert.Equal(result.AssemblyTitle, assemblyTitle.Title); + Assert.Equal(result.AssemblyProduct, assemblyProduct.Product); + Assert.Equal(result.AssemblyCompany, assemblyCompany.Company); + Assert.Equal(result.AssemblyCopyright, assemblyCopyright.Copyright); + } + else + { + Assert.Null(assemblyTitle); + Assert.Null(assemblyProduct); + Assert.Null(assemblyCompany); + Assert.Null(assemblyCopyright); + } + + const BindingFlags fieldFlags = BindingFlags.Static | BindingFlags.NonPublic; + Assert.Equal(result.AssemblyVersion, thisAssemblyClass.GetField("AssemblyVersion", fieldFlags).GetValue(null)); + Assert.Equal(result.AssemblyFileVersion, thisAssemblyClass.GetField("AssemblyFileVersion", fieldFlags).GetValue(null)); + Assert.Equal(result.AssemblyInformationalVersion, thisAssemblyClass.GetField("AssemblyInformationalVersion", fieldFlags).GetValue(null)); + Assert.Equal(result.AssemblyName, thisAssemblyClass.GetField("AssemblyName", fieldFlags).GetValue(null)); + Assert.Equal(result.RootNamespace, thisAssemblyClass.GetField("RootNamespace", fieldFlags).GetValue(null)); + Assert.Equal(result.AssemblyConfiguration, thisAssemblyClass.GetField("AssemblyConfiguration", fieldFlags).GetValue(null)); + Assert.Equal(result.AssemblyTitle, thisAssemblyClass.GetField("AssemblyTitle", fieldFlags)?.GetValue(null)); + Assert.Equal(result.AssemblyProduct, thisAssemblyClass.GetField("AssemblyProduct", fieldFlags)?.GetValue(null)); + Assert.Equal(result.AssemblyCompany, thisAssemblyClass.GetField("AssemblyCompany", fieldFlags)?.GetValue(null)); + Assert.Equal(result.AssemblyCopyright, thisAssemblyClass.GetField("AssemblyCopyright", fieldFlags)?.GetValue(null)); + Assert.Equal(result.GitCommitId, thisAssemblyClass.GetField("GitCommitId", fieldFlags)?.GetValue(null) ?? string.Empty); + Assert.Equal(result.PublicRelease, thisAssemblyClass.GetField("IsPublicRelease", fieldFlags)?.GetValue(null)); + Assert.Equal(!string.IsNullOrEmpty(result.PrereleaseVersion), thisAssemblyClass.GetField("IsPrerelease", fieldFlags)?.GetValue(null)); + + if (gitRepo) + { + Assert.True(long.TryParse(result.GitCommitDateTicks, out _), $"Invalid value for GitCommitDateTicks: '{result.GitCommitDateTicks}'"); + var gitCommitDate = new DateTime(long.Parse(result.GitCommitDateTicks), DateTimeKind.Utc); + Assert.Equal(gitCommitDate, thisAssemblyClass.GetProperty("GitCommitDate", fieldFlags)?.GetValue(null) ?? thisAssemblyClass.GetField("GitCommitDate", fieldFlags)?.GetValue(null) ?? string.Empty); + } + else + { + Assert.Empty(result.GitCommitDateTicks); + Assert.Null(thisAssemblyClass.GetProperty("GitCommitDate", fieldFlags)); + } + + // Verify that it doesn't have key fields + Assert.Null(thisAssemblyClass.GetField("PublicKey", fieldFlags)); + Assert.Null(thisAssemblyClass.GetField("PublicKeyToken", fieldFlags)); + } + + [Fact] + [Trait("TestCategory", "FailsInCloudTest")] + public async Task AssemblyInfo_IncrementalBuild() + { + this.WriteVersionFile(prerelease: "-beta"); + await this.BuildAsync("Build", logVerbosity: LoggerVerbosity.Minimal); + this.WriteVersionFile(prerelease: "-rc"); // two characters SHORTER, to test file truncation. + await this.BuildAsync("Build", logVerbosity: LoggerVerbosity.Minimal); + } + +#if !NETCOREAPP + /// + /// Create a native resource .dll and verify that its version + /// information is set correctly. + /// + [Fact] + public async Task NativeVersionInfo_CreateNativeResourceDll() + { + this.testProject = this.CreateNativeProjectRootElement(this.projectDirectory, "test.vcxproj"); + this.WriteVersionFile(); + BuildResults result = await this.BuildAsync(Targets.Build, logVerbosity: LoggerVerbosity.Minimal); + Assert.Empty(result.LoggedEvents.OfType()); + + string targetFile = Path.Combine(this.projectDirectory, result.BuildResult.ProjectStateAfterBuild.GetPropertyValue("TargetPath")); + Assert.True(File.Exists(targetFile)); + + var fileInfo = FileVersionInfo.GetVersionInfo(targetFile); + Assert.Equal("1.2", fileInfo.FileVersion); + Assert.Equal("1.2.0", fileInfo.ProductVersion); + Assert.Equal("test", fileInfo.InternalName); + Assert.Equal("Nerdbank", fileInfo.CompanyName); + Assert.Equal($"Copyright (c) {DateTime.Now.Year}. All rights reserved.", fileInfo.LegalCopyright); + } +#endif + + /// + protected override GitContext CreateGitContext(string path, string committish = null) => throw new NotImplementedException(); +} diff --git a/src/NerdBank.GitVersioning.Tests/TestUtilities.cs b/test/Nerdbank.GitVersioning.Tests/TestUtilities.cs similarity index 89% rename from src/NerdBank.GitVersioning.Tests/TestUtilities.cs rename to test/Nerdbank.GitVersioning.Tests/TestUtilities.cs index 6ea6efc3..35640944 100644 --- a/src/NerdBank.GitVersioning.Tests/TestUtilities.cs +++ b/test/Nerdbank.GitVersioning.Tests/TestUtilities.cs @@ -1,4 +1,5 @@ -using Validation; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Collections.Generic; @@ -8,6 +9,7 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; +using Validation; /// /// Test utility methods. @@ -52,10 +54,10 @@ internal static void ExtractEmbeddedResource(string resourcePath, string extract Requires.NotNullOrEmpty(resourcePath, nameof(resourcePath)); Requires.NotNullOrEmpty(extractedFilePath, nameof(extractedFilePath)); - using (var stream = GetEmbeddedResource(resourcePath)) + using (Stream stream = GetEmbeddedResource(resourcePath)) { Requires.Argument(stream is not null, nameof(resourcePath), "Resource not found."); - using (var extractedFile = File.OpenWrite(extractedFilePath)) + using (FileStream extractedFile = File.OpenWrite(extractedFilePath)) { stream.CopyTo(extractedFile); } @@ -93,6 +95,7 @@ internal ExpandedRepo(string repoPath) public string RepoPath { get; private set; } + /// public void Dispose() { if (Directory.Exists(this.RepoPath)) diff --git a/src/NerdBank.GitVersioning.Tests/VersionExtensionsTests.cs b/test/Nerdbank.GitVersioning.Tests/VersionExtensionsTests.cs similarity index 79% rename from src/NerdBank.GitVersioning.Tests/VersionExtensionsTests.cs rename to test/Nerdbank.GitVersioning.Tests/VersionExtensionsTests.cs index 66347dd8..9be24c67 100644 --- a/src/NerdBank.GitVersioning.Tests/VersionExtensionsTests.cs +++ b/test/Nerdbank.GitVersioning.Tests/VersionExtensionsTests.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -11,7 +14,7 @@ public class VersionExtensionsTests [Fact] public void EnsureNonNegativeComponents_NoValues() { - var version = new Version().EnsureNonNegativeComponents(); + Version version = new Version().EnsureNonNegativeComponents(); Assert.Equal(0, version.Major); Assert.Equal(0, version.Minor); Assert.Equal(0, version.Build); @@ -21,7 +24,7 @@ public void EnsureNonNegativeComponents_NoValues() [Fact] public void EnsureNonNegativeComponents_2Values() { - var version = new Version(1, 2).EnsureNonNegativeComponents(); + Version version = new Version(1, 2).EnsureNonNegativeComponents(); Assert.Equal(1, version.Major); Assert.Equal(2, version.Minor); Assert.Equal(0, version.Build); @@ -31,7 +34,7 @@ public void EnsureNonNegativeComponents_2Values() [Fact] public void EnsureNonNegativeComponents_3Values() { - var version = new Version(1, 2, 3).EnsureNonNegativeComponents(); + Version version = new Version(1, 2, 3).EnsureNonNegativeComponents(); Assert.Equal(1, version.Major); Assert.Equal(2, version.Minor); Assert.Equal(3, version.Build); @@ -42,7 +45,7 @@ public void EnsureNonNegativeComponents_3Values() public void EnsureNonNegativeComponents_4Values() { var original = new Version(1, 2, 3, 4); - var version = original.EnsureNonNegativeComponents(); + Version version = original.EnsureNonNegativeComponents(); Assert.Same(original, version); } diff --git a/src/NerdBank.GitVersioning.Tests/VersionFileTests.cs b/test/Nerdbank.GitVersioning.Tests/VersionFileTests.cs similarity index 83% rename from src/NerdBank.GitVersioning.Tests/VersionFileTests.cs rename to test/Nerdbank.GitVersioning.Tests/VersionFileTests.cs index afeedc2a..745c83df 100644 --- a/src/NerdBank.GitVersioning.Tests/VersionFileTests.cs +++ b/test/Nerdbank.GitVersioning.Tests/VersionFileTests.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -11,6 +14,9 @@ using Xunit.Abstractions; using Version = System.Version; +#pragma warning disable SA1402 // File may only contain a single type +#pragma warning disable SA1649 // File name should match first type name + [Trait("Engine", "Managed")] public class VersionFileManagedTests : VersionFileTests { @@ -19,8 +25,9 @@ public VersionFileManagedTests(ITestOutputHelper logger) { } + /// protected override GitContext CreateGitContext(string path, string committish = null) - => GitContext.Create(path, committish, writable: false); + => GitContext.Create(path, committish, engine: GitContext.Engine.ReadOnly); } [Trait("Engine", "LibGit2")] @@ -31,8 +38,9 @@ public VersionFileLibGit2Tests(ITestOutputHelper logger) { } + /// protected override GitContext CreateGitContext(string path, string committish = null) - => GitContext.Create(path, committish, writable: true); + => GitContext.Create(path, committish, engine: GitContext.Engine.ReadWrite); } public abstract class VersionFileTests : RepoTestBase @@ -58,8 +66,8 @@ public void IsVersionDefined_Commit() // Verify that we can find the version.txt file in the most recent commit, // But not in the initial commit. - using var tipContext = this.CreateGitContext(this.RepoPath, this.LibGit2Repository.Head.Commits.First().Sha); - using var initialContext = this.CreateGitContext(this.RepoPath, this.LibGit2Repository.Head.Commits.Last().Sha); + using GitContext tipContext = this.CreateGitContext(this.RepoPath, this.LibGit2Repository.Head.Commits.First().Sha); + using GitContext initialContext = this.CreateGitContext(this.RepoPath, this.LibGit2Repository.Head.Commits.Last().Sha); Assert.True(tipContext.VersionFile.IsVersionDefined()); Assert.False(initialContext.VersionFile.IsVersionDefined()); } @@ -80,9 +88,9 @@ public void IsVersionDefined_String_ConsiderAncestorFolders() Directory.CreateDirectory(subDirABC); this.Context.VersionFile.SetVersion(subDirAB, new Version(1, 1)); - using var subDirABCContext = this.CreateGitContext(subDirABC); - using var subDirABContext = this.CreateGitContext(subDirAB); - using var subDirAContext = this.CreateGitContext(subDirA); + using GitContext subDirABCContext = this.CreateGitContext(subDirABC); + using GitContext subDirABContext = this.CreateGitContext(subDirAB); + using GitContext subDirAContext = this.CreateGitContext(subDirA); Assert.True(subDirABCContext.VersionFile.IsVersionDefined()); Assert.True(subDirABContext.VersionFile.IsVersionDefined()); Assert.True(subDirAContext.VersionFile.IsVersionDefined()); @@ -102,7 +110,7 @@ public void GetVersion_JsonCompatibility(string version, string assemblyVersion, { File.WriteAllText(Path.Combine(this.RepoPath, VersionFile.JsonFileName), json); - var options = this.Context.VersionFile.GetVersion(); + VersionOptions options = this.Context.VersionFile.GetVersion(); Assert.NotNull(options); Assert.Equal(version, options.Version?.ToString()); Assert.Equal(assemblyVersion, options.AssemblyVersion?.Version?.ToString()); @@ -162,8 +170,8 @@ public void SetVersion_PathFilters_OutsideGitRepo() Version = SemanticVersion.Parse("1.2"), PathFilters = new[] { - new FilterPath("./foo", ""), - } + new FilterPath("./foo", string.Empty), + }, }; this.Context.VersionFile.SetVersion(this.RepoPath, versionOptions); @@ -181,15 +189,15 @@ public void SetVersion_PathFilters_DifferentRelativePaths() { new FilterPath("./foo", "bar"), new FilterPath("/absolute", "bar"), - } + }, }; var expected = versionOptions.PathFilters.Select(x => x.RepoRelativePath).ToList(); - var projectDirectory = Path.Combine(this.RepoPath, "quux"); + string projectDirectory = Path.Combine(this.RepoPath, "quux"); this.Context.VersionFile.SetVersion(projectDirectory, versionOptions); - using var projectContext = this.CreateGitContext(projectDirectory); - var actualVersionOptions = projectContext.VersionFile.GetVersion(); + using GitContext projectContext = this.CreateGitContext(projectDirectory); + VersionOptions actualVersionOptions = projectContext.VersionFile.GetVersion(); var actual = actualVersionOptions.PathFilters.Select(x => x.RepoRelativePath).ToList(); Assert.Equal(expected, actual); } @@ -204,24 +212,24 @@ public void SetVersion_PathFilters_InheritRelativePaths() Version = SemanticVersion.Parse("1.2"), PathFilters = new[] { - new FilterPath("./root-file.txt", ""), - new FilterPath("/absolute", ""), - } + new FilterPath("./root-file.txt", string.Empty), + new FilterPath("/absolute", string.Empty), + }, }; this.Context.VersionFile.SetVersion(this.RepoPath, rootVersionOptions); var versionOptions = new VersionOptions { Version = SemanticVersion.Parse("1.2"), - Inherit = true + Inherit = true, }; - var projectDirectory = Path.Combine(this.RepoPath, "quux"); + string projectDirectory = Path.Combine(this.RepoPath, "quux"); this.Context.VersionFile.SetVersion(projectDirectory, versionOptions); var expected = rootVersionOptions.PathFilters.Select(x => x.RepoRelativePath).ToList(); - using var projectContext = this.CreateGitContext(projectDirectory); - var actualVersionOptions = projectContext.VersionFile.GetVersion(); + using GitContext projectContext = this.CreateGitContext(projectDirectory); + VersionOptions actualVersionOptions = projectContext.VersionFile.GetVersion(); var actual = actualVersionOptions.PathFilters.Select(x => x.RepoRelativePath).ToList(); Assert.Equal(expected, actual); } @@ -236,9 +244,9 @@ public void SetVersion_PathFilters_InheritOverride() Version = SemanticVersion.Parse("1.2"), PathFilters = new[] { - new FilterPath("./root-file.txt", ""), - new FilterPath("/absolute", ""), - } + new FilterPath("./root-file.txt", string.Empty), + new FilterPath("/absolute", string.Empty), + }, }; this.Context.VersionFile.SetVersion(this.RepoPath, rootVersionOptions); @@ -250,15 +258,15 @@ public void SetVersion_PathFilters_InheritOverride() { new FilterPath("./project-file.txt", "quux"), new FilterPath("/absolute", "quux"), - } + }, }; - var projectDirectory = Path.Combine(this.RepoPath, "quux"); + string projectDirectory = Path.Combine(this.RepoPath, "quux"); this.Context.VersionFile.SetVersion(projectDirectory, versionOptions); var expected = versionOptions.PathFilters.Select(x => x.RepoRelativePath).ToList(); - using var projectContext = this.CreateGitContext(projectDirectory); - var actualVersionOptions = projectContext.VersionFile.GetVersion(); + using GitContext projectContext = this.CreateGitContext(projectDirectory); + VersionOptions actualVersionOptions = projectContext.VersionFile.GetVersion(); var actual = actualVersionOptions.PathFilters.Select(x => x.RepoRelativePath).ToList(); Assert.Equal(expected, actual); } @@ -283,12 +291,12 @@ public void SetVersion_PathFilters_InheritOverride() [InlineData(@"{""release"":{""branchName"":""someName"",""versionIncrement"":""major"",""firstUnstableTag"":""pre""}}", @"{""release"":{""branchName"":""someName"",""versionIncrement"":""major"",""firstUnstableTag"":""pre""}}")] public void JsonMinification(string full, string minimal) { - var settings = VersionOptions.GetJsonSettings(); + JsonSerializerSettings settings = VersionOptions.GetJsonSettings(); settings.Formatting = Formatting.None; // Assert that the two representations are equivalent. - var fullVersion = JsonConvert.DeserializeObject(full, settings); - var minimalVersion = JsonConvert.DeserializeObject(minimal, settings); + VersionOptions fullVersion = JsonConvert.DeserializeObject(full, settings); + VersionOptions minimalVersion = JsonConvert.DeserializeObject(minimal, settings); Assert.Equal(fullVersion, minimalVersion); string fullVersionSerialized = JsonConvert.SerializeObject(fullVersion, settings); @@ -368,7 +376,7 @@ public void GetVersion_String_FindsNearestFileInAncestorDirectories() Directory.CreateDirectory(subDirABC); this.Context.VersionFile.SetVersion(subDirAB, new Version(1, 1)); this.InitializeSourceControl(); - var commit = this.LibGit2Repository.Head.Commits.First().Sha; + string commit = this.LibGit2Repository.Head.Commits.First().Sha; this.AssertPathHasVersion(commit, subDirABC, subdirVersionSpec); this.AssertPathHasVersion(commit, subDirAB, subdirVersionSpec); @@ -399,7 +407,7 @@ public void GetVersion_String_FindsNearestFileInAncestorDirectories_WithAssembly Directory.CreateDirectory(subDirABC); this.Context.VersionFile.SetVersion(subDirAB, subdirVersionSpec); this.InitializeSourceControl(); - var commit = this.LibGit2Repository.Head.Commits.First().Sha; + string commit = this.LibGit2Repository.Head.Commits.First().Sha; this.AssertPathHasVersion(commit, subDirABC, subdirVersionSpec); this.AssertPathHasVersion(commit, subDirAB, subdirVersionSpec); @@ -407,15 +415,14 @@ public void GetVersion_String_FindsNearestFileInAncestorDirectories_WithAssembly this.AssertPathHasVersion(commit, this.RepoPath, rootVersionSpec); } - [Fact] public void GetVersion_ReadReleaseSettings_VersionIncrement() { - var json = @"{ ""version"" : ""1.2"", ""release"" : { ""versionIncrement"" : ""major"" } }"; - var path = Path.Combine(this.RepoPath, "version.json"); + string json = @"{ ""version"" : ""1.2"", ""release"" : { ""versionIncrement"" : ""major"" } }"; + string path = Path.Combine(this.RepoPath, "version.json"); File.WriteAllText(path, json); - var versionOptions = this.Context.VersionFile.GetVersion(); + VersionOptions versionOptions = this.Context.VersionFile.GetVersion(); Assert.NotNull(versionOptions.Release); Assert.NotNull(versionOptions.Release.VersionIncrement); @@ -425,11 +432,11 @@ public void GetVersion_ReadReleaseSettings_VersionIncrement() [Fact] public void GetVersion_ReadReleaseSettings_FirstUnstableTag() { - var json = @"{ ""version"" : ""1.2"", ""release"" : { ""firstUnstableTag"" : ""preview"" } }"; - var path = Path.Combine(this.RepoPath, "version.json"); + string json = @"{ ""version"" : ""1.2"", ""release"" : { ""firstUnstableTag"" : ""preview"" } }"; + string path = Path.Combine(this.RepoPath, "version.json"); File.WriteAllText(path, json); - var versionOptions = this.Context.VersionFile.GetVersion(); + VersionOptions versionOptions = this.Context.VersionFile.GetVersion(); Assert.NotNull(versionOptions.Release); Assert.NotNull(versionOptions.Release.FirstUnstableTag); @@ -439,11 +446,11 @@ public void GetVersion_ReadReleaseSettings_FirstUnstableTag() [Fact] public void GetVersion_ReadReleaseSettings_BranchName() { - var json = @"{ ""version"" : ""1.2"", ""release"" : { ""branchName"" : ""someValue{version}"" } }"; - var path = Path.Combine(this.RepoPath, "version.json"); + string json = @"{ ""version"" : ""1.2"", ""release"" : { ""branchName"" : ""someValue{version}"" } }"; + string path = Path.Combine(this.RepoPath, "version.json"); File.WriteAllText(path, json); - var versionOptions = this.Context.VersionFile.GetVersion(); + VersionOptions versionOptions = this.Context.VersionFile.GetVersion(); Assert.NotNull(versionOptions.Release); Assert.NotNull(versionOptions.Release.BranchName); @@ -455,12 +462,12 @@ public void GetVersion_ReadPathFilters() { this.InitializeSourceControl(); - var json = @"{ ""version"" : ""1.2"", ""pathFilters"" : [ "":/root.txt"", ""./hello"" ] }"; - var path = Path.Combine(this.RepoPath, "version.json"); + string json = @"{ ""version"" : ""1.2"", ""pathFilters"" : [ "":/root.txt"", ""./hello"" ] }"; + string path = Path.Combine(this.RepoPath, "version.json"); File.WriteAllText(path, json); - var repoRelativeBaseDirectory = "."; - var versionOptions = this.Context.VersionFile.GetVersion(); + string repoRelativeBaseDirectory = "."; + VersionOptions versionOptions = this.Context.VersionFile.GetVersion(); Assert.NotNull(versionOptions.PathFilters); Assert.Equal(new[] { "/root.txt", "./hello" }, versionOptions.PathFilters.Select(fp => fp.ToPathSpec(repoRelativeBaseDirectory))); @@ -469,8 +476,8 @@ public void GetVersion_ReadPathFilters() [Fact] public void GetVersion_WithPathFiltersOutsideOfGitRepo() { - var json = @"{ ""version"" : ""1.2"", ""pathFilters"" : [ ""."" ] }"; - var path = Path.Combine(this.RepoPath, "version.json"); + string json = @"{ ""version"" : ""1.2"", ""pathFilters"" : [ ""."" ] }"; + string path = Path.Combine(this.RepoPath, "version.json"); File.WriteAllText(path, json); this.Context.VersionFile.GetVersion(); @@ -553,20 +560,20 @@ public void VersionJson_Inheritance(bool commitInSourceControl) VersionOptions GetOption(string path) { - using var context = this.CreateGitContext(Path.Combine(this.RepoPath, path)); + using GitContext context = this.CreateGitContext(Path.Combine(this.RepoPath, path)); return context.VersionFile.GetVersion(); } - - var level1Options = GetOption(string.Empty); + + VersionOptions level1Options = GetOption(string.Empty); Assert.False(level1Options.Inherit); - var level2Options = GetOption("foo"); + VersionOptions level2Options = GetOption("foo"); Assert.Equal(level1.Version.Version.Major, level2Options.Version.Version.Major); Assert.Equal(level1.Version.Version.Minor, level2Options.Version.Version.Minor); Assert.Equal(level2.AssemblyVersion.Precision, level2Options.AssemblyVersion.Precision); Assert.True(level2Options.Inherit); - var level3Options = GetOption("foo/bar"); + VersionOptions level3Options = GetOption("foo/bar"); Assert.Equal(level1.Version.Version.Major, level3Options.Version.Version.Major); Assert.Equal(level1.Version.Version.Minor, level3Options.Version.Version.Minor); Assert.Equal(level2.AssemblyVersion.Precision, level3Options.AssemblyVersion.Precision); @@ -574,12 +581,12 @@ VersionOptions GetOption(string path) Assert.Equal(level3.VersionHeightOffset, level3Options.VersionHeightOffset); Assert.True(level3Options.Inherit); - var level2NoInheritOptions = GetOption("noInherit"); + VersionOptions level2NoInheritOptions = GetOption("noInherit"); Assert.Equal(level2NoInherit.Version, level2NoInheritOptions.Version); Assert.Equal(VersionOptions.DefaultVersionPrecision, level2NoInheritOptions.AssemblyVersionOrDefault.PrecisionOrDefault); Assert.False(level2NoInheritOptions.Inherit); - var level2InheritButResetVersionOptions = GetOption("inheritWithVersion"); + VersionOptions level2InheritButResetVersionOptions = GetOption("inheritWithVersion"); Assert.Equal(level2InheritButResetVersion.Version, level2InheritButResetVersionOptions.Version); Assert.True(level2InheritButResetVersionOptions.Inherit); @@ -608,17 +615,16 @@ public void GetVersion_ProducesAbsolutePath() Assert.True(Path.IsPathRooted(actualDirectory)); } - [Theory] [InlineData(1)] [InlineData(2)] public void GetVersion_ReadNuGetPackageVersionSettings_SemVer(int semVer) { - var json = $@"{{ ""version"" : ""1.0"", ""nugetPackageVersion"" : {{ ""semVer"" : {semVer} }} }}"; - var path = Path.Combine(this.RepoPath, "version.json"); + string json = $@"{{ ""version"" : ""1.0"", ""nugetPackageVersion"" : {{ ""semVer"" : {semVer} }} }}"; + string path = Path.Combine(this.RepoPath, "version.json"); File.WriteAllText(path, json); - var versionOptions = this.Context.VersionFile.GetVersion(); + VersionOptions versionOptions = this.Context.VersionFile.GetVersion(); Assert.NotNull(versionOptions.NuGetPackageVersion); Assert.NotNull(versionOptions.NuGetPackageVersion.SemVer); @@ -629,11 +635,11 @@ public void GetVersion_ReadNuGetPackageVersionSettings_SemVer(int semVer) [CombinatorialData] public void GetVersion_ReadNuGetPackageVersionSettings_Precision(VersionOptions.VersionPrecision precision) { - var json = $@"{{ ""version"" : ""1.0"", ""nugetPackageVersion"" : {{ ""precision"" : ""{precision}"" }} }}"; - var path = Path.Combine(this.RepoPath, "version.json"); + string json = $@"{{ ""version"" : ""1.0"", ""nugetPackageVersion"" : {{ ""precision"" : ""{precision}"" }} }}"; + string path = Path.Combine(this.RepoPath, "version.json"); File.WriteAllText(path, json); - var versionOptions = this.Context.VersionFile.GetVersion(); + VersionOptions versionOptions = this.Context.VersionFile.GetVersion(); Assert.NotNull(versionOptions.NuGetPackageVersion); Assert.NotNull(versionOptions.NuGetPackageVersion.Precision); @@ -642,7 +648,7 @@ public void GetVersion_ReadNuGetPackageVersionSettings_Precision(VersionOptions. private void AssertPathHasVersion(string committish, string absolutePath, VersionOptions expected) { - var actual = this.GetVersionOptions(absolutePath, committish); + VersionOptions actual = this.GetVersionOptions(absolutePath, committish); Assert.Equal(expected, this.GetVersionOptions(absolutePath, committish)); } } diff --git a/src/NerdBank.GitVersioning.Tests/VersionOptionsTests.cs b/test/Nerdbank.GitVersioning.Tests/VersionOptionsTests.cs similarity index 96% rename from src/NerdBank.GitVersioning.Tests/VersionOptionsTests.cs rename to test/Nerdbank.GitVersioning.Tests/VersionOptionsTests.cs index 603f02fe..233e2fdc 100644 --- a/src/NerdBank.GitVersioning.Tests/VersionOptionsTests.cs +++ b/test/Nerdbank.GitVersioning.Tests/VersionOptionsTests.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -184,7 +187,6 @@ public void CannotWriteToDefaultInstances() Assert.Throws(() => options.ReleaseOrDefault.FirstUnstableTag = "-tag"); } - [Fact] public void ReleaseOptions_Equality() { @@ -193,12 +195,12 @@ public void ReleaseOptions_Equality() var ro3 = new VersionOptions.ReleaseOptions() { BranchName = "branchName", - VersionIncrement = VersionOptions.ReleaseVersionIncrement.Major + VersionIncrement = VersionOptions.ReleaseVersionIncrement.Major, }; var ro4 = new VersionOptions.ReleaseOptions() { BranchName = "branchName", - VersionIncrement = VersionOptions.ReleaseVersionIncrement.Major + VersionIncrement = VersionOptions.ReleaseVersionIncrement.Major, }; var ro5 = new VersionOptions.ReleaseOptions() { @@ -209,13 +211,13 @@ public void ReleaseOptions_Equality() { BranchName = "branchName", VersionIncrement = VersionOptions.ReleaseVersionIncrement.Minor, - FirstUnstableTag = "tag" + FirstUnstableTag = "tag", }; var ro7 = new VersionOptions.ReleaseOptions() { BranchName = "branchName", VersionIncrement = VersionOptions.ReleaseVersionIncrement.Minor, - FirstUnstableTag = "tag" + FirstUnstableTag = "tag", }; Assert.Equal(ro1, ro2); @@ -237,19 +239,19 @@ public void NuGetPackageVersionOptions_Equality() var npvo2a = new VersionOptions.NuGetPackageVersionOptions { - SemVer = 2 + SemVer = 2, }; Assert.NotEqual(npvo2a, npvo1a); var npvo3a = new VersionOptions.NuGetPackageVersionOptions { - Precision = VersionOptions.VersionPrecision.Revision + Precision = VersionOptions.VersionPrecision.Revision, }; Assert.NotEqual(npvo3a, npvo1a); var npvo4a = new VersionOptions.NuGetPackageVersionOptions { - Precision = VersionOptions.VersionPrecision.Build + Precision = VersionOptions.VersionPrecision.Build, }; Assert.Equal(npvo4a, npvo1a); // Equal because we haven't changed defaults. } diff --git a/src/NerdBank.GitVersioning.Tests/VersionOracleTests.cs b/test/Nerdbank.GitVersioning.Tests/VersionOracleTests.cs similarity index 90% rename from src/NerdBank.GitVersioning.Tests/VersionOracleTests.cs rename to test/Nerdbank.GitVersioning.Tests/VersionOracleTests.cs index 1b121956..8098be4c 100644 --- a/src/NerdBank.GitVersioning.Tests/VersionOracleTests.cs +++ b/test/Nerdbank.GitVersioning.Tests/VersionOracleTests.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; using System.IO; using System.Linq; using LibGit2Sharp; @@ -8,6 +11,9 @@ using Xunit.Abstractions; using Version = System.Version; +#pragma warning disable SA1402 // File may only contain a single type +#pragma warning disable SA1649 // File name should match first type name + [Trait("Engine", "Managed")] public class VersionOracleManagedTests : VersionOracleTests { @@ -16,8 +22,9 @@ public VersionOracleManagedTests(ITestOutputHelper logger) { } + /// protected override GitContext CreateGitContext(string path, string committish = null) - => GitContext.Create(path, committish, writable: false); + => GitContext.Create(path, committish, engine: GitContext.Engine.ReadOnly); } [Trait("Engine", "LibGit2")] @@ -28,8 +35,9 @@ public VersionOracleLibGit2Tests(ITestOutputHelper logger) { } + /// protected override GitContext CreateGitContext(string path, string committish = null) - => GitContext.Create(path, committish, writable: true); + => GitContext.Create(path, committish, engine: GitContext.Engine.ReadWrite); } public abstract class VersionOracleTests : RepoTestBase @@ -45,7 +53,7 @@ public VersionOracleTests(ITestOutputHelper logger) public void NotRepo() { // Seems safe to assume a temporary path is not a Git directory. - var context = this.CreateGitContext(Path.GetTempPath()); + GitContext context = this.CreateGitContext(Path.GetTempPath()); var oracle = new VersionOracle(context); Assert.Equal(0, oracle.VersionHeight); } @@ -53,7 +61,7 @@ public void NotRepo() [Fact] public void Submodule_RecognizedWithCorrectVersion() { - using (var expandedRepo = TestUtilities.ExtractRepoArchive("submodules")) + using (TestUtilities.ExpandedRepo expandedRepo = TestUtilities.ExtractRepoArchive("submodules")) { this.Context = this.CreateGitContext(Path.Combine(expandedRepo.RepoPath, "a")); var oracleA = new VersionOracle(this.Context); @@ -161,9 +169,9 @@ public void VersionHeightResetsWithVersionSpecChanges(string initial, string nex if (this.Context is Nerdbank.GitVersioning.LibGit2.LibGit2Context libgit2Context) { - foreach (var commit in libgit2Context.Repository.Head.Commits) + foreach (Commit commit in libgit2Context.Repository.Head.Commits) { - var versionFromId = this.GetVersion(committish: commit.Sha); + Version versionFromId = this.GetVersion(committish: commit.Sha); Assert.Contains(commit, Nerdbank.GitVersioning.LibGit2.LibGit2GitExtensions.GetCommitsFromVersion(libgit2Context, versionFromId)); } } @@ -272,7 +280,7 @@ public void SemVerStableNonPublicVersionShortened() var workingCopyVersion = new VersionOptions { Version = SemanticVersion.Parse("2.3"), - GitCommitIdShortFixedLength = 7 + GitCommitIdShortFixedLength = 7, }; this.WriteVersionFile(workingCopyVersion); this.InitializeSourceControl(); @@ -346,7 +354,7 @@ public void NpmPackageVersionIsSemVer2() VersionOptions workingCopyVersion = new VersionOptions { Version = SemanticVersion.Parse("7.8.9-foo.25"), - SemVer1NumericIdentifierPadding = 2 + SemVer1NumericIdentifierPadding = 2, }; this.WriteVersionFile(workingCopyVersion); this.InitializeSourceControl(); @@ -364,7 +372,7 @@ public void CanSetSemVer2ForNuGetPackageVersionPublicRelease() NuGetPackageVersion = new VersionOptions.NuGetPackageVersionOptions { SemVer = 2, - } + }, }; this.WriteVersionFile(workingCopyVersion); this.InitializeSourceControl(); @@ -374,98 +382,98 @@ public void CanSetSemVer2ForNuGetPackageVersionPublicRelease() } [Theory] - // - // SemVer 1 - // - // 2 version fields configured in version.json + //// + //// SemVer 1 + //// + //// 2 version fields configured in version.json [InlineData(1, "1.2", VersionOptions.VersionPrecision.Major, "1.0.0")] [InlineData(1, "1.2", VersionOptions.VersionPrecision.Minor, "1.2.0")] [InlineData(1, "1.2", VersionOptions.VersionPrecision.Build, "1.2.1")] [InlineData(1, "1.2", VersionOptions.VersionPrecision.Revision, "1.2.1.")] - // 2 version fields and a static prerelease tag configured in version.json + //// 2 version fields and a static prerelease tag configured in version.json [InlineData(1, "1.2-alpha", VersionOptions.VersionPrecision.Major, "1.0.0-alpha")] [InlineData(1, "1.2-alpha", VersionOptions.VersionPrecision.Minor, "1.2.0-alpha")] [InlineData(1, "1.2-alpha", VersionOptions.VersionPrecision.Build, "1.2.1-alpha")] [InlineData(1, "1.2-alpha", VersionOptions.VersionPrecision.Revision, "1.2.1.-alpha")] - // 2 version fields with git height in prerelease tag configured in version.json + //// 2 version fields with git height in prerelease tag configured in version.json [InlineData(1, "1.2-alpha.{height}", VersionOptions.VersionPrecision.Major, "1.0.0-alpha-0001")] [InlineData(1, "1.2-alpha.{height}", VersionOptions.VersionPrecision.Minor, "1.2.0-alpha-0001")] [InlineData(1, "1.2-alpha.{height}", VersionOptions.VersionPrecision.Build, "1.2.0-alpha-0001")] [InlineData(1, "1.2-alpha.{height}", VersionOptions.VersionPrecision.Revision, "1.2.0.0-alpha-0001")] - // 3 version fields configured in version.json + //// 3 version fields configured in version.json [InlineData(1, "1.2.3", VersionOptions.VersionPrecision.Major, "1.0.0")] [InlineData(1, "1.2.3", VersionOptions.VersionPrecision.Minor, "1.2.0")] [InlineData(1, "1.2.3", VersionOptions.VersionPrecision.Build, "1.2.3")] [InlineData(1, "1.2.3", VersionOptions.VersionPrecision.Revision, "1.2.3.1")] - // 3 version fields and a static prerelease tag configured in version.json + //// 3 version fields and a static prerelease tag configured in version.json [InlineData(1, "1.2.3-alpha", VersionOptions.VersionPrecision.Major, "1.0.0-alpha")] [InlineData(1, "1.2.3-alpha", VersionOptions.VersionPrecision.Minor, "1.2.0-alpha")] [InlineData(1, "1.2.3-alpha", VersionOptions.VersionPrecision.Build, "1.2.3-alpha")] [InlineData(1, "1.2.3-alpha", VersionOptions.VersionPrecision.Revision, "1.2.3.1-alpha")] - // 3 version fields with git height in prerelease tag configured in version.json + //// 3 version fields with git height in prerelease tag configured in version.json [InlineData(1, "1.2.3-alpha.{height}", VersionOptions.VersionPrecision.Major, "1.0.0-alpha-0001")] [InlineData(1, "1.2.3-alpha.{height}", VersionOptions.VersionPrecision.Minor, "1.2.0-alpha-0001")] [InlineData(1, "1.2.3-alpha.{height}", VersionOptions.VersionPrecision.Build, "1.2.3-alpha-0001")] [InlineData(1, "1.2.3-alpha.{height}", VersionOptions.VersionPrecision.Revision, "1.2.3.0-alpha-0001")] - // 4 version fields configured in version.json + //// 4 version fields configured in version.json [InlineData(1, "1.2.3.4", VersionOptions.VersionPrecision.Major, "1.0.0")] [InlineData(1, "1.2.3.4", VersionOptions.VersionPrecision.Minor, "1.2.0")] [InlineData(1, "1.2.3.4", VersionOptions.VersionPrecision.Build, "1.2.3")] [InlineData(1, "1.2.3.4", VersionOptions.VersionPrecision.Revision, "1.2.3.4")] - // 4 version fields and a static prerelease tag configured in version.json + //// 4 version fields and a static prerelease tag configured in version.json [InlineData(1, "1.2.3.4-alpha", VersionOptions.VersionPrecision.Major, "1.0.0-alpha")] [InlineData(1, "1.2.3.4-alpha", VersionOptions.VersionPrecision.Minor, "1.2.0-alpha")] [InlineData(1, "1.2.3.4-alpha", VersionOptions.VersionPrecision.Build, "1.2.3-alpha")] [InlineData(1, "1.2.3.4-alpha", VersionOptions.VersionPrecision.Revision, "1.2.3.4-alpha")] - // 4 version fields with git height in prerelease tag configured in version.json + //// 4 version fields with git height in prerelease tag configured in version.json [InlineData(1, "1.2.3.4-alpha.{height}", VersionOptions.VersionPrecision.Major, "1.0.0-alpha-0001")] [InlineData(1, "1.2.3.4-alpha.{height}", VersionOptions.VersionPrecision.Minor, "1.2.0-alpha-0001")] [InlineData(1, "1.2.3.4-alpha.{height}", VersionOptions.VersionPrecision.Build, "1.2.3-alpha-0001")] [InlineData(1, "1.2.3.4-alpha.{height}", VersionOptions.VersionPrecision.Revision, "1.2.3.4-alpha-0001")] - // - // SemVer 2 - // - // 2 version fields configured in version.json + //// + //// SemVer 2 + //// + //// 2 version fields configured in version.json [InlineData(2, "1.2", VersionOptions.VersionPrecision.Major, "1.0.0")] [InlineData(2, "1.2", VersionOptions.VersionPrecision.Minor, "1.2.0")] [InlineData(2, "1.2", VersionOptions.VersionPrecision.Build, "1.2.1")] [InlineData(2, "1.2", VersionOptions.VersionPrecision.Revision, "1.2.1.")] - // 2 version fields and a static prerelease tag configured in version.json + //// 2 version fields and a static prerelease tag configured in version.json [InlineData(2, "1.2-alpha", VersionOptions.VersionPrecision.Major, "1.0.0-alpha")] [InlineData(2, "1.2-alpha", VersionOptions.VersionPrecision.Minor, "1.2.0-alpha")] [InlineData(2, "1.2-alpha", VersionOptions.VersionPrecision.Build, "1.2.1-alpha")] [InlineData(2, "1.2-alpha", VersionOptions.VersionPrecision.Revision, "1.2.1.-alpha")] - // 2 version fields with git height in prerelease tag configured in version.json + //// 2 version fields with git height in prerelease tag configured in version.json [InlineData(2, "1.2-alpha.{height}", VersionOptions.VersionPrecision.Major, "1.0.0-alpha.1")] [InlineData(2, "1.2-alpha.{height}", VersionOptions.VersionPrecision.Minor, "1.2.0-alpha.1")] [InlineData(2, "1.2-alpha.{height}", VersionOptions.VersionPrecision.Build, "1.2.0-alpha.1")] [InlineData(2, "1.2-alpha.{height}", VersionOptions.VersionPrecision.Revision, "1.2.0.0-alpha.1")] - // 3 version fields configured in version.json + //// 3 version fields configured in version.json [InlineData(2, "1.2.3", VersionOptions.VersionPrecision.Major, "1.0.0")] [InlineData(2, "1.2.3", VersionOptions.VersionPrecision.Minor, "1.2.0")] [InlineData(2, "1.2.3", VersionOptions.VersionPrecision.Build, "1.2.3")] [InlineData(2, "1.2.3", VersionOptions.VersionPrecision.Revision, "1.2.3.1")] - // 3 version fields and a static prerelease tag configured in version.json + //// 3 version fields and a static prerelease tag configured in version.json [InlineData(2, "1.2.3-alpha", VersionOptions.VersionPrecision.Major, "1.0.0-alpha")] [InlineData(2, "1.2.3-alpha", VersionOptions.VersionPrecision.Minor, "1.2.0-alpha")] [InlineData(2, "1.2.3-alpha", VersionOptions.VersionPrecision.Build, "1.2.3-alpha")] [InlineData(2, "1.2.3-alpha", VersionOptions.VersionPrecision.Revision, "1.2.3.1-alpha")] - // 3 version fields with git height in prerelease tag configured in version.json + //// 3 version fields with git height in prerelease tag configured in version.json [InlineData(2, "1.2.3-alpha.{height}", VersionOptions.VersionPrecision.Major, "1.0.0-alpha.1")] [InlineData(2, "1.2.3-alpha.{height}", VersionOptions.VersionPrecision.Minor, "1.2.0-alpha.1")] [InlineData(2, "1.2.3-alpha.{height}", VersionOptions.VersionPrecision.Build, "1.2.3-alpha.1")] [InlineData(2, "1.2.3-alpha.{height}", VersionOptions.VersionPrecision.Revision, "1.2.3.0-alpha.1")] - // 4 version fields configured in version.json + //// 4 version fields configured in version.json [InlineData(2, "1.2.3.4", VersionOptions.VersionPrecision.Major, "1.0.0")] [InlineData(2, "1.2.3.4", VersionOptions.VersionPrecision.Minor, "1.2.0")] [InlineData(2, "1.2.3.4", VersionOptions.VersionPrecision.Build, "1.2.3")] [InlineData(2, "1.2.3.4", VersionOptions.VersionPrecision.Revision, "1.2.3.4")] - // 4 version fields and a static prerelease tag configured in version.json + //// 4 version fields and a static prerelease tag configured in version.json [InlineData(2, "1.2.3.4-alpha", VersionOptions.VersionPrecision.Major, "1.0.0-alpha")] [InlineData(2, "1.2.3.4-alpha", VersionOptions.VersionPrecision.Minor, "1.2.0-alpha")] [InlineData(2, "1.2.3.4-alpha", VersionOptions.VersionPrecision.Build, "1.2.3-alpha")] [InlineData(2, "1.2.3.4-alpha", VersionOptions.VersionPrecision.Revision, "1.2.3.4-alpha")] - // 4 version fields with git height in prerelease tag configured in version.json + //// 4 version fields with git height in prerelease tag configured in version.json [InlineData(2, "1.2.3.4-alpha.{height}", VersionOptions.VersionPrecision.Major, "1.0.0-alpha.1")] [InlineData(2, "1.2.3.4-alpha.{height}", VersionOptions.VersionPrecision.Minor, "1.2.0-alpha.1")] [InlineData(2, "1.2.3.4-alpha.{height}", VersionOptions.VersionPrecision.Build, "1.2.3-alpha.1")] @@ -478,8 +486,8 @@ public void CanSetPrecisionForNuGetPackageVersion(int semVer, string version, Ve NuGetPackageVersion = new VersionOptions.NuGetPackageVersionOptions { SemVer = semVer, - Precision = precision - } + Precision = precision, + }, }; this.WriteVersionFile(workingCopyVersion); this.InitializeSourceControl(); @@ -498,7 +506,7 @@ public void CanSetSemVer2ForNuGetPackageVersionNonPublicRelease() NuGetPackageVersion = new VersionOptions.NuGetPackageVersionOptions { SemVer = 2, - } + }, }; this.WriteVersionFile(workingCopyVersion); this.InitializeSourceControl(); @@ -576,7 +584,7 @@ public void VersionJsonWithSingleIntegerForVersion() { File.WriteAllText(Path.Combine(this.RepoPath, VersionFile.JsonFileName), @"{""version"":""3""}"); this.InitializeSourceControl(); - var ex = Assert.Throws(() => new VersionOracle(this.Context)); + FormatException ex = Assert.Throws(() => new VersionOracle(this.Context)); Assert.Contains(this.Context.GitCommitId, ex.Message); Assert.Contains("\"3\"", ex.InnerException.Message); this.Logger.WriteLine(ex.ToString()); @@ -606,7 +614,7 @@ public void Worktree_Support(bool detachedHead) this.LibGit2Repository.Worktrees.Add("wtbranch", "myworktree", workTreePath, isLocked: false); } - var context = this.CreateGitContext(workTreePath); + GitContext context = this.CreateGitContext(workTreePath); var oracleWorkTree = new VersionOracle(context); Assert.Equal(oracleOriginal.Version, oracleWorkTree.Version); @@ -619,10 +627,10 @@ public void GetVersionHeight_Test() { this.InitializeSourceControl(); - var first = this.LibGit2Repository.Commit("First", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); - var second = this.LibGit2Repository.Commit("Second", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); + Commit first = this.LibGit2Repository.Commit("First", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); + Commit second = this.LibGit2Repository.Commit("Second", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); this.WriteVersionFile(); - var third = this.LibGit2Repository.Commit("Third", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); + Commit third = this.LibGit2Repository.Commit("Third", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); Assert.Equal(2, this.GetVersionHeight()); } @@ -707,14 +715,14 @@ public void GetVersionHeight_IncludeFilter(string includeFilter) Assert.Equal(1, this.GetVersionHeight(relativeDirectory)); // Expect commit outside of project tree to not affect version height - var otherFilePath = Path.Combine(this.RepoPath, "my-file.txt"); + string otherFilePath = Path.Combine(this.RepoPath, "my-file.txt"); File.WriteAllText(otherFilePath, "hello"); Commands.Stage(this.LibGit2Repository, otherFilePath); this.LibGit2Repository.Commit("Add other file outside of project root", this.Signer, this.Signer); Assert.Equal(1, this.GetVersionHeight(relativeDirectory)); // Expect commit inside project tree to affect version height - var containedFilePath = Path.Combine(this.RepoPath, relativeDirectory, "another-file.txt"); + string containedFilePath = Path.Combine(this.RepoPath, relativeDirectory, "another-file.txt"); File.WriteAllText(containedFilePath, "hello"); Commands.Stage(this.LibGit2Repository, containedFilePath); this.LibGit2Repository.Commit("Add file within project root", this.Signer, this.Signer); @@ -733,20 +741,20 @@ public void GetVersionHeight_IncludeExcludeFilter() { new FilterPath("./", relativeDirectory), new FilterPath(":^/some-sub-dir/ignore.txt", relativeDirectory), - new FilterPath(":^excluded-dir", relativeDirectory) + new FilterPath(":^excluded-dir", relativeDirectory), }; this.WriteVersionFile(versionData, relativeDirectory); Assert.Equal(1, this.GetVersionHeight(relativeDirectory)); // Commit touching excluded path does not affect version height - var ignoredFilePath = Path.Combine(this.RepoPath, relativeDirectory, "ignore.txt"); + string ignoredFilePath = Path.Combine(this.RepoPath, relativeDirectory, "ignore.txt"); File.WriteAllText(ignoredFilePath, "hello"); Commands.Stage(this.LibGit2Repository, ignoredFilePath); this.LibGit2Repository.Commit("Add excluded file", this.Signer, this.Signer); Assert.Equal(1, this.GetVersionHeight(relativeDirectory)); // Commit touching both excluded and included path does affect height - var includedFilePath = Path.Combine(this.RepoPath, relativeDirectory, "another-file.txt"); + string includedFilePath = Path.Combine(this.RepoPath, relativeDirectory, "another-file.txt"); File.WriteAllText(includedFilePath, "hello"); File.WriteAllText(ignoredFilePath, "changed"); Commands.Stage(this.LibGit2Repository, includedFilePath); @@ -755,7 +763,7 @@ public void GetVersionHeight_IncludeExcludeFilter() Assert.Equal(2, this.GetVersionHeight(relativeDirectory)); // Commit touching excluded directory does not affect version height - var fileInExcludedDirPath = Path.Combine(this.RepoPath, relativeDirectory, "excluded-dir", "ignore.txt"); + string fileInExcludedDirPath = Path.Combine(this.RepoPath, relativeDirectory, "excluded-dir", "ignore.txt"); Directory.CreateDirectory(Path.GetDirectoryName(fileInExcludedDirPath)); File.WriteAllText(fileInExcludedDirPath, "hello"); Commands.Stage(this.LibGit2Repository, fileInExcludedDirPath); @@ -773,13 +781,13 @@ public void GetVersionHeight_IncludeExcludeFilter_NoProjectDirectory() { new FilterPath("./", "."), new FilterPath(":^/some-sub-dir/ignore.txt", "."), - new FilterPath(":^/excluded-dir", ".") + new FilterPath(":^/excluded-dir", "."), }; this.WriteVersionFile(versionData); Assert.Equal(1, this.GetVersionHeight()); // Commit touching excluded path does not affect version height - var ignoredFilePath = Path.Combine(this.RepoPath, "some-sub-dir", "ignore.txt"); + string ignoredFilePath = Path.Combine(this.RepoPath, "some-sub-dir", "ignore.txt"); Directory.CreateDirectory(Path.GetDirectoryName(ignoredFilePath)); File.WriteAllText(ignoredFilePath, "hello"); Commands.Stage(this.LibGit2Repository, ignoredFilePath); @@ -787,7 +795,7 @@ public void GetVersionHeight_IncludeExcludeFilter_NoProjectDirectory() Assert.Equal(1, this.GetVersionHeight()); // Commit touching both excluded and included path does affect height - var includedFilePath = Path.Combine(this.RepoPath, "some-sub-dir", "another-file.txt"); + string includedFilePath = Path.Combine(this.RepoPath, "some-sub-dir", "another-file.txt"); File.WriteAllText(includedFilePath, "hello"); File.WriteAllText(ignoredFilePath, "changed"); Commands.Stage(this.LibGit2Repository, includedFilePath); @@ -796,7 +804,7 @@ public void GetVersionHeight_IncludeExcludeFilter_NoProjectDirectory() Assert.Equal(2, this.GetVersionHeight()); // Commit touching excluded directory does not affect version height - var fileInExcludedDirPath = Path.Combine(this.RepoPath, "excluded-dir", "ignore.txt"); + string fileInExcludedDirPath = Path.Combine(this.RepoPath, "excluded-dir", "ignore.txt"); Directory.CreateDirectory(Path.GetDirectoryName(fileInExcludedDirPath)); File.WriteAllText(fileInExcludedDirPath, "hello"); Commands.Stage(this.LibGit2Repository, fileInExcludedDirPath); @@ -818,7 +826,7 @@ public void GetVersionHeight_AddingExcludeDoesNotLowerHeight(string excludePathF Assert.Equal(1, this.GetVersionHeight(relativeDirectory)); // Commit a file which will later be ignored - var ignoredFilePath = Path.Combine(this.RepoPath, "excluded-dir", "ignore.txt"); + string ignoredFilePath = Path.Combine(this.RepoPath, "excluded-dir", "ignore.txt"); Directory.CreateDirectory(Path.GetDirectoryName(ignoredFilePath)); File.WriteAllText(ignoredFilePath, "hello"); Commands.Stage(this.LibGit2Repository, ignoredFilePath); @@ -849,14 +857,14 @@ public void GetVersionHeight_IncludeRoot() Assert.Equal(1, this.GetVersionHeight(relativeDirectory)); // Expect commit outside of project tree to affect version height - var otherFilePath = Path.Combine(this.RepoPath, "my-file.txt"); + string otherFilePath = Path.Combine(this.RepoPath, "my-file.txt"); File.WriteAllText(otherFilePath, "hello"); Commands.Stage(this.LibGit2Repository, otherFilePath); this.LibGit2Repository.Commit("Add other file outside of project root", this.Signer, this.Signer); Assert.Equal(2, this.GetVersionHeight(relativeDirectory)); // Expect commit inside project tree to affect version height - var containedFilePath = Path.Combine(this.RepoPath, relativeDirectory, "another-file.txt"); + string containedFilePath = Path.Combine(this.RepoPath, relativeDirectory, "another-file.txt"); File.WriteAllText(containedFilePath, "hello"); Commands.Stage(this.LibGit2Repository, containedFilePath); this.LibGit2Repository.Commit("Add file within project root", this.Signer, this.Signer); @@ -880,7 +888,7 @@ public void GetVersionHeight_IncludeRootExcludeSome() Assert.Equal(1, this.GetVersionHeight(relativeDirectory)); // Expect commit in an excluded directory to not affect version height - var ignoredFilePath = Path.Combine(this.RepoPath, "excluded-dir", "my-file.txt"); + string ignoredFilePath = Path.Combine(this.RepoPath, "excluded-dir", "my-file.txt"); Directory.CreateDirectory(Path.GetDirectoryName(ignoredFilePath)); File.WriteAllText(ignoredFilePath, "hello"); Commands.Stage(this.LibGit2Repository, ignoredFilePath); @@ -888,7 +896,7 @@ public void GetVersionHeight_IncludeRootExcludeSome() Assert.Equal(1, this.GetVersionHeight(relativeDirectory)); // Expect commit within another directory to affect version height - var otherFilePath = Path.Combine(this.RepoPath, "another-dir", "another-file.txt"); + string otherFilePath = Path.Combine(this.RepoPath, "another-dir", "another-file.txt"); Directory.CreateDirectory(Path.GetDirectoryName(otherFilePath)); File.WriteAllText(otherFilePath, "hello"); Commands.Stage(this.LibGit2Repository, otherFilePath); @@ -960,13 +968,13 @@ public void GetVersionHeight_ProjectDirectoryDifferentToVersionJsonDirectory() var versionData = VersionOptions.FromVersion(new Version("1.2")); versionData.PathFilters = new[] { - new FilterPath(".", "") + new FilterPath(".", string.Empty), }; - this.WriteVersionFile(versionData, ""); + this.WriteVersionFile(versionData, string.Empty); Assert.Equal(1, this.GetVersionHeight(relativeDirectory)); // Expect commit in an excluded directory to not affect version height - var ignoredFilePath = Path.Combine(this.RepoPath, "other-dir", "my-file.txt"); + string ignoredFilePath = Path.Combine(this.RepoPath, "other-dir", "my-file.txt"); Directory.CreateDirectory(Path.GetDirectoryName(ignoredFilePath)); File.WriteAllText(ignoredFilePath, "hello"); Commands.Stage(this.LibGit2Repository, ignoredFilePath); @@ -992,14 +1000,14 @@ public void GetVersionHeight_ProjectDirectoryIsMoved() Assert.Equal(1, this.GetVersionHeight(relativeDirectory)); // Commit touching excluded path does not affect version height - var ignoredFilePath = Path.Combine(this.RepoPath, relativeDirectory, "ignore.txt"); + string ignoredFilePath = Path.Combine(this.RepoPath, relativeDirectory, "ignore.txt"); File.WriteAllText(ignoredFilePath, "hello"); Commands.Stage(this.LibGit2Repository, ignoredFilePath); this.LibGit2Repository.Commit("Add excluded file", this.Signer, this.Signer); Assert.Equal(1, this.GetVersionHeight(relativeDirectory)); // Commit touching both excluded and included path does affect height - var includedFilePath = Path.Combine(this.RepoPath, relativeDirectory, "another-file.txt"); + string includedFilePath = Path.Combine(this.RepoPath, relativeDirectory, "another-file.txt"); File.WriteAllText(includedFilePath, "hello"); File.WriteAllText(ignoredFilePath, "changed"); Commands.Stage(this.LibGit2Repository, includedFilePath); @@ -1008,7 +1016,7 @@ public void GetVersionHeight_ProjectDirectoryIsMoved() Assert.Equal(2, this.GetVersionHeight(relativeDirectory)); // Commit touching excluded directory does not affect version height - var fileInExcludedDirPath = Path.Combine(this.RepoPath, relativeDirectory, "excluded-dir", "ignore.txt"); + string fileInExcludedDirPath = Path.Combine(this.RepoPath, relativeDirectory, "excluded-dir", "ignore.txt"); Directory.CreateDirectory(Path.GetDirectoryName(fileInExcludedDirPath)); File.WriteAllText(fileInExcludedDirPath, "hello"); Commands.Stage(this.LibGit2Repository, fileInExcludedDirPath); @@ -1098,8 +1106,8 @@ public void GetVersionHeight_VeryLongHistory() [InlineData(VersionOptions.CloudBuildNumberCommitWhere.BuildMetadata, "1.2.3.4-alpha", "1.2.3.4-alpha+")] [InlineData(VersionOptions.CloudBuildNumberCommitWhere.FourthVersionComponent, "1.2.3.4-alpha", "1.2.3.4-alpha")] // 4 version fields with git height in prerelease tag configured in version.json - [InlineData(VersionOptions.CloudBuildNumberCommitWhere.BuildMetadata, "1.2.3.4-alpha.{height}", "1.2.3.4-alpha.1+")] - [InlineData(VersionOptions.CloudBuildNumberCommitWhere.FourthVersionComponent, "1.2.3.4-alpha.{height}", "1.2.3.4-alpha.1")] + [InlineData(VersionOptions.CloudBuildNumberCommitWhere.BuildMetadata, "1.2.3.4-alpha.{height}", "1.2.3.4-alpha.1+")] + [InlineData(VersionOptions.CloudBuildNumberCommitWhere.FourthVersionComponent, "1.2.3.4-alpha.{height}", "1.2.3.4-alpha.1")] public void CloudBuildNumber_4thPosition(VersionOptions.CloudBuildNumberCommitWhere where, string version, string expectedCloudBuildNumber) { VersionOptions workingCopyVersion = new VersionOptions @@ -1112,10 +1120,10 @@ public void CloudBuildNumber_4thPosition(VersionOptions.CloudBuildNumberCommitWh IncludeCommitId = new VersionOptions.CloudBuildNumberCommitIdOptions { When = VersionOptions.CloudBuildNumberCommitWhen.Always, - Where = where - } - } - } + Where = where, + }, + }, + }, }; this.WriteVersionFile(workingCopyVersion); this.InitializeSourceControl(); @@ -1123,7 +1131,7 @@ public void CloudBuildNumber_4thPosition(VersionOptions.CloudBuildNumberCommitWh oracle.PublicRelease = true; expectedCloudBuildNumber = expectedCloudBuildNumber.Replace("", GitObjectId.Parse(oracle.GitCommitId).AsUInt16().ToString()); expectedCloudBuildNumber = expectedCloudBuildNumber.Replace("", oracle.GitCommitIdShort); - + Assert.Equal(expectedCloudBuildNumber, oracle.CloudBuildNumber); } } diff --git a/src/NerdBank.GitVersioning.Tests/VersionSchemaTests.cs b/test/Nerdbank.GitVersioning.Tests/VersionSchemaTests.cs similarity index 93% rename from src/NerdBank.GitVersioning.Tests/VersionSchemaTests.cs rename to test/Nerdbank.GitVersioning.Tests/VersionSchemaTests.cs index db00a999..50d87f14 100644 --- a/src/NerdBank.GitVersioning.Tests/VersionSchemaTests.cs +++ b/test/Nerdbank.GitVersioning.Tests/VersionSchemaTests.cs @@ -1,4 +1,7 @@ -using System.IO; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.IO; using System.Reflection; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -8,7 +11,7 @@ public class VersionSchemaTests { - private readonly ITestOutputHelper Logger; + private readonly ITestOutputHelper logger; private readonly JSchema schema; @@ -16,7 +19,7 @@ public class VersionSchemaTests public VersionSchemaTests(ITestOutputHelper logger) { - this.Logger = logger; + this.logger = logger; using (var schemaStream = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream($"{ThisAssembly.RootNamespace}.version.schema.json"))) { this.schema = JSchema.Load(new JsonTextReader(schemaStream)); @@ -97,14 +100,14 @@ public void Inherit_AllowsOmissionOfVersion() public void ReleaseProperty_ValidJson(string json) { this.json = JObject.Parse(json); - Assert.True(this.json.IsValid(this.schema)); + Assert.True(this.json.IsValid(this.schema)); } [Theory] [InlineData(@"{ ""version"": ""2.3"", ""release"": { ""versionIncrement"" : ""revision"" } }")] [InlineData(@"{ ""version"": ""2.3"", ""release"": { ""branchName"" : ""formatWithoutPlaceholder"" } }")] [InlineData(@"{ ""version"": ""2.3"", ""release"": { ""branchName"" : ""formatWithoutPlaceholder{0}"" } }")] - [InlineData(@"{ ""version"": ""2.3"", ""release"": { ""unknownProperty"" : ""value"" } }")] + [InlineData(@"{ ""version"": ""2.3"", ""release"": { ""unknownProperty"" : ""value"" } }")] public void ReleaseProperty_InvalidJson(string json) { this.json = JObject.Parse(json); diff --git a/src/NerdBank.GitVersioning.Tests/WindowsTheoryAttribute.cs b/test/Nerdbank.GitVersioning.Tests/WindowsTheoryAttribute.cs similarity index 67% rename from src/NerdBank.GitVersioning.Tests/WindowsTheoryAttribute.cs rename to test/Nerdbank.GitVersioning.Tests/WindowsTheoryAttribute.cs index 66639cc6..1a6a379a 100644 --- a/src/NerdBank.GitVersioning.Tests/WindowsTheoryAttribute.cs +++ b/test/Nerdbank.GitVersioning.Tests/WindowsTheoryAttribute.cs @@ -1,9 +1,13 @@ -using System; +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; using System.Runtime.InteropServices; using Xunit; public class WindowsTheoryAttribute : TheoryAttribute { + /// public override string Skip { get @@ -15,6 +19,7 @@ public override string Skip return null; } + set { throw new NotSupportedException(); diff --git a/test/Nerdbank.GitVersioning.Tests/app.config b/test/Nerdbank.GitVersioning.Tests/app.config new file mode 100644 index 00000000..61890f05 --- /dev/null +++ b/test/Nerdbank.GitVersioning.Tests/app.config @@ -0,0 +1,5 @@ + + + + + diff --git a/src/NerdBank.GitVersioning.Tests/repos/PackedHeadRef.zip b/test/Nerdbank.GitVersioning.Tests/repos/PackedHeadRef.zip similarity index 100% rename from src/NerdBank.GitVersioning.Tests/repos/PackedHeadRef.zip rename to test/Nerdbank.GitVersioning.Tests/repos/PackedHeadRef.zip diff --git a/src/NerdBank.GitVersioning.Tests/repos/submodules.zip b/test/Nerdbank.GitVersioning.Tests/repos/submodules.zip similarity index 100% rename from src/NerdBank.GitVersioning.Tests/repos/submodules.zip rename to test/Nerdbank.GitVersioning.Tests/repos/submodules.zip diff --git a/src/NerdBank.GitVersioning.Tests/test.prj b/test/Nerdbank.GitVersioning.Tests/test.prj similarity index 94% rename from src/NerdBank.GitVersioning.Tests/test.prj rename to test/Nerdbank.GitVersioning.Tests/test.prj index 984a3367..c7ddcca7 100644 --- a/src/NerdBank.GitVersioning.Tests/test.prj +++ b/test/Nerdbank.GitVersioning.Tests/test.prj @@ -9,7 +9,7 @@ TestCompany TestCopyright TestConfiguration - v4.5 + v4.7.2 Library bin\ $(NoWarn);1607 diff --git a/src/NerdBank.GitVersioning.Tests/test.vcprj b/test/Nerdbank.GitVersioning.Tests/test.vcprj similarity index 99% rename from src/NerdBank.GitVersioning.Tests/test.vcprj rename to test/Nerdbank.GitVersioning.Tests/test.vcprj index a3f9e7e7..d0c4915b 100644 --- a/src/NerdBank.GitVersioning.Tests/test.vcprj +++ b/test/Nerdbank.GitVersioning.Tests/test.vcprj @@ -31,7 +31,7 @@ test DynamicLibrary true - NerdBank + Nerdbank @@ -126,4 +126,4 @@ - \ No newline at end of file + diff --git a/tools/Check-DotNetRuntime.ps1 b/tools/Check-DotNetRuntime.ps1 new file mode 100644 index 00000000..9d012109 --- /dev/null +++ b/tools/Check-DotNetRuntime.ps1 @@ -0,0 +1,41 @@ +<# +.SYNOPSIS + Checks whether a given .NET Core runtime is installed. +#> +[CmdletBinding()] +Param ( + [Parameter()] + [ValidateSet('Microsoft.AspNetCore.App','Microsoft.NETCore.App')] + [string]$Runtime='Microsoft.NETCore.App', + [Parameter(Mandatory=$true)] + [Version]$Version +) + +$dotnet = Get-Command dotnet -ErrorAction SilentlyContinue +if (!$dotnet) { + # Nothing is installed. + Write-Output $false + exit 1 +} + +Function IsVersionMatch { + Param( + [Parameter()] + $actualVersion + ) + return $actualVersion -and + $Version.Major -eq $actualVersion.Major -and + $Version.Minor -eq $actualVersion.Minor -and + (($Version.Build -eq -1) -or ($Version.Build -eq $actualVersion.Build)) -and + (($Version.Revision -eq -1) -or ($Version.Revision -eq $actualVersion.Revision)) +} + +$installedRuntimes = dotnet --list-runtimes |? { $_.Split()[0] -ieq $Runtime } |% { $v = $null; [Version]::tryparse($_.Split()[1], [ref] $v); $v } +$matchingRuntimes = $installedRuntimes |? { IsVersionMatch -actualVersion $_ } +if (!$matchingRuntimes) { + Write-Output $false + exit 1 +} + +Write-Output $true +exit 0 diff --git a/tools/Check-DotNetSdk.ps1 b/tools/Check-DotNetSdk.ps1 new file mode 100644 index 00000000..6c9fa772 --- /dev/null +++ b/tools/Check-DotNetSdk.ps1 @@ -0,0 +1,37 @@ +<# +.SYNOPSIS + Checks whether the .NET Core SDK required by this repo is installed. +#> +[CmdletBinding()] +Param ( +) + +$dotnet = Get-Command dotnet -ErrorAction SilentlyContinue +if (!$dotnet) { + # Nothing is installed. + Write-Output $false + exit 1 +} + +# We need to set the current directory so dotnet considers the SDK required by our global.json file. +Push-Location "$PSScriptRoot\.." +try { + dotnet -h 2>&1 | Out-Null + if (($LASTEXITCODE -eq 129) -or # On Linux + ($LASTEXITCODE -eq -2147450751) # On Windows + ) { + # These exit codes indicate no matching SDK exists. + Write-Output $false + exit 2 + } + + # The required SDK is already installed! + Write-Output $true + exit 0 +} catch { + # I don't know why, but on some build agents (e.g. MicroBuild), an exception is thrown from the `dotnet` invocation when a match is not found. + Write-Output $false + exit 3 +} finally { + Pop-Location +} diff --git a/tools/Install-DotNetSdk.ps1 b/tools/Install-DotNetSdk.ps1 index 2300d752..e190fcfb 100755 --- a/tools/Install-DotNetSdk.ps1 +++ b/tools/Install-DotNetSdk.ps1 @@ -3,7 +3,7 @@ <# .SYNOPSIS Installs the .NET SDK specified in the global.json file at the root of this repository, - along with supporting .NET Core runtimes used for testing. + along with supporting .NET runtimes used for testing. .DESCRIPTION This MAY not require elevation, as the SDK and runtimes are installed locally to this repo location, unless `-InstallLocality machine` is specified. @@ -15,63 +15,88 @@ When using 'repo', environment variables are set to cause the locally installed dotnet SDK to be used. Per-repo can lead to file locking issues when dotnet.exe is left running as a build server and can be mitigated by running `dotnet build-server shutdown`. Per-machine requires elevation and will download and install all SDKs and runtimes to machine-wide locations so all applications can find it. +.PARAMETER SdkOnly + Skips installing the runtime. +.PARAMETER IncludeX86 + Installs a x86 SDK and runtimes in addition to the x64 ones. Only supported on Windows. Ignored on others. +.PARAMETER IncludeAspNetCore + Installs the ASP.NET Core runtime along with the .NET runtime. #> [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Medium')] Param ( [ValidateSet('repo','user','machine')] - [string]$InstallLocality='user' + [string]$InstallLocality='user', + [switch]$SdkOnly, + [switch]$IncludeX86, + [switch]$IncludeAspNetCore ) $DotNetInstallScriptRoot = "$PSScriptRoot/../obj/tools" if (!(Test-Path $DotNetInstallScriptRoot)) { New-Item -ItemType Directory -Path $DotNetInstallScriptRoot -WhatIf:$false | Out-Null } $DotNetInstallScriptRoot = Resolve-Path $DotNetInstallScriptRoot -# Look up actual required .NET Core SDK version from global.json +# Look up actual required .NET SDK version from global.json $sdkVersion = & "$PSScriptRoot/../azure-pipelines/variables/DotNetSdkVersion.ps1" +If ($IncludeX86 -and ($IsMacOS -or $IsLinux)) { + Write-Verbose "Ignoring -IncludeX86 switch because 32-bit runtimes are only supported on Windows." + $IncludeX86 = $false +} + $arch = [System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture if (!$arch) { # Windows Powershell leaves this blank $arch = 'x64' if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { $arch = 'ARM64' } + if (${env:ProgramFiles(Arm)}) { $arch = 'ARM64' } } -# Search for all .NET Core runtime versions referenced from MSBuild projects and arrange to install them. +# Search for all .NET runtime versions referenced from MSBuild projects and arrange to install them. $runtimeVersions = @() $windowsDesktopRuntimeVersions = @() -Get-ChildItem "$PSScriptRoot\..\src\*.*proj","$PSScriptRoot\..\Directory.Build.props" -Recurse |% { - $projXml = [xml](Get-Content -Path $_) - $pg = $projXml.Project.PropertyGroup - if ($pg) { - $targetFrameworks = $pg.TargetFramework - if (!$targetFrameworks) { - $targetFrameworks = $pg.TargetFrameworks - if ($targetFrameworks) { - $targetFrameworks = $targetFrameworks -Split ';' +$aspnetRuntimeVersions = @() +if (!$SdkOnly) { + Get-ChildItem "$PSScriptRoot\..\src\*.*proj","$PSScriptRoot\..\test\*.*proj","$PSScriptRoot\..\Directory.Build.props" -Recurse |% { + $projXml = [xml](Get-Content -Path $_) + $pg = $projXml.Project.PropertyGroup + if ($pg) { + $targetFrameworks = @() + $tf = $pg.TargetFramework + $targetFrameworks += $tf + $tfs = $pg.TargetFrameworks + if ($tfs) { + $targetFrameworks = $tfs -Split ';' } } - } - $targetFrameworks |? { $_ -match 'net(?:coreapp)?(\d+\.\d+)' } |% { - $v = $Matches[1] - $runtimeVersions += $v - if ($v -ge '3.0' -and -not ($IsMacOS -or $IsLinux)) { - $windowsDesktopRuntimeVersions += $v + $targetFrameworks |? { $_ -match 'net(?:coreapp)?(\d+\.\d+)' } |% { + $v = $Matches[1] + $runtimeVersions += $v + $aspnetRuntimeVersions += $v + if ($v -ge '3.0' -and -not ($IsMacOS -or $IsLinux)) { + $windowsDesktopRuntimeVersions += $v + } } - } - # Add target frameworks of the form: netXX - $targetFrameworks |? { $_ -match 'net(\d+\.\d+)' } |% { - $v = $Matches[1] - $runtimeVersions += $v - if (-not ($IsMacOS -or $IsLinux)) { - $windowsDesktopRuntimeVersions += $v + # Add target frameworks of the form: netXX + $targetFrameworks |? { $_ -match 'net(\d+\.\d+)' } |% { + $v = $Matches[1] + $runtimeVersions += $v + $aspnetRuntimeVersions += $v + if (-not ($IsMacOS -or $IsLinux)) { + $windowsDesktopRuntimeVersions += $v + } } - } + } +} + +if (!$IncludeAspNetCore) { + $aspnetRuntimeVersions = @() } Function Get-FileFromWeb([Uri]$Uri, $OutDir) { $OutFile = Join-Path $OutDir $Uri.Segments[-1] if (!(Test-Path $OutFile)) { Write-Verbose "Downloading $Uri..." + if (!(Test-Path $OutDir)) { New-Item -ItemType Directory -Path $OutDir | Out-Null } try { (New-Object System.Net.WebClient).DownloadFile($Uri, $OutFile) } finally { @@ -82,35 +107,80 @@ Function Get-FileFromWeb([Uri]$Uri, $OutDir) { $OutFile } -Function Get-InstallerExe($Version, [switch]$Runtime) { - $sdkOrRuntime = 'Sdk' - if ($Runtime) { $sdkOrRuntime = 'Runtime' } - +Function Get-InstallerExe( + $Version, + $Architecture, + [ValidateSet('Sdk','Runtime','WindowsDesktop')] + [string]$sku +) { # Get the latest/actual version for the specified one - if (([Version]$Version).Build -eq -1) { - $versionInfo = -Split (Invoke-WebRequest -Uri "https://dotnetcli.blob.core.windows.net/dotnet/$sdkOrRuntime/$Version/latest.version" -UseBasicParsing) + $TypedVersion = $null + if (![Version]::TryParse($Version, [ref] $TypedVersion)) { + Write-Error "Unable to parse $Version into an a.b.c.d version. This version cannot be installed machine-wide." + exit 1 + } + + if ($TypedVersion.Build -eq -1) { + $versionInfo = -Split (Invoke-WebRequest -Uri "https://dotnetcli.blob.core.windows.net/dotnet/$sku/$Version/latest.version" -UseBasicParsing) $Version = $versionInfo[-1] } - Get-FileFromWeb -Uri "https://dotnetcli.blob.core.windows.net/dotnet/$sdkOrRuntime/$Version/dotnet-$($sdkOrRuntime.ToLowerInvariant())-$Version-win-$arch.exe" -OutDir "$DotNetInstallScriptRoot" + $majorMinor = "$($TypedVersion.Major).$($TypedVersion.Minor)" + $ReleasesFile = Join-Path $DotNetInstallScriptRoot "$majorMinor\releases.json" + if (!(Test-Path $ReleasesFile)) { + Get-FileFromWeb -Uri "https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/$majorMinor/releases.json" -OutDir (Split-Path $ReleasesFile) | Out-Null + } + + $releases = Get-Content $ReleasesFile | ConvertFrom-Json + $url = $null + foreach ($release in $releases.releases) { + $filesElement = $null + if ($release.$sku.version -eq $Version) { + $filesElement = $release.$sku.files + } + if (!$filesElement -and ($sku -eq 'sdk') -and $release.sdks) { + foreach ($sdk in $release.sdks) { + if ($sdk.version -eq $Version) { + $filesElement = $sdk.files + break + } + } + } + + if ($filesElement) { + foreach ($file in $filesElement) { + if ($file.rid -eq "win-$Architecture") { + $url = $file.url + Break + } + } + + if ($url) { + Break + } + } + } + + if ($url) { + Get-FileFromWeb -Uri $url -OutDir $DotNetInstallScriptRoot + } else { + throw "Unable to find release of $sku v$Version" + } } -Function Install-DotNet($Version, [switch]$Runtime) { - if ($Runtime) { $sdkSubstring = '' } else { $sdkSubstring = 'SDK ' } - Write-Host "Downloading .NET Core $sdkSubstring$Version..." - $Installer = Get-InstallerExe -Version $Version -Runtime:$Runtime - Write-Host "Installing .NET Core $sdkSubstring$Version..." +Function Install-DotNet($Version, $Architecture, [ValidateSet('Sdk','Runtime','WindowsDesktop','AspNetCore')][string]$sku = 'Sdk') { + Write-Host "Downloading .NET $sku $Version..." + $Installer = Get-InstallerExe -Version $Version -Architecture $Architecture -sku $sku + Write-Host "Installing .NET $sku $Version..." cmd /c start /wait $Installer /install /passive /norestart if ($LASTEXITCODE -eq 3010) { Write-Verbose "Restart required" } elseif ($LASTEXITCODE -ne 0) { - throw "Failure to install .NET Core SDK" + throw "Failure to install .NET SDK" } } -$switches = @( - '-Architecture',$arch -) +$switches = @() $envVars = @{ # For locally installed dotnet, skip first time experience which takes a long time 'DOTNET_SKIP_FIRST_TIME_EXPERIENCE' = 'true'; @@ -121,18 +191,51 @@ if ($InstallLocality -eq 'machine') { $DotNetInstallDir = '/usr/share/dotnet' } else { $restartRequired = $false - if ($PSCmdlet.ShouldProcess(".NET Core SDK $sdkVersion", "Install")) { - Install-DotNet -Version $sdkVersion + if ($PSCmdlet.ShouldProcess(".NET SDK $sdkVersion", "Install")) { + Install-DotNet -Version $sdkVersion -Architecture $arch $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + + if ($IncludeX86) { + Install-DotNet -Version $sdkVersion -Architecture x86 + $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + } } - $runtimeVersions | Get-Unique |% { - if ($PSCmdlet.ShouldProcess(".NET Core runtime $_", "Install")) { - Install-DotNet -Version $_ -Runtime + $runtimeVersions | Sort-Object | Get-Unique |% { + if ($PSCmdlet.ShouldProcess(".NET runtime $_", "Install")) { + Install-DotNet -Version $_ -sku Runtime -Architecture $arch $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + + if ($IncludeX86) { + Install-DotNet -Version $_ -sku Runtime -Architecture x86 + $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + } + } + } + + $windowsDesktopRuntimeVersions | Sort-Object | Get-Unique |% { + if ($PSCmdlet.ShouldProcess(".NET Windows Desktop $_", "Install")) { + Install-DotNet -Version $_ -sku WindowsDesktop -Architecture $arch + $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + + if ($IncludeX86) { + Install-DotNet -Version $_ -sku WindowsDesktop -Architecture x86 + $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + } } } + $aspnetRuntimeVersions | Sort-Object | Get-Unique |% { + if ($PSCmdlet.ShouldProcess("ASP.NET Core $_", "Install")) { + Install-DotNet -Version $_ -sku AspNetCore -Architecture $arch + $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + + if ($IncludeX86) { + Install-DotNet -Version $_ -sku AspNetCore -Architecture x86 + $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + } + } + } if ($restartRequired) { Write-Host -ForegroundColor Yellow "System restart required" Exit 3010 @@ -142,25 +245,39 @@ if ($InstallLocality -eq 'machine') { } } elseif ($InstallLocality -eq 'repo') { $DotNetInstallDir = "$DotNetInstallScriptRoot/.dotnet" + $DotNetX86InstallDir = "$DotNetInstallScriptRoot/x86/.dotnet" } elseif ($env:AGENT_TOOLSDIRECTORY) { $DotNetInstallDir = "$env:AGENT_TOOLSDIRECTORY/dotnet" + $DotNetX86InstallDir = "$env:AGENT_TOOLSDIRECTORY/x86/dotnet" } else { $DotNetInstallDir = Join-Path $HOME .dotnet } -Write-Host "Installing .NET Core SDK and runtimes to $DotNetInstallDir" -ForegroundColor Blue - if ($DotNetInstallDir) { - $switches += '-InstallDir',"`"$DotNetInstallDir`"" + if (!(Test-Path $DotNetInstallDir)) { New-Item -ItemType Directory -Path $DotNetInstallDir } + $DotNetInstallDir = Resolve-Path $DotNetInstallDir + Write-Host "Installing .NET SDK and runtimes to $DotNetInstallDir" -ForegroundColor Blue $envVars['DOTNET_MULTILEVEL_LOOKUP'] = '0' $envVars['DOTNET_ROOT'] = $DotNetInstallDir } +if ($IncludeX86) { + if ($DotNetX86InstallDir) { + if (!(Test-Path $DotNetX86InstallDir)) { New-Item -ItemType Directory -Path $DotNetX86InstallDir } + $DotNetX86InstallDir = Resolve-Path $DotNetX86InstallDir + Write-Host "Installing x86 .NET SDK and runtimes to $DotNetX86InstallDir" -ForegroundColor Blue + } else { + # Only machine-wide or repo-wide installations can handle two unique dotnet.exe architectures. + Write-Error "The installation location or OS isn't supported for x86 installation. Try a different -InstallLocality value." + return 1 + } +} + if ($IsMacOS -or $IsLinux) { - $DownloadUri = "https://raw.githubusercontent.com/dotnet/install-scripts/781752509a890ca7520f1182e8bae71f9a53d754/src/dotnet-install.sh" + $DownloadUri = "https://raw.githubusercontent.com/dotnet/install-scripts/0b09de9bc136cacb5f849a6957ebd4062173c148/src/dotnet-install.sh" $DotNetInstallScriptPath = "$DotNetInstallScriptRoot/dotnet-install.sh" } else { - $DownloadUri = "https://raw.githubusercontent.com/dotnet/install-scripts/781752509a890ca7520f1182e8bae71f9a53d754/src/dotnet-install.ps1" + $DownloadUri = "https://raw.githubusercontent.com/dotnet/install-scripts/0b09de9bc136cacb5f849a6957ebd4062173c148/src/dotnet-install.ps1" $DotNetInstallScriptPath = "$DotNetInstallScriptRoot/dotnet-install.ps1" } @@ -179,47 +296,119 @@ $DotNetInstallScriptPathExpression = "& '$DotNetInstallScriptPathExpression'" $anythingInstalled = $false $global:LASTEXITCODE = 0 -if ($PSCmdlet.ShouldProcess(".NET Core SDK $sdkVersion", "Install")) { +if ($PSCmdlet.ShouldProcess(".NET SDK $sdkVersion", "Install")) { $anythingInstalled = $true - Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Version $sdkVersion $switches" + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Version $sdkVersion -Architecture $arch -InstallDir $DotNetInstallDir $switches" if ($LASTEXITCODE -ne 0) { Write-Error ".NET SDK installation failure: $LASTEXITCODE" exit $LASTEXITCODE } } else { - Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Version $sdkVersion $switches -DryRun" + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Version $sdkVersion -Architecture $arch -InstallDir $DotNetInstallDir $switches -DryRun" +} + +if ($IncludeX86) { + if ($PSCmdlet.ShouldProcess(".NET x86 SDK $sdkVersion", "Install")) { + $anythingInstalled = $true + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Version $sdkVersion -Architecture x86 -InstallDir $DotNetX86InstallDir $switches" + + if ($LASTEXITCODE -ne 0) { + Write-Error ".NET x86 SDK installation failure: $LASTEXITCODE" + exit $LASTEXITCODE + } + } else { + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Version $sdkVersion -Architecture x86 -InstallDir $DotNetX86InstallDir $switches -DryRun" + } } $dotnetRuntimeSwitches = $switches + '-Runtime','dotnet' $runtimeVersions | Sort-Object -Unique |% { - if ($PSCmdlet.ShouldProcess(".NET Core runtime $_", "Install")) { + if ($PSCmdlet.ShouldProcess(".NET $Arch runtime $_", "Install")) { $anythingInstalled = $true - Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ $dotnetRuntimeSwitches" + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $dotnetRuntimeSwitches" if ($LASTEXITCODE -ne 0) { Write-Error ".NET SDK installation failure: $LASTEXITCODE" exit $LASTEXITCODE } } else { - Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ $dotnetRuntimeSwitches -DryRun" + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $dotnetRuntimeSwitches -DryRun" + } + + if ($IncludeX86) { + if ($PSCmdlet.ShouldProcess(".NET x86 runtime $_", "Install")) { + $anythingInstalled = $true + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $dotnetRuntimeSwitches" + + if ($LASTEXITCODE -ne 0) { + Write-Error ".NET SDK installation failure: $LASTEXITCODE" + exit $LASTEXITCODE + } + } else { + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $dotnetRuntimeSwitches -DryRun" + } } } $windowsDesktopRuntimeSwitches = $switches + '-Runtime','windowsdesktop' $windowsDesktopRuntimeVersions | Sort-Object -Unique |% { - if ($PSCmdlet.ShouldProcess(".NET Core WindowsDesktop runtime $_", "Install")) { + if ($PSCmdlet.ShouldProcess(".NET WindowsDesktop $arch runtime $_", "Install")) { + $anythingInstalled = $true + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $windowsDesktopRuntimeSwitches" + + if ($LASTEXITCODE -ne 0) { + Write-Error ".NET SDK installation failure: $LASTEXITCODE" + exit $LASTEXITCODE + } + } else { + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $windowsDesktopRuntimeSwitches -DryRun" + } + + if ($IncludeX86) { + if ($PSCmdlet.ShouldProcess(".NET WindowsDesktop x86 runtime $_", "Install")) { + $anythingInstalled = $true + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $windowsDesktopRuntimeSwitches" + + if ($LASTEXITCODE -ne 0) { + Write-Error ".NET SDK installation failure: $LASTEXITCODE" + exit $LASTEXITCODE + } + } else { + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $windowsDesktopRuntimeSwitches -DryRun" + } + } +} + +$aspnetRuntimeSwitches = $switches + '-Runtime','aspnetcore' + +$aspnetRuntimeVersions | Sort-Object -Unique |% { + if ($PSCmdlet.ShouldProcess(".NET ASP.NET Core $arch runtime $_", "Install")) { $anythingInstalled = $true - Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ $windowsDesktopRuntimeSwitches" + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $aspnetRuntimeSwitches" if ($LASTEXITCODE -ne 0) { Write-Error ".NET SDK installation failure: $LASTEXITCODE" exit $LASTEXITCODE } } else { - Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ $windowsDesktopRuntimeSwitches -DryRun" + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $aspnetRuntimeSwitches -DryRun" + } + + if ($IncludeX86) { + if ($PSCmdlet.ShouldProcess(".NET ASP.NET Core x86 runtime $_", "Install")) { + $anythingInstalled = $true + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $aspnetRuntimeSwitches" + + if ($LASTEXITCODE -ne 0) { + Write-Error ".NET SDK installation failure: $LASTEXITCODE" + exit $LASTEXITCODE + } + } else { + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $aspnetRuntimeSwitches -DryRun" + } } } @@ -228,5 +417,5 @@ if ($PSCmdlet.ShouldProcess("Set DOTNET environment variables to discover these } if ($anythingInstalled -and ($InstallLocality -ne 'machine') -and !$env:TF_BUILD -and !$env:GITHUB_ACTIONS) { - Write-Warning ".NET Core runtimes or SDKs were installed to a non-machine location. Perform your builds or open Visual Studio from this same environment in order for tools to discover the location of these dependencies." + Write-Warning ".NET runtimes or SDKs were installed to a non-machine location. Perform your builds or open Visual Studio from this same environment in order for tools to discover the location of these dependencies." } diff --git a/tools/Install-NuGetCredProvider.ps1 b/tools/Install-NuGetCredProvider.ps1 new file mode 100644 index 00000000..496049a2 --- /dev/null +++ b/tools/Install-NuGetCredProvider.ps1 @@ -0,0 +1,76 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Downloads and installs the Microsoft Artifacts Credential Provider + from https://github.com/microsoft/artifacts-credprovider + to assist in authenticating to Azure Artifact feeds in interactive development + or unattended build agents. +.PARAMETER Force + Forces install of the CredProvider plugin even if one already exists. This is useful to upgrade an older version. +.PARAMETER AccessToken + An optional access token for authenticating to Azure Artifacts authenticated feeds. +#> +[CmdletBinding()] +Param ( + [Parameter()] + [switch]$Force, + [Parameter()] + [string]$AccessToken +) + +$envVars = @{} + +$toolsPath = & "$PSScriptRoot\..\azure-pipelines\Get-TempToolsPath.ps1" + +if ($IsMacOS -or $IsLinux) { + $installerScript = "installcredprovider.sh" + $sourceUrl = "https://raw.githubusercontent.com/microsoft/artifacts-credprovider/master/helpers/installcredprovider.sh" +} else { + $installerScript = "installcredprovider.ps1" + $sourceUrl = "https://raw.githubusercontent.com/microsoft/artifacts-credprovider/master/helpers/installcredprovider.ps1" +} + +$installerScript = Join-Path $toolsPath $installerScript + +if (!(Test-Path $installerScript) -or $Force) { + Invoke-WebRequest $sourceUrl -OutFile $installerScript +} + +$installerScript = (Resolve-Path $installerScript).Path + +if ($IsMacOS -or $IsLinux) { + chmod u+x $installerScript +} + +& $installerScript -Force:$Force -AddNetfx -InstallNet6 + +if ($AccessToken) { + $endpoints = @() + + $endpointURIs = @() + Get-ChildItem "$PSScriptRoot\..\nuget.config" -Recurse |% { + $nugetConfig = [xml](Get-Content -Path $_) + + $nugetConfig.configuration.packageSources.add |? { ($_.value -match '^https://pkgs\.dev\.azure\.com/') -or ($_.value -match '^https://[\w\-]+\.pkgs\.visualstudio\.com/') } |% { + if ($endpointURIs -notcontains $_.Value) { + $endpointURIs += $_.Value + $endpoint = New-Object -TypeName PSObject + Add-Member -InputObject $endpoint -MemberType NoteProperty -Name endpoint -Value $_.value + Add-Member -InputObject $endpoint -MemberType NoteProperty -Name username -Value ado + Add-Member -InputObject $endpoint -MemberType NoteProperty -Name password -Value $AccessToken + $endpoints += $endpoint + } + } + } + + $auth = New-Object -TypeName PSObject + Add-Member -InputObject $auth -MemberType NoteProperty -Name endpointCredentials -Value $endpoints + + $authJson = ConvertTo-Json -InputObject $auth + $envVars += @{ + 'VSS_NUGET_EXTERNAL_FEED_ENDPOINTS'=$authJson; + } +} + +& "$PSScriptRoot/Set-EnvVars.ps1" -Variables $envVars | Out-Null diff --git a/azure-pipelines/Set-EnvVars.ps1 b/tools/Set-EnvVars.ps1 similarity index 67% rename from azure-pipelines/Set-EnvVars.ps1 rename to tools/Set-EnvVars.ps1 index 9d14d9aa..3f6f86ba 100644 --- a/azure-pipelines/Set-EnvVars.ps1 +++ b/tools/Set-EnvVars.ps1 @@ -4,8 +4,13 @@ Azure Pipeline and CMD environments are considered. .PARAMETER Variables A hashtable of variables to be set. +.PARAMETER PrependPath + A set of paths to prepend to the PATH environment variable. .OUTPUTS A boolean indicating whether the environment variables can be expected to propagate to the caller's environment. +.DESCRIPTION + The CmdEnvScriptPath environment variable may be optionally set to a path to a cmd shell script to be created (or appended to if it already exists) that will set the environment variables in cmd.exe that are set within the PowerShell environment. + This is used by init.cmd in order to reapply any new environment variables to the parent cmd.exe process that were set in the powershell child process. #> [CmdletBinding(SupportsShouldProcess=$true)] Param( @@ -18,7 +23,7 @@ if ($Variables.Count -eq 0) { return $true } -$cmdInstructions = !$env:TF_BUILD -and !$env:GITHUB_ACTIONS -and $env:PS1UnderCmd -eq '1' +$cmdInstructions = !$env:TF_BUILD -and !$env:GITHUB_ACTIONS -and !$env:CmdEnvScriptPath -and ($env:PS1UnderCmd -eq '1') if ($cmdInstructions) { Write-Warning "Environment variables have been set that will be lost because you're running under cmd.exe" Write-Host "Environment variables that must be set manually:" -ForegroundColor Blue @@ -38,6 +43,7 @@ if ($env:GITHUB_ACTIONS) { Write-Host "GitHub Actions detected. Logging commands will be used to propagate environment variables and prepend path." } +$CmdEnvScript = '' $Variables.GetEnumerator() |% { Set-Item -Path env:$($_.Key) -Value $_.Value @@ -46,12 +52,14 @@ $Variables.GetEnumerator() |% { Write-Host "##vso[task.setvariable variable=$($_.Key);]$($_.Value)" } if ($env:GITHUB_ACTIONS) { - Write-Host "::set-env name=$($_.Key)::$($_.Value)" + Add-Content -Path $env:GITHUB_ENV -Value "$($_.Key)=$($_.Value)" } if ($cmdInstructions) { Write-Host "SET $($_.Key)=$($_.Value)" } + + $CmdEnvScript += "SET $($_.Key)=$($_.Value)`r`n" } $pathDelimiter = ';' @@ -71,9 +79,19 @@ if ($PrependPath) { Write-Host "##vso[task.prependpath]$_" } if ($env:GITHUB_ACTIONS) { - Write-Host "::add-path::$_" + Add-Content -Path $env:GITHUB_PATH -Value $_ } + + $CmdEnvScript += "SET PATH=$_$pathDelimiter%PATH%" + } +} + +if ($env:CmdEnvScriptPath) { + if (Test-Path $env:CmdEnvScriptPath) { + $CmdEnvScript = (Get-Content -Path $env:CmdEnvScriptPath) + $CmdEnvScript } + + Set-Content -Path $env:CmdEnvScriptPath -Value $CmdEnvScript } return !$cmdInstructions diff --git a/version.json b/version.json index 6c6b3a59..c6a9cad5 100644 --- a/version.json +++ b/version.json @@ -1,12 +1,18 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "3.5", + "version": "3.6", "assemblyVersion": { "precision": "revision" }, "publicReleaseRefSpec": [ - "^refs/heads/master$", + "^refs/heads/main$", "^refs/heads/develop$", "^refs/heads/v\\d+\\.\\d+$" - ] + ], + "cloudBuild": { + "setVersionVariables": false, + "buildNumber": { + "enabled": false + } + } } \ No newline at end of file