From 8177c2a9946684ebec455f251a014b3364519bc4 Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Wed, 21 Oct 2026 11:47:15 +0200 Subject: [PATCH 01/15] feat: remove LibGit2Sharp and native Git payloads --- .github/actions/docker-test/action.yml | 7 - .github/codecov.yml | 3 - .github/dependabot.yml | 4 - .github/skills/dotnet-dev/SKILL.md | 2 +- .github/workflows/_artifacts_linux.yml | 26 +- .github/workflows/_artifacts_windows.yml | 17 +- .github/workflows/_build.yml | 1 - .github/workflows/_docker.yml | 18 +- .github/workflows/_unit_tests.yml | 12 +- AGENTS.md | 4 +- .../Package/PackageContentsValidation.cs | 31 + build/build/Tasks/Package/PackageNuget.cs | 5 + build/build/Tasks/Test/UnitTest.cs | 3 +- .../Utilities/DockerContextExtensions.cs | 8 - build/parity-corpus.md | 51 -- build/parity-corpus.ps1 | 299 --------- docs/design/managed-git-migration.md | 32 +- docs/input/docs/migration/v6-to-v7.md | 6 +- new-cli/Directory.Packages.props | 1 - new-cli/GitVersion.Cli/GitVersion.Cli.csproj | 2 +- new-cli/GitVersion.Cli/Program.cs | 2 +- .../GitVersion.Common.csproj | 2 + .../Git/GitRepository.extended.cs | 3 - .../GitVersion.Core.Libgit2Sharp.csproj | 17 - .../LibGit2SharpCoreModule.cs | 9 - .../LockedFileException.cs | 20 - .../GitVersion.Core.Managed.csproj | 12 + .../ManagedCoreModule.cs | 15 + .../GitVersion.Core.Tester.csproj | 2 +- new-cli/GitVersion.Core.Tester/Program.cs | 2 +- new-cli/GitVersion.slnx | 4 +- src/Directory.Packages.props | 1 - src/GitVersion.App.Tests/CliSchemaTests.cs | 2 - .../ConfigurationQueryIntegrationTests.cs | 2 +- ...ConfigurationValidationIntegrationTests.cs | 2 - .../ExecFeatureSelectorTests.cs | 8 +- .../FeatureSelectorTests.cs | 8 +- .../PullRequestInBuildAgentTest.cs | 2 +- .../TagCheckoutInBuildAgentTests.cs | 2 +- src/GitVersion.App/CliHost.cs | 4 +- src/GitVersion.App/FeatureSelections.cs | 2 +- src/GitVersion.App/GitVersion.App.csproj | 6 +- .../GitVersion.Configuration.csproj | 1 - .../Core/DualBackendParityTests.cs | 551 ----------------- .../Core/GitBackendSelectorTests.cs | 33 +- .../Core/GitCliFixtureTests.cs | 112 ++++ .../Core/GitCliMutatorTests.cs | 2 +- .../Core/GitVersionExecutorTests.cs | 12 +- .../Core/GitVersionToolDirectoryTests.cs | 6 +- .../Core/ManagedRepositoryContractTests.cs | 362 +++++++++++ .../RepositoryStoreEqualTimestampTests.cs | 13 +- .../GitRepositoryTestingExtensions.cs | 4 +- .../GitVersion.Core.Tests.csproj | 1 - .../Helpers/GitVersionCoreTestModule.cs | 5 +- .../IntegrationTests/BranchContextTests.cs | 4 +- .../BranchWithoutCommitScenarios.cs | 2 +- .../IntegrationTests/DevelopScenarios.cs | 2 +- .../DocumentationSamplesForGitFlow.cs | 2 +- .../DocumentationSamplesForGitHubFlow.cs | 2 +- .../FeatureBranchScenarios.cs | 2 +- .../IntegrationTests/HotfixBranchScenarios.cs | 2 +- .../IntegrationTests/IgnoreCommitScenarios.cs | 10 +- .../IntegrationTests/MainScenarios.cs | 2 +- .../IntegrationTests/OtherBranchScenarios.cs | 2 +- .../IntegrationTests/OtherScenarios.cs | 2 +- .../IntegrationTests/PullRequestScenarios.cs | 2 +- .../ReleaseBranchScenarios.cs | 2 +- .../RemoteRepositoryScenarios.cs | 2 +- .../IntegrationTests/ShaLabelScenarios.cs | 4 - .../SupportBranchScenarios.cs | 2 +- .../SwitchingToGitFlowScenarios.cs | 2 +- .../TrackReleaseBranchesScenarios.cs | 4 +- .../VersionBumpingScenarios.cs | 2 +- .../VersionInCurrentBranchNameScenarios.cs | 2 +- .../VersionInMergedBranchNameScenarios.cs | 2 +- .../IntegrationTests/WorktreeScenarios.cs | 4 +- .../NextVersionCalculatorTests.cs | 2 +- src/GitVersion.Core/Git/GitBackend.cs | 50 +- src/GitVersion.Core/GitVersion.Core.csproj | 1 - .../Helpers/FileSystemHelper.cs | 4 +- .../Approved/RevisionWalks.json | 555 +++++++++++++++++ .../GitRevisionWalkerTests.cs | 104 ++-- .../GitStatusCalculatorTests.cs | 26 +- .../GitTreeDiffTests.cs | 16 +- .../GitVersion.Git.Managed.Tests.csproj | 3 + src/GitVersion.LibGit2Sharp/Git/Branch.cs | 39 -- .../Git/BranchCollection.cs | 54 -- src/GitVersion.LibGit2Sharp/Git/Commit.cs | 55 -- .../Git/CommitCollection.cs | 72 --- .../Git/GitRepository.cs | 110 ---- .../Git/GitRepository.mutating.cs | 134 ----- .../Git/GitRepositoryCache.cs | 37 -- .../Git/GitRepositoryInfo.cs | 124 ---- src/GitVersion.LibGit2Sharp/Git/ObjectId.cs | 29 - src/GitVersion.LibGit2Sharp/Git/RefSpec.cs | 22 - .../Git/RefSpecCollection.cs | 17 - src/GitVersion.LibGit2Sharp/Git/Reference.cs | 34 -- .../Git/ReferenceCollection.cs | 56 -- src/GitVersion.LibGit2Sharp/Git/Remote.cs | 41 -- .../Git/RemoteCollection.cs | 49 -- .../Git/RepositoryExtensions.cs | 30 - src/GitVersion.LibGit2Sharp/Git/Tag.cs | 46 -- .../Git/TagCollection.cs | 20 - .../Git/TreeChanges.cs | 8 - .../GitVersion.LibGit2Sharp.csproj | 18 - .../GitVersionLibGit2SharpModule.cs | 13 - .../LibGit2SharpExtensions.cs | 17 - .../PublicAPI.Shipped.txt | 10 - .../PublicAPI.Unshipped.txt | 1 - .../GitVersion.MsBuild.Tests.csproj | 2 - .../Tasks/TestTaskBase.cs | 2 +- .../Output/FormatArgumentTests.cs | 2 +- .../Extensions/GitTestExtensions.cs | 2 +- .../Fixtures/BaseGitFlowRepositoryFixture.cs | 2 +- .../Fixtures/LocalRepositoryFixture.cs | 2 +- .../Fixtures/RemoteRepositoryFixture.cs | 2 +- .../Fixtures/RepositoryFixtureBase.cs | 4 +- src/GitVersion.Testing/Generate.cs | 6 +- src/GitVersion.Testing/Git/Repository.cs | 568 ++++++++++++++++++ .../Git/RepositoryExtensions.cs | 24 + .../GitVersion.Testing.csproj | 2 +- src/GitVersion.slnx | 1 - 122 files changed, 1911 insertions(+), 2326 deletions(-) create mode 100644 build/build/Tasks/Package/PackageContentsValidation.cs delete mode 100644 build/parity-corpus.md delete mode 100644 build/parity-corpus.ps1 delete mode 100644 new-cli/GitVersion.Core.Libgit2Sharp/Git/GitRepository.extended.cs delete mode 100644 new-cli/GitVersion.Core.Libgit2Sharp/GitVersion.Core.Libgit2Sharp.csproj delete mode 100644 new-cli/GitVersion.Core.Libgit2Sharp/LibGit2SharpCoreModule.cs delete mode 100644 new-cli/GitVersion.Core.Libgit2Sharp/LockedFileException.cs create mode 100644 new-cli/GitVersion.Core.Managed/GitVersion.Core.Managed.csproj create mode 100644 new-cli/GitVersion.Core.Managed/ManagedCoreModule.cs delete mode 100644 src/GitVersion.Core.Tests/Core/DualBackendParityTests.cs create mode 100644 src/GitVersion.Core.Tests/Core/GitCliFixtureTests.cs create mode 100644 src/GitVersion.Core.Tests/Core/ManagedRepositoryContractTests.cs create mode 100644 src/GitVersion.Git.Managed.Tests/Approved/RevisionWalks.json delete mode 100644 src/GitVersion.LibGit2Sharp/Git/Branch.cs delete mode 100644 src/GitVersion.LibGit2Sharp/Git/BranchCollection.cs delete mode 100644 src/GitVersion.LibGit2Sharp/Git/Commit.cs delete mode 100644 src/GitVersion.LibGit2Sharp/Git/CommitCollection.cs delete mode 100644 src/GitVersion.LibGit2Sharp/Git/GitRepository.cs delete mode 100644 src/GitVersion.LibGit2Sharp/Git/GitRepository.mutating.cs delete mode 100644 src/GitVersion.LibGit2Sharp/Git/GitRepositoryCache.cs delete mode 100644 src/GitVersion.LibGit2Sharp/Git/GitRepositoryInfo.cs delete mode 100644 src/GitVersion.LibGit2Sharp/Git/ObjectId.cs delete mode 100644 src/GitVersion.LibGit2Sharp/Git/RefSpec.cs delete mode 100644 src/GitVersion.LibGit2Sharp/Git/RefSpecCollection.cs delete mode 100644 src/GitVersion.LibGit2Sharp/Git/Reference.cs delete mode 100644 src/GitVersion.LibGit2Sharp/Git/ReferenceCollection.cs delete mode 100644 src/GitVersion.LibGit2Sharp/Git/Remote.cs delete mode 100644 src/GitVersion.LibGit2Sharp/Git/RemoteCollection.cs delete mode 100644 src/GitVersion.LibGit2Sharp/Git/RepositoryExtensions.cs delete mode 100644 src/GitVersion.LibGit2Sharp/Git/Tag.cs delete mode 100644 src/GitVersion.LibGit2Sharp/Git/TagCollection.cs delete mode 100644 src/GitVersion.LibGit2Sharp/Git/TreeChanges.cs delete mode 100644 src/GitVersion.LibGit2Sharp/GitVersion.LibGit2Sharp.csproj delete mode 100644 src/GitVersion.LibGit2Sharp/GitVersionLibGit2SharpModule.cs delete mode 100644 src/GitVersion.LibGit2Sharp/LibGit2SharpExtensions.cs delete mode 100644 src/GitVersion.LibGit2Sharp/PublicAPI.Shipped.txt delete mode 100644 src/GitVersion.LibGit2Sharp/PublicAPI.Unshipped.txt create mode 100644 src/GitVersion.Testing/Git/Repository.cs create mode 100644 src/GitVersion.Testing/Git/RepositoryExtensions.cs diff --git a/.github/actions/docker-test/action.yml b/.github/actions/docker-test/action.yml index 0f9010eb4d..9007ad0cdb 100644 --- a/.github/actions/docker-test/action.yml +++ b/.github/actions/docker-test/action.yml @@ -10,17 +10,12 @@ inputs: dotnet_version: description: '.net version' default: '8.0' - git_backend: - description: 'Git backend to exercise (libgit2 or managed)' - default: 'libgit2' runs: using: 'composite' steps: - name: '[Docker Build & Test] DockerHub' uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 - env: - GITVERSION_GIT_BACKEND: ${{ inputs.git_backend }} with: shell: pwsh timeout_minutes: 30 @@ -33,8 +28,6 @@ runs: - name: '[Docker Build & Test] GitHub' uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 - env: - GITVERSION_GIT_BACKEND: ${{ inputs.git_backend }} with: shell: pwsh timeout_minutes: 30 diff --git a/.github/codecov.yml b/.github/codecov.yml index 311b097fce..45f8fd41d6 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -16,9 +16,6 @@ component_management: - component_id: gitversion-core name: GitVersion.Core paths: [ src/GitVersion.Core/** ] - - component_id: gitversion-libgit2sharp - name: GitVersion.LibGit2Sharp - paths: [ src/GitVersion.LibGit2Sharp/** ] - component_id: gitversion-msbuild name: GitVersion.MsBuild paths: [ src/GitVersion.MsBuild/** ] diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5627a2d082..27be02d9b6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -41,10 +41,6 @@ updates: patterns: - "NUnit.*" - "NUnit3TestAdapter" - LibGit2Sharp: - group-by: dependency-name - patterns: - - "LibGit2Sharp.*" JsonSchemaNet: group-by: dependency-name patterns: diff --git a/.github/skills/dotnet-dev/SKILL.md b/.github/skills/dotnet-dev/SKILL.md index 76af5bf533..3736c30f89 100644 --- a/.github/skills/dotnet-dev/SKILL.md +++ b/.github/skills/dotnet-dev/SKILL.md @@ -81,7 +81,7 @@ dotnet add ./src/ProjectName/ProjectName.csproj package PackageName | `GitVersion.Output` | Output formatters (JSON, BuildServer) | | `GitVersion.BuildAgents` | CI/CD platform integrations | | `GitVersion.MsBuild` | MSBuild task integration | -| `GitVersion.LibGit2Sharp` | Git repository abstraction | +| `GitVersion.Git.Managed` | Managed Git reads and Git CLI writes | ## Coding Conventions diff --git a/.github/workflows/_artifacts_linux.yml b/.github/workflows/_artifacts_linux.yml index a3d24d1043..bd0cff5bb7 100644 --- a/.github/workflows/_artifacts_linux.yml +++ b/.github/workflows/_artifacts_linux.yml @@ -56,13 +56,8 @@ jobs: - name: Set up Docker uses: $/.github/actions/docker-setup # NOSONAR -- $/ resolves to the running commit. - # Run both backends concurrently in the same job (GitHub Actions parallel steps). - # Each ArtifactsTest sub-test runs in ephemeral --rm containers, so the runs are independent. - - name: '[Test Artifacts] (libgit2)' - background: true + - name: '[Test Artifacts]' uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 - env: - GITVERSION_GIT_BACKEND: libgit2 with: shell: pwsh timeout_minutes: 30 @@ -73,22 +68,3 @@ jobs: --target=ArtifactsTest --arch=${{ inputs.arch }} ` --dotnet_version=${{ matrix.dotnet_version }} ` --docker_distro=${{ matrix.docker_distro }} - - - name: '[Test Artifacts] (managed)' - background: true - uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 - env: - GITVERSION_GIT_BACKEND: managed - with: - shell: pwsh - timeout_minutes: 30 - max_attempts: 3 - retry_on: error - command: | - dotnet run/artifacts.dll ` - --target=ArtifactsTest --arch=${{ inputs.arch }} ` - --dotnet_version=${{ matrix.dotnet_version }} ` - --docker_distro=${{ matrix.docker_distro }} - - - name: Wait for artifact backend tests - wait-all: diff --git a/.github/workflows/_artifacts_windows.yml b/.github/workflows/_artifacts_windows.yml index 569776f8b7..01fe1e1d54 100644 --- a/.github/workflows/_artifacts_windows.yml +++ b/.github/workflows/_artifacts_windows.yml @@ -38,23 +38,8 @@ jobs: name: nuget path: ${{ github.workspace }}/artifacts/packages/nuget - # In-process tests share a single package install directory, so the two backends - # run sequentially here (not as parallel background steps). - - name: '[Test Artifacts] (libgit2)' + - name: '[Test Artifacts]' uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 - env: - GITVERSION_GIT_BACKEND: libgit2 - with: - shell: pwsh - timeout_minutes: 30 - max_attempts: 3 - retry_on: error - command: dotnet run/artifacts.dll --target=Artifacts${{ matrix.package }}Test - - - name: '[Test Artifacts] (managed)' - uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 - env: - GITVERSION_GIT_BACKEND: managed with: shell: pwsh timeout_minutes: 30 diff --git a/.github/workflows/_build.yml b/.github/workflows/_build.yml index b37558e99b..5b34244679 100644 --- a/.github/workflows/_build.yml +++ b/.github/workflows/_build.yml @@ -4,7 +4,6 @@ on: env: DOTNET_INSTALL_DIR: "./.dotnet" DOTNET_ROLL_FORWARD: "Major" - GITVERSION_GIT_BACKEND: managed jobs: build: diff --git a/.github/workflows/_docker.yml b/.github/workflows/_docker.yml index 8f9199efc5..430026523b 100644 --- a/.github/workflows/_docker.yml +++ b/.github/workflows/_docker.yml @@ -59,30 +59,14 @@ jobs: - name: Set up Docker uses: $/.github/actions/docker-setup # NOSONAR -- $/ resolves to the running commit. - # Run both backends concurrently in the same job (GitHub Actions parallel steps). - # Each uses an ephemeral --rm container, so the two runs don't interfere. - - name: Docker Test (libgit2) + - name: Docker Test if: success() && inputs.publish_images == false - background: true uses: $/.github/actions/docker-test # NOSONAR -- $/ resolves to the running commit. with: arch: ${{ inputs.arch }} docker_distro: ${{ matrix.docker_distro }} dotnet_version: ${{ matrix.dotnet_version }} - git_backend: libgit2 - - name: Docker Test (managed) - if: success() && inputs.publish_images == false - background: true - uses: $/.github/actions/docker-test # NOSONAR -- $/ resolves to the running commit. - with: - arch: ${{ inputs.arch }} - docker_distro: ${{ matrix.docker_distro }} - dotnet_version: ${{ matrix.dotnet_version }} - git_backend: managed - - - name: Wait for docker backend tests - wait-all: publish: name: ${{ matrix.docker_distro }} - net${{ matrix.dotnet_version }} diff --git a/.github/workflows/_unit_tests.yml b/.github/workflows/_unit_tests.yml index c31a583ec0..baa5328127 100644 --- a/.github/workflows/_unit_tests.yml +++ b/.github/workflows/_unit_tests.yml @@ -16,7 +16,7 @@ permissions: jobs: unit_test: - name: ${{ matrix.os }} - net${{ matrix.dotnet_version }} - ${{ matrix.git_backend }} + name: ${{ matrix.os }} - net${{ matrix.dotnet_version }} permissions: id-token: write strategy: @@ -24,15 +24,11 @@ jobs: matrix: os: [ windows-2025-vs2026, ubuntu-24.04, macos-26 ] dotnet_version: ${{ fromJson(inputs.dotnet_versions) }} - # Run the suite against both Git backends until libgit2 is removed, - # see https://github.com/GitTools/GitVersion/issues/5031 - git_backend: [ libgit2, managed ] runs-on: ${{ matrix.os }} timeout-minutes: 45 env: - GITVERSION_GIT_BACKEND: ${{ matrix.git_backend }} - TEST_RESULTS: ${{ github.workspace }}/artifacts/test-results/${{ matrix.git_backend }}/attempt-${{ github.run_attempt }} + TEST_RESULTS: ${{ github.workspace }}/artifacts/test-results/attempt-${{ github.run_attempt }} steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1 @@ -83,7 +79,7 @@ jobs: uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 if: always() with: - name: test-results-${{ matrix.os }}-net${{ matrix.dotnet_version }}-${{ matrix.git_backend }}-${{ github.run_id }}-${{ github.run_attempt }} + name: test-results-${{ matrix.os }}-net${{ matrix.dotnet_version }}-${{ github.run_id }}-${{ github.run_attempt }} path: artifacts/test-results/ if-no-files-found: warn retention-days: 7 @@ -94,7 +90,6 @@ jobs: with: files: artifacts/test-results/**/results.xml report_type: 'test_results' - flags: ${{ matrix.git_backend }} use_oidc: true - name: Upload Coverage @@ -103,5 +98,4 @@ jobs: with: directory: artifacts/test-results report_type: 'coverage' - flags: ${{ matrix.git_backend }} use_oidc: true diff --git a/AGENTS.md b/AGENTS.md index dda6a3b100..03f49db6e3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,7 +29,7 @@ The repo has two parallel solution trees: | `GitVersion.Configuration` | Configuration loading and validation | | `GitVersion.App` | CLI entry point | | `GitVersion.BuildAgents` | Platform-specific build agent adapters | -| `GitVersion.LibGit2Sharp` | Git repository access | +| `GitVersion.Git.Managed` | Managed Git reads and Git CLI writes | | `*Tests` projects | Unit and integration tests | Build-agent adapters live in `src/GitVersion.BuildAgents/Agents/`. They write `GitVersion_`-prefixed environment variables — preserve that prefix when reading or writing outputs. @@ -45,7 +45,7 @@ Build-agent adapters live in `src/GitVersion.BuildAgents/Agents/`. They write `G | `GitVersion.Normalization` | Normalization plugin | | `GitVersion.Output` | Output plugin | | `GitVersion.Common` | Shared utilities | -| `GitVersion.Core.Libgit2Sharp` | Git repository access | +| `GitVersion.Core.Managed` | Managed Git repository access | | `GitVersion.Cli.Generator` | Source generator for CLI commands | | `GitVersion.Cli.Generator.Tests` | Generator tests | diff --git a/build/build/Tasks/Package/PackageContentsValidation.cs b/build/build/Tasks/Package/PackageContentsValidation.cs new file mode 100644 index 0000000000..4958fb1017 --- /dev/null +++ b/build/build/Tasks/Package/PackageContentsValidation.cs @@ -0,0 +1,31 @@ +using System.IO.Compression; + +namespace Build.Tasks; + +internal static class PackageContentsValidation +{ + public static void Validate(string packagePath) + { + using var archive = ZipFile.OpenRead(packagePath); + foreach (var entry in archive.Entries) + { + var path = entry.FullName.Replace('\\', '/'); + if (path.Contains("libgit2", StringComparison.OrdinalIgnoreCase) + || path.Contains("runtimes/", StringComparison.OrdinalIgnoreCase) && path.Contains("/native/", StringComparison.OrdinalIgnoreCase)) + { + throw new CakeException($"Native Git or runtime payload '{path}' is forbidden in {packagePath}."); + } + + if (!path.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase) + && !path.EndsWith(".deps.json", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + using var reader = new StreamReader(entry.Open()); + if (reader.ReadToEnd().Contains("LibGit2Sharp", StringComparison.OrdinalIgnoreCase)) + { + throw new CakeException($"LibGit2Sharp dependency remains in '{path}' in {packagePath}."); + } + } + } +} diff --git a/build/build/Tasks/Package/PackageNuget.cs b/build/build/Tasks/Package/PackageNuget.cs index d1f5f6a6e2..a489f0058f 100644 --- a/build/build/Tasks/Package/PackageNuget.cs +++ b/build/build/Tasks/Package/PackageNuget.cs @@ -41,5 +41,10 @@ private static void PackageWithCli(BuildContext context) settings.ArgumentCustomization = arg => arg.Append("-p:IsPackaging=true"); context.DotNetPack("./src/GitVersion.MsBuild", settings); + + foreach (var package in context.GetFiles($"{Paths.Nuget}/GitVersion.Tool*.nupkg") + context.GetFiles($"{Paths.Nuget}/GitVersion.MsBuild*.nupkg")) + { + PackageContentsValidation.Validate(package.FullPath); + } } } diff --git a/build/build/Tasks/Test/UnitTest.cs b/build/build/Tasks/Test/UnitTest.cs index 8989d4a0f2..5f1af0c339 100644 --- a/build/build/Tasks/Test/UnitTest.cs +++ b/build/build/Tasks/Test/UnitTest.cs @@ -90,10 +90,9 @@ private static int TestProjectForTarget(BuildContext context, FilePath project, throw new CakeException($"Test assembly not found for {project} / net{framework}: {targetPath}"); } - var backend = context.EnvironmentVariable("GITVERSION_GIT_BACKEND") ?? "default"; var attempt = context.EnvironmentVariable("GITHUB_RUN_ATTEMPT") ?? "local"; var root = new DirectoryPath(context.Argument(Arguments.TestResults, - Paths.TestOutput.Combine(backend).Combine($"attempt-{attempt}").FullPath)); + Paths.TestOutput.Combine($"attempt-{attempt}").FullPath)); var resultsDirectory = context.MakeAbsolute(root.Combine(project.GetFilenameWithoutExtension().FullPath).Combine($"net{framework}")); context.CleanDirectory(resultsDirectory); // CI publishes coverage from the LTS target; other targets validate runtime compatibility. diff --git a/build/common/Utilities/DockerContextExtensions.cs b/build/common/Utilities/DockerContextExtensions.cs index c4c11e77c1..31a7a341c0 100644 --- a/build/common/Utilities/DockerContextExtensions.cs +++ b/build/common/Utilities/DockerContextExtensions.cs @@ -279,14 +279,6 @@ private DockerContainerRunSettings GetDockerRunSettings(Architecture arch, bool ]; } - // Forward the selected Git backend into the container so the same image/package is - // exercised against both libgit2 and managed (set per-step in CI). Absent = managed default. - var gitBackend = context.EnvironmentVariable("GITVERSION_GIT_BACKEND"); - if (!string.IsNullOrWhiteSpace(gitBackend)) - { - settings.Env = [.. settings.Env ?? [], $"GITVERSION_GIT_BACKEND={gitBackend}"]; - } - // SDK artifact tests need writable CLI configuration. Final-image tests // deliberately inherit HOME to exercise the entrypoint's UID fallback. if (useSdkHome) diff --git a/build/parity-corpus.md b/build/parity-corpus.md deleted file mode 100644 index 0d308fc44f..0000000000 --- a/build/parity-corpus.md +++ /dev/null @@ -1,51 +0,0 @@ -# parity-corpus.ps1 — real-world corpus parity check - -Phase B validation tooling for the managed-git migration -(see `docs/design/managed-git-migration.md`, §5 Phase B and §7 item 3). - -Runs the locally built `GitVersion.App` twice per repository/checkout shape — -once with `GITVERSION_GIT_BACKEND=libgit2`, once with `=managed` — using -`--output json --no-cache --no-fetch --no-normalize` (read-only, deterministic) -and diffs the JSON version variables field by field. - -## Usage - -```bash -# default corpus: GitVersion, GitReleaseManager, and this checkout itself -pwsh ./build/parity-corpus.ps1 - -# custom corpus / subset of variants / reuse binaries -pwsh ./build/parity-corpus.ps1 -Repos /path/to/repo -Variants full,worktree -SkipBuild - -# keep clones in a persistent cache -pwsh ./build/parity-corpus.ps1 -WorkDir ~/.cache/gitversion-parity -``` - -## Parameters - -| Parameter | Default | Meaning | -|---|---|---| -| `-Repos` | GitVersion, GitReleaseManager, local checkout | Git URLs or local paths | -| `-WorkDir` | `$TMPDIR/gitversion-parity-corpus` | Clone cache; repeat runs reuse it | -| `-Variants` | `full,shallow,worktree` | Checkout shapes to exercise | -| `-Configuration` / `-Framework` | `Release` / `net10.0` | Which App build to run | -| `-SkipBuild` | off | Reuse existing `GitVersion.App` binaries | - -## What it does per repository - -1. Full clone into the cache (skipped when already present). -2. Derives a **shallow** clone (`git clone --depth 1 file://`, run with - `--allow-shallow`) and a **linked worktree** (`git worktree add --detach`) — the two - CI-style shapes called out in the migration design. -3. Runs both backends per variant, normalizes/parses the JSON, and reports: - - `MATCH` — identical variables, - - `DIFF` — any field difference, or one backend failing while the other succeeds - (a parity failure; a readable per-field diff is printed), - - `ERROR` — variant setup failed or *both* backends refused the shape - (environment limitation, not counted as a parity failure). -4. Prints a summary table with per-backend elapsed milliseconds (coarse perf signal). - -## Exit code - -The number of repo/variant combinations with result `DIFF`. `0` = full parity. -Errors do not abort the run; remaining repos/variants are still checked. diff --git a/build/parity-corpus.ps1 b/build/parity-corpus.ps1 deleted file mode 100644 index 5ac6e38974..0000000000 --- a/build/parity-corpus.ps1 +++ /dev/null @@ -1,299 +0,0 @@ -#!/usr/bin/env pwsh -#Requires -Version 7.0 -<# -.SYNOPSIS - Real-world corpus parity check between the libgit2 and managed git backends. - -.DESCRIPTION - For every repository in the corpus this script prepares three checkout shapes - (full clone, shallow clone, linked worktree), runs the locally built - GitVersion.App once per backend (GITVERSION_GIT_BACKEND=libgit2|managed) with - read-only, cache-free flags, and diffs the JSON outputs field by field. - - Any field difference (or a one-sided failure) is a parity failure. The exit - code is the number of failing repo/variant combinations; 0 means full parity. - -.EXAMPLE - ./build/parity-corpus.ps1 - ./build/parity-corpus.ps1 -Repos https://github.com/GitTools/GitReleaseManager.git -Variants full,shallow - ./build/parity-corpus.ps1 -Repos /path/to/local/repo -WorkDir ~/parity-cache -SkipBuild -#> -[CmdletBinding()] -param( - # Git URLs or local paths to include in the corpus. - [string[]]$Repos, - - # Cache directory for clones; repeat runs reuse it. - [string]$WorkDir = (Join-Path ([IO.Path]::GetTempPath()) 'gitversion-parity-corpus'), - - # Checkout shapes to exercise per repository. - [ValidateSet('full', 'shallow', 'worktree')] - [string[]]$Variants = @('full', 'shallow', 'worktree'), - - [string]$Configuration = 'Release', - [string]$Framework = 'net10.0', - - # Skip building GitVersion.App (reuse existing binaries). - [switch]$SkipBuild -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path -if (-not $Repos) { - $Repos = @( - 'https://github.com/GitTools/GitVersion.git' - 'https://github.com/GitTools/GitReleaseManager.git' - $repoRoot - ) -} - -# --------------------------------------------------------------------------- -# helpers -# --------------------------------------------------------------------------- - -function Invoke-Git { - param([string[]]$Arguments) - $output = & git @Arguments 2>&1 - if ($LASTEXITCODE -ne 0) { - throw "git $($Arguments -join ' ') failed ($LASTEXITCODE): $($output | Out-String)" - } - $output -} - -function Get-CorpusName { - param([string]$Source) - $base = ([IO.Path]::GetFileName($Source.TrimEnd('/', '\'))) -replace '\.git$', '' - if (-not $base) { $base = 'repo' } - $hashBytes = [System.Security.Cryptography.SHA256]::HashData([Text.Encoding]::UTF8.GetBytes($Source)) - $suffix = -join ($hashBytes[0..3] | ForEach-Object { $_.ToString('x2') }) - "$base-$suffix" -} - -function Invoke-GitVersion { - <# Runs gitversion.dll with the given backend; returns exit code, stdout, stderr, elapsed ms. #> - param( - [string]$AppDll, - [string]$RepoPath, - [string]$Backend, - [string[]]$ExtraArgs - ) - $logFile = [IO.Path]::GetTempFileName() - $psi = [System.Diagnostics.ProcessStartInfo]::new() - $psi.FileName = 'dotnet' - $argList = @($AppDll, $RepoPath, '--output', 'json', '--no-cache', '--no-fetch', '--no-normalize', '-l', $logFile) + @($ExtraArgs) - foreach ($a in $argList) { - if (-not [string]::IsNullOrEmpty($a)) { $psi.ArgumentList.Add($a) } - } - $psi.RedirectStandardOutput = $true - $psi.RedirectStandardError = $true - $psi.UseShellExecute = $false - $psi.EnvironmentVariables['GITVERSION_GIT_BACKEND'] = $Backend - - $sw = [System.Diagnostics.Stopwatch]::StartNew() - $process = [System.Diagnostics.Process]::Start($psi) - $stdout = $process.StandardOutput.ReadToEnd() - $stderr = $process.StandardError.ReadToEnd() - $process.WaitForExit() - $sw.Stop() - - # On failure the reason usually only appears in the log; pull the first error line out. - $errorSummary = '' - if ($process.ExitCode -ne 0) { - $errorSummary = ($stderr + "`n" + $stdout).Trim() -split '\r?\n' | - Where-Object { $_ } | - Select-Object -First 1 - if (-not $errorSummary -and (Test-Path $logFile)) { - $errorSummary = Get-Content $logFile | - Where-Object { $_ -match 'EROR' -or $_ -match 'Exception' } | - Select-Object -First 2 | - Join-String -Separator ' ' - } - } - Remove-Item $logFile -ErrorAction SilentlyContinue - - [pscustomobject]@{ - ExitCode = $process.ExitCode - StdOut = $stdout - StdErr = $stderr - ErrorSummary = [string]$errorSummary - ElapsedMs = $sw.ElapsedMilliseconds - } -} - -function ConvertFrom-GitVersionJson { - <# Extracts the JSON object from gitversion stdout and returns a hashtable, or $null. #> - param([string]$Text) - $start = $Text.IndexOf('{') - $end = $Text.LastIndexOf('}') - if ($start -lt 0 -or $end -le $start) { return $null } - try { - $Text.Substring($start, $end - $start + 1) | ConvertFrom-Json -AsHashtable - } catch { - $null - } -} - -function Compare-VersionVariables { - <# Field-by-field diff of two hashtables; returns a list of difference descriptions. #> - param([hashtable]$Left, [hashtable]$Right) - $differences = [System.Collections.Generic.List[string]]::new() - $keys = @($Left.Keys) + @($Right.Keys) | Sort-Object -Unique - foreach ($key in $keys) { - $l = if ($Left.ContainsKey($key)) { [string]$Left[$key] } else { '' } - $r = if ($Right.ContainsKey($key)) { [string]$Right[$key] } else { '' } - if ($l -cne $r) { - $differences.Add((" {0,-28} libgit2: {1} | managed: {2}" -f $key, $l, $r)) - } - } - $differences -} - -function Get-VariantPath { - <# Ensures the requested checkout shape exists under the repo's cache dir; returns its path. #> - param([string]$RepoCacheDir, [string]$Source, [string]$Variant) - - $fullDir = Join-Path $RepoCacheDir 'full' - if (-not (Test-Path (Join-Path $fullDir '.git'))) { - Write-Verbose "cloning $Source -> $fullDir" - $null = Invoke-Git @('clone', '--quiet', $Source, $fullDir) - } - if ($Variant -eq 'full') { return $fullDir } - - if ($Variant -eq 'shallow') { - $shallowDir = Join-Path $RepoCacheDir 'shallow' - if (-not (Test-Path (Join-Path $shallowDir '.git'))) { - # Derive the shallow clone from the local full clone: deterministic and offline. - $fullUri = ([System.Uri]::new($fullDir)).AbsoluteUri - Write-Verbose "creating shallow clone -> $shallowDir" - $null = Invoke-Git @('clone', '--quiet', '--depth', '1', $fullUri, $shallowDir) - } - return $shallowDir - } - - # worktree - $worktreeDir = Join-Path $RepoCacheDir 'worktree' - if (-not (Test-Path (Join-Path $worktreeDir '.git'))) { - Write-Verbose "creating linked worktree -> $worktreeDir" - $null = Invoke-Git @('-C', $fullDir, 'worktree', 'prune') - $null = Invoke-Git @('-C', $fullDir, 'worktree', 'add', '--detach', $worktreeDir) - } - return $worktreeDir -} - -# --------------------------------------------------------------------------- -# build once -# --------------------------------------------------------------------------- - -$appDll = Join-Path $repoRoot "src/GitVersion.App/bin/$Configuration/$Framework/gitversion.dll" -if (-not $SkipBuild) { - Write-Host "Building GitVersion.App ($Configuration, $Framework)..." - & dotnet build (Join-Path $repoRoot 'src/GitVersion.App/GitVersion.App.csproj') -c $Configuration -f $Framework --nologo -v q - if ($LASTEXITCODE -ne 0) { throw 'dotnet build failed.' } -} -if (-not (Test-Path $appDll)) { throw "GitVersion.App binary not found at $appDll (build it or drop -SkipBuild)." } - -$null = New-Item -ItemType Directory -Force -Path $WorkDir -Write-Host "Corpus cache: $WorkDir" -Write-Host "App: $appDll" -Write-Host '' - -# --------------------------------------------------------------------------- -# run corpus -# --------------------------------------------------------------------------- - -$results = [System.Collections.Generic.List[pscustomobject]]::new() - -foreach ($source in $Repos) { - $name = Get-CorpusName $source - $repoCacheDir = Join-Path $WorkDir $name - $null = New-Item -ItemType Directory -Force -Path $repoCacheDir - Write-Host "=== $source ($name) ===" - - foreach ($variant in $Variants) { - $record = [pscustomobject]@{ - Repo = $source - Variant = $variant - Result = 'ERROR' - Libgit2Ms = $null - ManagedMs = $null - Detail = '' - } - $results.Add($record) - - try { - $variantPath = Get-VariantPath -RepoCacheDir $repoCacheDir -Source $source -Variant $variant - } catch { - $record.Detail = "variant setup failed: $($_.Exception.Message)" - Write-Warning "[$name/$variant] $($record.Detail)" - continue - } - - [string[]]$extraArgs = @() - if ($variant -eq 'shallow') { $extraArgs = @('--allow-shallow') } - - $runs = @{} - foreach ($backend in 'libgit2', 'managed') { - $runs[$backend] = Invoke-GitVersion -AppDll $appDll -RepoPath $variantPath -Backend $backend -ExtraArgs $extraArgs - } - $record.Libgit2Ms = $runs['libgit2'].ElapsedMs - $record.ManagedMs = $runs['managed'].ElapsedMs - - $failed = @('libgit2', 'managed').Where({ $runs[$_].ExitCode -ne 0 }) - if ($failed.Count -eq 2) { - # Both backends refuse this shape identically: an environment limitation, not a parity break. - $record.Detail = 'both backends failed: ' + $runs['libgit2'].ErrorSummary - Write-Warning "[$name/$variant] $($record.Detail)" - continue - } - if ($failed.Count -eq 1) { - $only = $failed[0] - $record.Result = 'DIFF' - $record.Detail = "$only failed (exit $($runs[$only].ExitCode)) while the other backend succeeded: " + $runs[$only].ErrorSummary - Write-Host "[$name/$variant] PARITY FAILURE - $($record.Detail)" -ForegroundColor Red - continue - } - - $libgit2Json = ConvertFrom-GitVersionJson $runs['libgit2'].StdOut - $managedJson = ConvertFrom-GitVersionJson $runs['managed'].StdOut - if (-not $libgit2Json -or -not $managedJson) { - $record.Detail = 'could not parse JSON output from one or both backends' - Write-Warning "[$name/$variant] $($record.Detail)" - continue - } - - $differences = @(Compare-VersionVariables -Left $libgit2Json -Right $managedJson) - if ($differences.Count -eq 0) { - $record.Result = 'MATCH' - Write-Host "[$name/$variant] MATCH (libgit2 $($record.Libgit2Ms) ms, managed $($record.ManagedMs) ms)" -ForegroundColor Green - } else { - $record.Result = 'DIFF' - $record.Detail = "$($differences.Count) field(s) differ" - Write-Host "[$name/$variant] PARITY FAILURE - $($record.Detail):" -ForegroundColor Red - $differences | ForEach-Object { Write-Host $_ -ForegroundColor Red } - } - } - Write-Host '' -} - -# --------------------------------------------------------------------------- -# summary -# --------------------------------------------------------------------------- - -Write-Host '=== Summary ===' -$results | - Format-Table Repo, Variant, Result, Libgit2Ms, ManagedMs, Detail -AutoSize | - Out-String -Width 500 | - Write-Host - -$failures = @($results | Where-Object Result -eq 'DIFF') -$errors = @($results | Where-Object Result -eq 'ERROR') -if ($failures.Count -eq 0 -and $errors.Count -eq 0) { - Write-Host 'Full parity across the corpus.' -ForegroundColor Green -} elseif ($failures.Count -eq 0) { - Write-Host "No parity failures, but $($errors.Count) variant(s) could not be compared (ERROR)." -ForegroundColor Yellow -} else { - Write-Host "$($failures.Count) repo/variant combination(s) failed parity." -ForegroundColor Red -} -exit $failures.Count diff --git a/docs/design/managed-git-migration.md b/docs/design/managed-git-migration.md index e78e219b5b..de288a3373 100644 --- a/docs/design/managed-git-migration.md +++ b/docs/design/managed-git-migration.md @@ -1,5 +1,25 @@ # Replacing LibGit2Sharp with a managed Git implementation +## Implementation status after #5040 + +The established CLI and experimental CLI now use the managed backend exclusively. +Both LibGit2Sharp adapter projects and all package dependencies are removed. +Repository scenario fixtures use Git CLI writes and reads; calculation assertions +open the managed reader. The previous ordered revision-walk oracle is retained as +recorded expectations captured from LibGit2Sharp 0.32.0. Other migration scenarios +use explicit expectations or Git CLI oracles, without shipping a native test dependency. +Ordinary package verification rejects native Git payloads before single-file bundling +and checks Tool/RID/MSBuild nupkgs for native entries or LibGit2Sharp dependencies. + +Release sequencing remains v7.0 managed default, then v7.1 removal. Explicit +`managed` remains a no-op throughout v7.x; `libgit2` produces an actionable removal +error from v7.1. Removing the compatibility variable belongs to #5136 in v8. +Parser/configuration removal is separate (#5188). NativeAOT remains separate (#3981). + +The survey and phase estimates below describe the migration's original baseline, +not remaining dependencies in the current source tree. + + Research and migration plan for [#236][236] — researched 2026-07, based on a full survey of the `next/v7` tree. ## 1. Motivation @@ -19,7 +39,7 @@ LibGit2Sharp wraps the native `libgit2` library and has been a recurring source (linux x64/arm/arm64/musl/ppc64le, osx x64/arm64, win x64/x86/arm64) into `runtimes//native/` of every published artifact — and `GitVersion.MsBuild` ships that payload **per TFM** (net8.0/net9.0/net10.0). - **Platform lag**: new RIDs, libcs and OS versions require waiting on libgit2 + LibGit2Sharp + NativeBinaries releases - (GitVersion is currently pinned to LibGit2Sharp 0.31.0). + (GitVersion is previously pinned to LibGit2Sharp 0.32.0). - **Feature lag**: libgit2 trails git itself (SHA-256 repos, reftable, partial clone), and its global native state complicates concurrent use inside MSBuild. @@ -174,11 +194,11 @@ CLI mutator, later also the managed reader). The version-to-default mapping is e | Release | Default backend | Opt-in / opt-out | |---|---|---| -| **v7.0** | `libgit2` | `GITVERSION_GIT_BACKEND=managed` to test the new backend | -| **v7.1** | `managed` | `GITVERSION_GIT_BACKEND=libgit2` to fall back | -| v7.x (later) | `managed` | libgit2 removed (Phase E) once the fallback has gone several releases unused/regression-free | +| **v7.0** | `managed` | `GITVERSION_GIT_BACKEND=libgit2` for the temporary fallback | +| **v7.1** | `managed` | `libgit2` fails; explicit `managed` remains accepted | +| v7.x (later) | `managed` | Explicit `managed` remains a compatibility no-op | -Both backends ship side by side throughout the v7.0–v7.x window so users can validate `managed` against +Historical v7.0 validation: both backends shipped side by side during that window so users can validate `managed` against their real repositories and report issues before libgit2 is dropped. For that whole window, the CI unit-test matrix runs the full suite against both backends (`git_backend` dimension in `_unit_tests.yml`) — both legs must stay green. @@ -215,7 +235,7 @@ backend selection via `GITVERSION_GIT_BACKEND=managed|libgit2` (initially `libgi parser/configuration selectors default to `v7`. All selectors trim values, ignore case, treat blanks as unset and reject unknown values. Effective selections are logged without mixing logs into machine-readable stdout. -The dual-backend CI matrix stays green throughout the v7.0 window. +The dual-backend CI matrix was the v7.0 validation gate. After #5040, CI runs only the managed implementation across the existing OS/TFM matrix. ### Phase D — fixture migration (parallel with B) · ~2–3 weeks `GitVersion.Testing` moves to pure git-CLI writes via the existing `ExecuteGitCmd` pattern: diff --git a/docs/input/docs/migration/v6-to-v7.md b/docs/input/docs/migration/v6-to-v7.md index 99fae6014d..7f146abf9a 100644 --- a/docs/input/docs/migration/v6-to-v7.md +++ b/docs/input/docs/migration/v6-to-v7.md @@ -278,7 +278,7 @@ migrate` to retain schema validation. GitVersion v7.0 uses the managed Git backend by default. The `GITVERSION_GIT_BACKEND` environment variable accepts `managed` or the temporary `libgit2` fallback. :::{.alert .alert-info} -In v7.0, use `GITVERSION_GIT_BACKEND=libgit2` only if you need the temporary native backend fallback. LibGit2Sharp and its native binaries are scheduled for removal in v7.1. Explicit `managed` remains accepted throughout v7.x; the selector is removed in v8. +In v7.0, use `GITVERSION_GIT_BACKEND=libgit2` only if you need the temporary native backend fallback. From v7.1, LibGit2Sharp and its native binaries are removed. Selecting `libgit2` fails with guidance to unset the variable or select `managed`. Explicit `managed` remains accepted throughout v7.x; the selector is removed in v8. ::: - `libgit2` — the temporary native backend fallback in v7.0. @@ -295,7 +295,7 @@ The environment variables relevant to migrating from v6 to v7: | Variable | Purpose | | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `GITVERSION_CONFIGURATION_VERSION` | Selects the configuration layout: `v7` (default) or temporary flat `v6` fallback. | -| `GITVERSION_GIT_BACKEND` | Selects the Git backend: `managed` (default) or temporary `libgit2` fallback. See [Git backend][git-backend]. | +| `GITVERSION_GIT_BACKEND` | Compatibility setting: `managed` is accepted; `libgit2` was available only in v7.0 and fails from v7.1. See [Git backend][git-backend]. | | `GITVERSION_ARGUMENT_PARSER_VERSION` | Selects the argument parser: `v7` (default, POSIX syntax) or temporary `v6` fallback (`/switch` and `-switch`). | | `GITVERSION_REMOTE_USERNAME` | Alternative to `--username` for dynamic-repository credentials. | | `GITVERSION_REMOTE_PASSWORD` | Alternative to `--password` for dynamic-repository credentials. | @@ -315,7 +315,7 @@ retain their normal output channel. For migration, place logging options before `gitversion --log-file console config migrate --config GitVersion.yml`. In v7.1, legacy parser and flat configuration runtime support are scheduled -for removal alongside libgit2. Retired `v6`/`libgit2` selections will report +for removal separately from the completed libgit2 removal. Retired `v6`/`libgit2` selections report actionable errors; explicit `v7`/`managed` selections remain accepted during v7.x. All three selectors are removed in v8. `gitversion config migrate` remains available to convert flat files after runtime support is removed. diff --git a/new-cli/Directory.Packages.props b/new-cli/Directory.Packages.props index 5cc5cda5fc..11c9dc9f1d 100644 --- a/new-cli/Directory.Packages.props +++ b/new-cli/Directory.Packages.props @@ -5,7 +5,6 @@ - diff --git a/new-cli/GitVersion.Cli/GitVersion.Cli.csproj b/new-cli/GitVersion.Cli/GitVersion.Cli.csproj index db8ac4e94e..32bc610b3c 100644 --- a/new-cli/GitVersion.Cli/GitVersion.Cli.csproj +++ b/new-cli/GitVersion.Cli/GitVersion.Cli.csproj @@ -23,7 +23,7 @@ - + diff --git a/new-cli/GitVersion.Cli/Program.cs b/new-cli/GitVersion.Cli/Program.cs index 94a736d8f2..615ca0b76e 100644 --- a/new-cli/GitVersion.Cli/Program.cs +++ b/new-cli/GitVersion.Cli/Program.cs @@ -7,7 +7,7 @@ var modules = new IGitVersionModule[] { new CoreModule(), - new LibGit2SharpCoreModule(), + new ManagedCoreModule(), new GitVersion.Generated.CommandsModule() }; diff --git a/new-cli/GitVersion.Common/GitVersion.Common.csproj b/new-cli/GitVersion.Common/GitVersion.Common.csproj index 2e1e3444d7..038c61c1f8 100644 --- a/new-cli/GitVersion.Common/GitVersion.Common.csproj +++ b/new-cli/GitVersion.Common/GitVersion.Common.csproj @@ -11,6 +11,7 @@ + @@ -24,5 +25,6 @@ + diff --git a/new-cli/GitVersion.Core.Libgit2Sharp/Git/GitRepository.extended.cs b/new-cli/GitVersion.Core.Libgit2Sharp/Git/GitRepository.extended.cs deleted file mode 100644 index c7286ff0fb..0000000000 --- a/new-cli/GitVersion.Core.Libgit2Sharp/Git/GitRepository.extended.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace GitVersion.Git; - -internal sealed partial class GitRepository : IGitRepository; diff --git a/new-cli/GitVersion.Core.Libgit2Sharp/GitVersion.Core.Libgit2Sharp.csproj b/new-cli/GitVersion.Core.Libgit2Sharp/GitVersion.Core.Libgit2Sharp.csproj deleted file mode 100644 index a42d02992f..0000000000 --- a/new-cli/GitVersion.Core.Libgit2Sharp/GitVersion.Core.Libgit2Sharp.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/new-cli/GitVersion.Core.Libgit2Sharp/LibGit2SharpCoreModule.cs b/new-cli/GitVersion.Core.Libgit2Sharp/LibGit2SharpCoreModule.cs deleted file mode 100644 index 40572492dc..0000000000 --- a/new-cli/GitVersion.Core.Libgit2Sharp/LibGit2SharpCoreModule.cs +++ /dev/null @@ -1,9 +0,0 @@ -using GitVersion.Infrastructure; -using Microsoft.Extensions.DependencyInjection; - -namespace GitVersion.Git; - -public class LibGit2SharpCoreModule : IGitVersionModule -{ - public IServiceCollection RegisterTypes(IServiceCollection services) => services.AddSingleton(); -} diff --git a/new-cli/GitVersion.Core.Libgit2Sharp/LockedFileException.cs b/new-cli/GitVersion.Core.Libgit2Sharp/LockedFileException.cs deleted file mode 100644 index 3cef30a364..0000000000 --- a/new-cli/GitVersion.Core.Libgit2Sharp/LockedFileException.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace GitVersion; - -public class LockedFileException : Exception -{ - public LockedFileException(Exception inner) : base(inner.Message, inner) - { - } - - public LockedFileException() - { - } - - public LockedFileException(string? message) : base(message) - { - } - - public LockedFileException(string? message, Exception? innerException) : base(message, innerException) - { - } -} diff --git a/new-cli/GitVersion.Core.Managed/GitVersion.Core.Managed.csproj b/new-cli/GitVersion.Core.Managed/GitVersion.Core.Managed.csproj new file mode 100644 index 0000000000..82cc014a58 --- /dev/null +++ b/new-cli/GitVersion.Core.Managed/GitVersion.Core.Managed.csproj @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/new-cli/GitVersion.Core.Managed/ManagedCoreModule.cs b/new-cli/GitVersion.Core.Managed/ManagedCoreModule.cs new file mode 100644 index 0000000000..b39cda0744 --- /dev/null +++ b/new-cli/GitVersion.Core.Managed/ManagedCoreModule.cs @@ -0,0 +1,15 @@ +using GitVersion.Infrastructure; +using Microsoft.Extensions.DependencyInjection; + +namespace GitVersion.Git; + +public class ManagedCoreModule : IGitVersionModule +{ + public IServiceCollection RegisterTypes(IServiceCollection services) + { + GitBackendSelector.Resolve(); + services.AddSingleton(); + services.AddSingleton(); + return services.AddSingleton(); + } +} diff --git a/new-cli/GitVersion.Core.Tester/GitVersion.Core.Tester.csproj b/new-cli/GitVersion.Core.Tester/GitVersion.Core.Tester.csproj index cf6cd1370c..8f8f0e79bd 100644 --- a/new-cli/GitVersion.Core.Tester/GitVersion.Core.Tester.csproj +++ b/new-cli/GitVersion.Core.Tester/GitVersion.Core.Tester.csproj @@ -6,7 +6,7 @@ - + diff --git a/new-cli/GitVersion.Core.Tester/Program.cs b/new-cli/GitVersion.Core.Tester/Program.cs index 5d802dd15c..0cb9b914d4 100644 --- a/new-cli/GitVersion.Core.Tester/Program.cs +++ b/new-cli/GitVersion.Core.Tester/Program.cs @@ -7,7 +7,7 @@ var modules = new IGitVersionModule[] { new CoreModule(), - new LibGit2SharpCoreModule(), + new ManagedCoreModule(), }; var cts = new CancellationTokenSource(); diff --git a/new-cli/GitVersion.slnx b/new-cli/GitVersion.slnx index 5fad617683..a9ee708b21 100644 --- a/new-cli/GitVersion.slnx +++ b/new-cli/GitVersion.slnx @@ -31,7 +31,7 @@ - + - \ No newline at end of file + diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index eb56c866ee..724ae22d06 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -11,7 +11,6 @@ - diff --git a/src/GitVersion.App.Tests/CliSchemaTests.cs b/src/GitVersion.App.Tests/CliSchemaTests.cs index 3f0303d51a..ca71439dc5 100644 --- a/src/GitVersion.App.Tests/CliSchemaTests.cs +++ b/src/GitVersion.App.Tests/CliSchemaTests.cs @@ -122,8 +122,6 @@ public async Task HostSkipsExecutorsAndFeatureLogging() CliSchemaExporterTests.ValidateSchema(result.Output!); } - [TestCase("v6", "libgit2")] - [TestCase("v7", "libgit2")] [TestCase("v6", "managed")] [TestCase("v7", "managed")] public void ProcessExportsSameDocumentWithoutRepositoryOrFiles(string configuration, string backend) diff --git a/src/GitVersion.App.Tests/ConfigurationQueryIntegrationTests.cs b/src/GitVersion.App.Tests/ConfigurationQueryIntegrationTests.cs index bcb7df3a81..ffddf5dced 100644 --- a/src/GitVersion.App.Tests/ConfigurationQueryIntegrationTests.cs +++ b/src/GitVersion.App.Tests/ConfigurationQueryIntegrationTests.cs @@ -34,7 +34,7 @@ public void QueryUsesDefaultAndWorkflowConfiguration(string? workflow) [Test] [Combinatorial] public void QueryAndDisplayHonorFileThenRuntimeOverrides( - [Values("managed", "libgit2")] string backend, + [Values("managed")] string backend, [Values(false, true)] bool logToFile) { using var fixture = new EmptyRepositoryFixture(); diff --git a/src/GitVersion.App.Tests/ConfigurationValidationIntegrationTests.cs b/src/GitVersion.App.Tests/ConfigurationValidationIntegrationTests.cs index 5dd85fbda6..232ffe2743 100644 --- a/src/GitVersion.App.Tests/ConfigurationValidationIntegrationTests.cs +++ b/src/GitVersion.App.Tests/ConfigurationValidationIntegrationTests.cs @@ -252,9 +252,7 @@ public void ValidationIsUnavailableWithLegacyArgumentParser() } [TestCase("managed", "{}", true)] - [TestCase("libgit2", "{}", true)] [TestCase("managed", "calculation: [", false)] - [TestCase("libgit2", "calculation: [", false)] public async Task ExplicitValidationNeverConstructsGitOrCalculationServices(string backend, string content, bool valid) { Write(content); diff --git a/src/GitVersion.App.Tests/ExecFeatureSelectorTests.cs b/src/GitVersion.App.Tests/ExecFeatureSelectorTests.cs index 23a030d8fe..a8f5b13c0a 100644 --- a/src/GitVersion.App.Tests/ExecFeatureSelectorTests.cs +++ b/src/GitVersion.App.Tests/ExecFeatureSelectorTests.cs @@ -9,7 +9,7 @@ public class ExecFeatureSelectorTests public void SelectorCombinationsCalculateAndLogWithoutPollutingOutput( [Values("v6", "v7")] string parser, [Values("v6", "v7")] string configuration, - [Values("libgit2", "managed")] string backend, + [Values("managed")] string backend, [Values("json", "variable", "config")] string output, [Values(false, true)] bool buildServer) { @@ -85,7 +85,9 @@ public void DefaultsSelectV7AndManagedWithConsoleLogsOnStderr(string? value) [TestCase("GITVERSION_ARGUMENT_PARSER_VERSION", "true", "'v6' and 'v7'")] [TestCase("GITVERSION_CONFIGURATION_VERSION", "v8", "'v6' and 'v7'")] - [TestCase("GITVERSION_GIT_BACKEND", "manged", "'libgit2' and 'managed'")] + [TestCase("GITVERSION_GIT_BACKEND", "manged", "'managed'")] + [TestCase("GITVERSION_GIT_BACKEND", "libgit2", "removed in v7.1")] + [TestCase("GITVERSION_GIT_BACKEND", " LiBgIt2 ", "removed in v7.1")] [TestCase("GITVERSION_USE_V6_ARGUMENT_PARSER", "true", "GITVERSION_ARGUMENT_PARSER_VERSION=v6")] [TestCase("GITVERSION_USE_V6_ARGUMENT_PARSER", "false", "GITVERSION_ARGUMENT_PARSER_VERSION=v6")] [TestCase("GITVERSION_USE_V6_ARGUMENT_PARSER", "", "GITVERSION_ARGUMENT_PARSER_VERSION=v6")] @@ -108,7 +110,7 @@ public void InvalidSelectorsFailBeforeHelpWithActionableStderr(string variable, [Combinatorial] public void MigrationRetainsYamlStdoutWithEitherConfigurationAndBackend( [Values("v6", "v7")] string configuration, - [Values("libgit2", "managed")] string backend) + [Values("managed")] string backend) { var directory = Directory.CreateTempSubdirectory(); try diff --git a/src/GitVersion.App.Tests/FeatureSelectorTests.cs b/src/GitVersion.App.Tests/FeatureSelectorTests.cs index 2f833b6ad8..cba51fd958 100644 --- a/src/GitVersion.App.Tests/FeatureSelectorTests.cs +++ b/src/GitVersion.App.Tests/FeatureSelectorTests.cs @@ -52,7 +52,7 @@ public void RetiredBooleanIsRejectedEvenWithNewSelector(string value) public void CompositionRegistersSelectedImplementations( [Values("v6", "v7")] string parser, [Values("v6", "v7")] string configuration, - [Values("libgit2", "managed")] string backend) + [Values("managed")] string backend) { using var scope = new SelectorScope(parser, configuration, backend); using var fixture = new EmptyRepositoryFixture(); @@ -62,14 +62,14 @@ public void CompositionRegistersSelectedImplementations( host.Services.GetRequiredService().GetType() .ShouldBe(parser == "v6" ? typeof(LegacyArgumentParser) : typeof(ArgumentParser)); host.Services.GetRequiredService().GetType().Assembly - .ShouldBe(backend == "libgit2" ? typeof(GitVersionLibGit2SharpModule).Assembly : typeof(GitVersionManagedGitModule).Assembly); + .ShouldBe(typeof(GitVersionManagedGitModule).Assembly); } [Test] public void SelectionLogUsesCapturedValues() { FeatureSelections selections; - using (new SelectorScope("v6", "v6", "libgit2")) + using (new SelectorScope("v6", "v6", " managed ")) { selections = FeatureSelections.Resolve(); } @@ -78,7 +78,7 @@ public void SelectionLogUsesCapturedValues() var messages = new List(); selections.Log(new TestLogger(messages.Add)); - messages.ShouldBe(new[] { "Argument parser version: v6; Configuration version: v6; Git backend: libgit2" }); + messages.ShouldBe(new[] { "Argument parser version: v6; Configuration version: v6; Git backend: managed" }); } private sealed class SelectorScope : IDisposable diff --git a/src/GitVersion.App.Tests/PullRequestInBuildAgentTest.cs b/src/GitVersion.App.Tests/PullRequestInBuildAgentTest.cs index ee9dcc623b..0472dcc589 100644 --- a/src/GitVersion.App.Tests/PullRequestInBuildAgentTest.cs +++ b/src/GitVersion.App.Tests/PullRequestInBuildAgentTest.cs @@ -4,8 +4,8 @@ using GitVersion.Helpers; using GitVersion.Output; using GitVersion.Testing.Extensions; +using GitVersion.Testing.Git; using GitVersion.Tests; -using LibGit2Sharp; namespace GitVersion.App.Tests; diff --git a/src/GitVersion.App.Tests/TagCheckoutInBuildAgentTests.cs b/src/GitVersion.App.Tests/TagCheckoutInBuildAgentTests.cs index 1264ca48b0..9057eab3b0 100644 --- a/src/GitVersion.App.Tests/TagCheckoutInBuildAgentTests.cs +++ b/src/GitVersion.App.Tests/TagCheckoutInBuildAgentTests.cs @@ -1,8 +1,8 @@ using GitVersion.Agents; using GitVersion.Helpers; using GitVersion.Testing.Extensions; +using GitVersion.Testing.Git; using GitVersion.Tests; -using LibGit2Sharp; namespace GitVersion.App.Tests; diff --git a/src/GitVersion.App/CliHost.cs b/src/GitVersion.App/CliHost.cs index 7a9655b434..8ecb188167 100644 --- a/src/GitVersion.App/CliHost.cs +++ b/src/GitVersion.App/CliHost.cs @@ -36,9 +36,7 @@ private static void RegisterGitVersionModules(IServiceCollection services, strin services.AddModule(new GitVersionConfigurationModule()); services.AddModule(new GitVersionOutputModule()); - services.AddModule(selections.GitBackend == GitBackend.Managed - ? new GitVersionManagedGitModule() - : new GitVersionLibGit2SharpModule()); + services.AddModule(new GitVersionManagedGitModule()); services.AddModule(new GitVersionAppModule(args, selections.ArgumentParser == ArgumentParserVersion.V6)); } diff --git a/src/GitVersion.App/FeatureSelections.cs b/src/GitVersion.App/FeatureSelections.cs index 631d7072e5..50244754d6 100644 --- a/src/GitVersion.App/FeatureSelections.cs +++ b/src/GitVersion.App/FeatureSelections.cs @@ -13,5 +13,5 @@ public void Log(ILogger logger) => "Argument parser version: {ArgumentParserVersion}; Configuration version: {ConfigurationVersion}; Git backend: {GitBackend}", ArgumentParser == ArgumentParserVersion.V6 ? "v6" : "v7", Configuration == ConfigurationVersion.V6 ? "v6" : "v7", - GitBackend == GitBackend.Managed ? "managed" : "libgit2"); + "managed"); } diff --git a/src/GitVersion.App/GitVersion.App.csproj b/src/GitVersion.App/GitVersion.App.csproj index 055cef0310..42b17864c2 100644 --- a/src/GitVersion.App/GitVersion.App.csproj +++ b/src/GitVersion.App/GitVersion.App.csproj @@ -30,7 +30,6 @@ - @@ -59,4 +58,9 @@ Condition="'%(TfmSpecificPackageFile.Filename)%(TfmSpecificPackageFile.Extension)' == 'DotnetToolSettings.xml' and '%(TfmSpecificPackageFile.PackagePath)' == 'tools/any/any/'" /> + + + + diff --git a/src/GitVersion.Configuration/GitVersion.Configuration.csproj b/src/GitVersion.Configuration/GitVersion.Configuration.csproj index 79f5abf2d5..25bde181b0 100644 --- a/src/GitVersion.Configuration/GitVersion.Configuration.csproj +++ b/src/GitVersion.Configuration/GitVersion.Configuration.csproj @@ -16,7 +16,6 @@ - diff --git a/src/GitVersion.Core.Tests/Core/DualBackendParityTests.cs b/src/GitVersion.Core.Tests/Core/DualBackendParityTests.cs deleted file mode 100644 index 14fbf16226..0000000000 --- a/src/GitVersion.Core.Tests/Core/DualBackendParityTests.cs +++ /dev/null @@ -1,551 +0,0 @@ -using System.IO.Abstractions; -using GitVersion.Git; -using GitVersion.Helpers; -using GitVersion.Testing.Extensions; - -namespace GitVersion.Tests; - -/// -/// Opens the same on-disk repository with both Git backends (libgit2 and managed) and -/// deep-compares every read surface: reference/branch/tag enumeration, order-sensitive -/// commit walks, merge bases over all commit pairs, diff paths and the uncommitted -/// changes count. This is the direct parity oracle demanded by the managed-git -/// migration plan, independent of which backend the test run selects globally. -/// -[TestFixture] -public class DualBackendParityTests : TestBase -{ - private static readonly CommitSortStrategies[] SortCombinations = - [ - CommitSortStrategies.None, - CommitSortStrategies.Time, - CommitSortStrategies.Topological, - CommitSortStrategies.Topological | CommitSortStrategies.Time, - CommitSortStrategies.Time | CommitSortStrategies.Reverse, - CommitSortStrategies.Topological | CommitSortStrategies.Time | CommitSortStrategies.Reverse - ]; - - [Test] - public void ReferencesAreIdenticalOnBothBackends() - { - using var fixture = CreateComplexFixture(); - using var backends = OpenBothBackends(fixture); - - static IEnumerable Describe(IGitRepository repository) => - repository.References.Select(r => $"{r.Name.Canonical} -> {r.TargetIdentifier} ({r.ReferenceTargetId?.Sha ?? "symbolic"})"); - - Describe(backends.Managed).ShouldBe(Describe(backends.LibGit2)); - - var libGit2Head = backends.LibGit2.References.Head.ShouldNotBeNull(); - var managedHead = backends.Managed.References.Head.ShouldNotBeNull(); - managedHead.Name.Canonical.ShouldBe(libGit2Head.Name.Canonical); - managedHead.TargetIdentifier.ShouldBe(libGit2Head.TargetIdentifier); - - static IEnumerable FromGlob(IGitRepository repository) => - repository.References.FromGlob("refs/heads/*").Select(r => $"{r.Name.Canonical} -> {r.TargetIdentifier}"); - - FromGlob(backends.Managed).ShouldBe(FromGlob(backends.LibGit2)); - } - - [Test] - public void BranchesAreIdenticalOnBothBackends() - { - using var fixture = CreateComplexFixture(); - using var backends = OpenBothBackends(fixture); - - static IEnumerable Describe(IGitRepository repository) => - repository.Branches.Select(b => $"{b.Name.Canonical}@{b.Tip?.Sha} remote:{b.IsRemote} tracking:{b.IsTracking} detached:{b.IsDetachedHead}"); - - Describe(backends.Managed).ShouldBe(Describe(backends.LibGit2)); - - foreach (var name in new[] { "main", "develop", "refs/heads/develop", "feature/complex", "missing" }) - { - var libGit2Branch = backends.LibGit2.Branches[name]; - var managedBranch = backends.Managed.Branches[name]; - (managedBranch?.Name.Canonical).ShouldBe(libGit2Branch?.Name.Canonical, $"lookup of '{name}'"); - (managedBranch?.Tip?.Sha).ShouldBe(libGit2Branch?.Tip?.Sha, $"tip of '{name}'"); - } - - backends.Managed.Head.Name.Canonical.ShouldBe(backends.LibGit2.Head.Name.Canonical); - (backends.Managed.Head.Tip?.Sha).ShouldBe(backends.LibGit2.Head.Tip?.Sha); - backends.Managed.IsHeadDetached.ShouldBe(backends.LibGit2.IsHeadDetached); - backends.Managed.IsShallow.ShouldBe(backends.LibGit2.IsShallow); - } - - [Test] - public void TagsAreIdenticalOnBothBackends() - { - using var fixture = CreateComplexFixture(); - using var backends = OpenBothBackends(fixture); - - static IEnumerable Describe(IGitRepository repository) => - repository.Tags.Select(t => $"{t.Name.Canonical} target:{t.TargetSha} commit:{t.Commit.Sha}"); - - Describe(backends.Managed).ShouldBe(Describe(backends.LibGit2)); - } - - [Test] - public void CommitLogsAreIdenticalOnBothBackends() - { - using var fixture = CreateComplexFixture(); - using var backends = OpenBothBackends(fixture); - - static IEnumerable Describe(IGitRepository repository) => - repository.Commits.Select(c => $"{c.Sha} when:{c.When:O} merge:{c.IsMergeCommit} parents:{string.Join(',', c.Parents.Select(p => p.Sha))} message:{c.Message}"); - - Describe(backends.Managed).ShouldBe(Describe(backends.LibGit2)); - - foreach (var branchName in new[] { "main", "develop", "feature/complex" }) - { - var libGit2Commits = backends.LibGit2.Branches[branchName].ShouldNotBeNull().Commits.Select(c => c.Sha); - var managedCommits = backends.Managed.Branches[branchName].ShouldNotBeNull().Commits.Select(c => c.Sha); - managedCommits.ShouldBe(libGit2Commits, $"commits of '{branchName}'"); - } - } - - [Test] - public void QueryBySequencesAreIdenticalOnBothBackends() - { - using var fixture = CreateCrissCrossFixture(); - using var backends = OpenBothBackends(fixture); - - var libGit2Commits = CollectAllCommits(backends.LibGit2); - var managedCommits = CollectAllCommits(backends.Managed); - managedCommits.Keys.Order().ShouldBe(libGit2Commits.Keys.Order()); - - string?[] excludeCandidates = [null, .. libGit2Commits.Keys.Order().Take(3)]; - - foreach (var sortBy in SortCombinations) - { - foreach (var firstParentOnly in new[] { false, true }) - { - foreach (var excludeSha in excludeCandidates) - { - var description = $"sort:{sortBy} firstParent:{firstParentOnly} exclude:{excludeSha ?? "none"}"; - - var libGit2Result = backends.LibGit2.Commits.QueryBy(new CommitFilter - { - IncludeReachableFrom = backends.LibGit2.Branches["main"], - ExcludeReachableFrom = excludeSha is null ? null : libGit2Commits[excludeSha], - SortBy = sortBy, - FirstParentOnly = firstParentOnly - }).Select(c => c.Sha); - - var managedResult = backends.Managed.Commits.QueryBy(new CommitFilter - { - IncludeReachableFrom = backends.Managed.Branches["main"], - ExcludeReachableFrom = excludeSha is null ? null : managedCommits[excludeSha], - SortBy = sortBy, - FirstParentOnly = firstParentOnly - }).Select(c => c.Sha); - - managedResult.ShouldBe(libGit2Result, description); - } - } - } - } - - [Test] - public void MergeBasesAreIdenticalOnBothBackendsForAllCommitPairs() - { - using var fixture = CreateCrissCrossFixture(); - using var backends = OpenBothBackends(fixture); - - var libGit2Commits = CollectAllCommits(backends.LibGit2); - var managedCommits = CollectAllCommits(backends.Managed); - var shas = libGit2Commits.Keys.Order().ToList(); - - foreach (var first in shas) - { - foreach (var second in shas) - { - var libGit2MergeBase = backends.LibGit2.FindMergeBase(libGit2Commits[first], libGit2Commits[second]); - var managedMergeBase = backends.Managed.FindMergeBase(managedCommits[first], managedCommits[second]); - (managedMergeBase?.Sha).ShouldBe(libGit2MergeBase?.Sha, $"merge base of {first[..7]} and {second[..7]}"); - } - } - } - - [Test] - public void DiffPathsAreIdenticalOnBothBackendsForAllCommits() - { - using var fixture = CreateComplexFixture(); - using var backends = OpenBothBackends(fixture); - - var libGit2Commits = CollectAllCommits(backends.LibGit2); - var managedCommits = CollectAllCommits(backends.Managed); - - foreach (var sha in libGit2Commits.Keys.Order()) - { - managedCommits[sha].DiffPaths.ShouldBe(libGit2Commits[sha].DiffPaths, $"diff paths of {sha[..7]}"); - } - } - - [Test] - public void UncommittedChangesCountIsIdenticalOnBothBackends() - { - using var fixture = new EmptyRepositoryFixture(); - var signature = new LibGit2Sharp.Signature("unit test", "test@example.com", DateTimeOffset.Now); - - var trackedFile = FileSystemHelper.Path.Combine(fixture.RepositoryPath, "tracked.txt"); - File.WriteAllText(trackedFile, "one"); - LibGit2Sharp.Commands.Stage(fixture.Repository, "tracked.txt"); - fixture.Repository.Commit("add tracked file", signature, signature, new LibGit2Sharp.CommitOptions()); - - File.WriteAllText(trackedFile, "two"); - File.WriteAllText(FileSystemHelper.Path.Combine(fixture.RepositoryPath, "untracked.txt"), "new"); - File.WriteAllText(FileSystemHelper.Path.Combine(fixture.RepositoryPath, "staged.txt"), "staged"); - LibGit2Sharp.Commands.Stage(fixture.Repository, "staged.txt"); - - using var backends = OpenBothBackends(fixture); - backends.Managed.UncommittedChangesCount().ShouldBe(backends.LibGit2.UncommittedChangesCount()); - } - - [Test] - public void EmptyRepositoryBehavesIdenticallyOnBothBackends() - { - using var fixture = new EmptyRepositoryFixture(); - File.WriteAllText(FileSystemHelper.Path.Combine(fixture.RepositoryPath, "untracked.txt"), "new"); - - using var backends = OpenBothBackends(fixture); - - backends.Managed.Head.Name.Canonical.ShouldBe(backends.LibGit2.Head.Name.Canonical); - backends.Managed.Head.Tip.ShouldBe(backends.LibGit2.Head.Tip); - backends.Managed.IsHeadDetached.ShouldBe(backends.LibGit2.IsHeadDetached); - backends.Managed.UncommittedChangesCount().ShouldBe(backends.LibGit2.UncommittedChangesCount()); - } - - [Test] - public void DetachedHeadBehavesIdenticallyOnBothBackends() - { - using var fixture = CreateComplexFixture(); - var detachAt = fixture.Repository.Head.Tip.Parents.First(); - LibGit2Sharp.Commands.Checkout(fixture.Repository, detachAt); - - using var backends = OpenBothBackends(fixture); - - backends.Managed.IsHeadDetached.ShouldBeTrue(); - backends.Managed.IsHeadDetached.ShouldBe(backends.LibGit2.IsHeadDetached); - backends.Managed.Head.Name.Canonical.ShouldBe(backends.LibGit2.Head.Name.Canonical); - backends.Managed.Head.IsDetachedHead.ShouldBe(backends.LibGit2.Head.IsDetachedHead); - (backends.Managed.Head.Tip?.Sha).ShouldBe(backends.LibGit2.Head.Tip?.Sha); - } - - [Test] - public void RemotesAndRemoteBranchesAreIdenticalOnBothBackends() - { - using var fixture = new RemoteRepositoryFixture(); - using var backends = OpenBothBackends(fixture.LocalRepositoryFixture); - - static IEnumerable DescribeRemotes(IGitRepository repository) => - repository.Remotes.Select(r => - $"{r.Name} {r.Url} fetch:[{string.Join(';', r.FetchRefSpecs.Select(s => s.Specification))}] push:[{string.Join(';', r.PushRefSpecs.Select(s => s.Specification))}]"); - - DescribeRemotes(backends.Managed).ShouldBe(DescribeRemotes(backends.LibGit2)); - - var libGit2Remote = backends.LibGit2.Remotes["origin"].ShouldNotBeNull(); - var managedRemote = backends.Managed.Remotes["origin"].ShouldNotBeNull(); - managedRemote.Url.ShouldBe(libGit2Remote.Url); - - foreach (var (managedSpec, libGit2Spec) in managedRemote.FetchRefSpecs.Zip(libGit2Remote.FetchRefSpecs)) - { - managedSpec.Specification.ShouldBe(libGit2Spec.Specification); - managedSpec.Direction.ShouldBe(libGit2Spec.Direction); - managedSpec.Source.ShouldBe(libGit2Spec.Source); - managedSpec.Destination.ShouldBe(libGit2Spec.Destination); - } - - static IEnumerable DescribeBranches(IGitRepository repository) => - repository.Branches.Select(b => $"{b.Name.Canonical}@{b.Tip?.Sha} remote:{b.IsRemote} tracking:{b.IsTracking}"); - - DescribeBranches(backends.Managed).ShouldBe(DescribeBranches(backends.LibGit2)); - } - - [Test] - public void RepositoryPathsAreIdenticalOnBothBackends() - { - using var fixture = CreateComplexFixture(); - using var backends = OpenBothBackends(fixture); - - backends.Managed.Path.ShouldBe(backends.LibGit2.Path); - backends.Managed.WorkingDirectory.ShouldBe(backends.LibGit2.WorkingDirectory); - } - - [Test] - public void BareRepositoryHasNullProjectRootDirectoryOnBothBackends() - { - var barePath = FileSystemHelper.Path.GetRepositoryTempPath(); - LibGit2Sharp.Repository.Init(barePath, isBare: true); - try - { - var fileSystem = new FileSystem(); - var options = Options.Create(new GitVersionOptions { WorkingDirectory = barePath }); - - IGitRepositoryInfo libGit2Info = new GitRepositoryInfo(fileSystem, options); - IGitRepositoryInfo managedInfo = new ManagedGitRepositoryInfo(fileSystem, options); - - // Discovery succeeds for a bare repository, but there is no working directory: - // the project root is legitimately null and resolving it must not throw. - libGit2Info.ProjectRootDirectory.ShouldBeNull(); - managedInfo.ProjectRootDirectory.ShouldBeNull(); - - managedInfo.DotGitDirectory.ShouldNotBeNull(); - managedInfo.DotGitDirectory.ShouldBe(libGit2Info.DotGitDirectory); - } - finally - { - FileSystemHelper.Directory.DeleteDirectory(barePath); - } - } - - [Test] - public void OctopusMergeBehavesIdenticallyOnBothBackends() - { - using var fixture = new EmptyRepositoryFixture(); - fixture.MakeACommit("base"); - fixture.BranchTo("topic/one"); - fixture.MakeACommit("topic one"); - fixture.Checkout("main"); - fixture.BranchTo("topic/two"); - fixture.MakeACommit("topic two"); - fixture.Checkout("main"); - fixture.MakeACommit("main one"); - GitTestExtensions.ExecuteGitCmd($"-C {fixture.RepositoryPath} -c user.name=test -c user.email=test@example.com merge topic/one topic/two -m octopus", "."); - - using var backends = OpenBothBackends(fixture); - - backends.Managed.Head.Tip.ShouldNotBeNull().Parents.Count.ShouldBe(3); - AssertCommitLogParity(backends); - AssertMergeBaseParity(backends); - AssertDiffPathsParity(backends); - } - - [Test] - public void PackedAndLooseReferencesBehaveIdenticallyOnBothBackends() - { - using var fixture = CreateComplexFixture(); - - // Pack everything, then shadow one packed ref with a loose one and add brand-new loose refs, - // so both stores must merge packed and loose entries (and their peeled annotated-tag lines). - GitTestExtensions.ExecuteGitCmd($"-C {fixture.RepositoryPath} pack-refs --all", "."); - fixture.MakeACommit("shadows the packed main entry"); - fixture.BranchTo("loose/branch"); - fixture.ApplyTag("loose-tag"); - - using var backends = OpenBothBackends(fixture); - - static IEnumerable DescribeReferences(IGitRepository repository) => - repository.References.Select(r => $"{r.Name.Canonical} -> {r.TargetIdentifier}"); - - DescribeReferences(backends.Managed).ShouldBe(DescribeReferences(backends.LibGit2)); - - static IEnumerable DescribeTags(IGitRepository repository) => - repository.Tags.Select(t => $"{t.Name.Canonical} target:{t.TargetSha} commit:{t.Commit.Sha}"); - - DescribeTags(backends.Managed).ShouldBe(DescribeTags(backends.LibGit2)); - - static IEnumerable DescribeBranches(IGitRepository repository) => - repository.Branches.Select(b => $"{b.Name.Canonical}@{b.Tip?.Sha}"); - - DescribeBranches(backends.Managed).ShouldBe(DescribeBranches(backends.LibGit2)); - } - - [Test] - public void LightweightAndAnnotatedTagOnTheSameCommitBehaveIdenticallyOnBothBackends() - { - using var fixture = new EmptyRepositoryFixture(); - fixture.MakeACommit("only commit"); - fixture.ApplyTag("lightweight"); - fixture.Repository.Tags.Add("annotated", fixture.Repository.Head.Tip, new LibGit2Sharp.Signature("unit test", "test@example.com", DateTimeOffset.Now), "the same commit, annotated"); - - using var backends = OpenBothBackends(fixture); - - static IEnumerable Describe(IGitRepository repository) => - repository.Tags.Select(t => $"{t.Name.Canonical} target:{t.TargetSha} commit:{t.Commit.Sha}"); - - Describe(backends.Managed).ShouldBe(Describe(backends.LibGit2)); - } - - [Test] - public void ShallowCloneBehavesIdenticallyOnBothBackends() - { - using var fixture = new RemoteRepositoryFixture(); - using var localFixture = fixture.CloneRepository(); - localFixture.MakeShallow(); - - using var backends = OpenBothBackends(localFixture); - - backends.Managed.IsShallow.ShouldBeTrue(); - backends.Managed.IsShallow.ShouldBe(backends.LibGit2.IsShallow); - AssertCommitLogParity(backends); - } - - [Test] - public void WorktreeBehavesIdenticallyOnBothBackends() - { - using var fixture = CreateComplexFixture(); - var worktreePath = FileSystemHelper.Path.GetRepositoryTempPath(); - GitTestExtensions.ExecuteGitCmd($"-C {fixture.RepositoryPath} worktree add {worktreePath} develop", "."); - - try - { - using var backends = OpenBothBackendsAt(worktreePath); - - backends.Managed.Path.ShouldBe(backends.LibGit2.Path); - backends.Managed.WorkingDirectory.ShouldBe(backends.LibGit2.WorkingDirectory); - backends.Managed.Head.Name.Canonical.ShouldBe(backends.LibGit2.Head.Name.Canonical); - (backends.Managed.Head.Tip?.Sha).ShouldBe(backends.LibGit2.Head.Tip?.Sha); - backends.Managed.UncommittedChangesCount().ShouldBe(backends.LibGit2.UncommittedChangesCount()); - - static IEnumerable DescribeReferences(IGitRepository repository) => - repository.References.Select(r => $"{r.Name.Canonical} -> {r.TargetIdentifier}"); - - DescribeReferences(backends.Managed).ShouldBe(DescribeReferences(backends.LibGit2)); - AssertCommitLogParity(backends); - } - finally - { - GitTestExtensions.ExecuteGitCmd($"-C {fixture.RepositoryPath} worktree remove --force {worktreePath}", "."); - } - } - - [Test] - public void IndexVersionFourBehavesIdenticallyOnBothBackends() - { - using var fixture = new EmptyRepositoryFixture(); - var signature = new LibGit2Sharp.Signature("unit test", "test@example.com", DateTimeOffset.Now); - - var trackedFile = FileSystemHelper.Path.Combine(fixture.RepositoryPath, "tracked.txt"); - File.WriteAllText(trackedFile, "one"); - LibGit2Sharp.Commands.Stage(fixture.Repository, "tracked.txt"); - fixture.Repository.Commit("add tracked file", signature, signature, new LibGit2Sharp.CommitOptions()); - - GitTestExtensions.ExecuteGitCmd($"-C {fixture.RepositoryPath} update-index --index-version 4", "."); - File.WriteAllText(trackedFile, "two"); - File.WriteAllText(FileSystemHelper.Path.Combine(fixture.RepositoryPath, "untracked.txt"), "new"); - - using var backends = OpenBothBackends(fixture); - backends.Managed.UncommittedChangesCount().ShouldBe(backends.LibGit2.UncommittedChangesCount()); - } - - private static void AssertCommitLogParity(BackendPair backends) - { - static IEnumerable Describe(IGitRepository repository) => - repository.Commits.Select(c => $"{c.Sha} merge:{c.IsMergeCommit} parents:{string.Join(',', c.Parents.Select(p => p.Sha))}"); - - Describe(backends.Managed).ShouldBe(Describe(backends.LibGit2)); - } - - private static void AssertMergeBaseParity(BackendPair backends) - { - var libGit2Commits = CollectAllCommits(backends.LibGit2); - var managedCommits = CollectAllCommits(backends.Managed); - - foreach (var first in libGit2Commits.Keys.Order()) - { - foreach (var second in libGit2Commits.Keys.Order()) - { - var libGit2MergeBase = backends.LibGit2.FindMergeBase(libGit2Commits[first], libGit2Commits[second]); - var managedMergeBase = backends.Managed.FindMergeBase(managedCommits[first], managedCommits[second]); - (managedMergeBase?.Sha).ShouldBe(libGit2MergeBase?.Sha, $"merge base of {first[..7]} and {second[..7]}"); - } - } - } - - private static void AssertDiffPathsParity(BackendPair backends) - { - var libGit2Commits = CollectAllCommits(backends.LibGit2); - var managedCommits = CollectAllCommits(backends.Managed); - - foreach (var sha in libGit2Commits.Keys.Order()) - { - managedCommits[sha].DiffPaths.ShouldBe(libGit2Commits[sha].DiffPaths, $"diff paths of {sha[..7]}"); - } - } - - private static RepositoryFixtureBase CreateComplexFixture() - { - var fixture = new EmptyRepositoryFixture(); - fixture.MakeACommit("initial commit"); - fixture.ApplyTag("1.0.0"); - fixture.BranchTo("develop"); - fixture.MakeACommit("develop one"); - fixture.Checkout("main"); - fixture.MakeACommit("main two"); - fixture.MergeNoFF("develop"); - fixture.Repository.Tags.Add("v2.0.0", fixture.Repository.Head.Tip, new LibGit2Sharp.Signature("unit test", "test@example.com", DateTimeOffset.Now), "an annotated tag"); - fixture.BranchTo("feature/complex"); - fixture.MakeACommit("feature one"); - fixture.Checkout("develop"); - fixture.MakeACommit("develop two"); - fixture.Checkout("main"); - return fixture; - } - - /// - /// Builds a criss-cross history (two merge commits whose parents are swapped), which is - /// the classic ambiguous merge-base scenario, plus commits with equal timestamps. - /// - private static RepositoryFixtureBase CreateCrissCrossFixture() - { - var fixture = new EmptyRepositoryFixture(); - var signature = new LibGit2Sharp.Signature("unit test", "test@example.com", DateTimeOffset.Now); - var mergeOptions = new LibGit2Sharp.MergeOptions { FastForwardStrategy = LibGit2Sharp.FastForwardStrategy.NoFastForward }; - - fixture.MakeACommit("base"); - fixture.BranchTo("develop"); - fixture.MakeACommit("develop one"); - var developHead = fixture.Repository.Head.Tip; - fixture.Checkout("main"); - fixture.MakeACommit("main one"); - var mainHead = fixture.Repository.Head.Tip; - - // main merges develop's old head while develop merges main's old head: criss-cross. - fixture.Repository.Merge(developHead, signature, mergeOptions); - fixture.Checkout("develop"); - fixture.Repository.Merge(mainHead, signature, mergeOptions); - - fixture.MakeACommit("develop two"); - fixture.Checkout("main"); - fixture.MakeACommit("main two"); - return fixture; - } - - private static BackendPair OpenBothBackends(RepositoryFixtureBase fixture) => OpenBothBackendsAt(fixture.RepositoryPath); - - private static BackendPair OpenBothBackendsAt(string repositoryPath) - { - var libGit2 = new GitRepository(NullLogger.Instance); - libGit2.DiscoverRepository(repositoryPath); - - var cliMutator = new GitCliMutator(NullLogger.Instance, new GitCliExecutor(NullLogger.Instance)); - var managed = new ManagedGitRepository(NullLogger.Instance, cliMutator); - managed.DiscoverRepository(repositoryPath); - - return new(libGit2, managed); - } - - private static Dictionary CollectAllCommits(IGitRepository repository) - { - var commits = new Dictionary(StringComparer.Ordinal); - - foreach (var branch in repository.Branches) - { - foreach (var commit in branch.Commits) - { - commits[commit.Sha] = commit; - } - } - - return commits; - } - - private sealed record BackendPair(IGitRepository LibGit2, IGitRepository Managed) : IDisposable - { - public void Dispose() - { - LibGit2.Dispose(); - Managed.Dispose(); - } - } -} diff --git a/src/GitVersion.Core.Tests/Core/GitBackendSelectorTests.cs b/src/GitVersion.Core.Tests/Core/GitBackendSelectorTests.cs index e8f6d62344..3df187d13c 100644 --- a/src/GitVersion.Core.Tests/Core/GitBackendSelectorTests.cs +++ b/src/GitVersion.Core.Tests/Core/GitBackendSelectorTests.cs @@ -6,20 +6,17 @@ namespace GitVersion.Tests; [NonParallelizable] public class GitBackendSelectorTests : TestBase { - [TestCase(null, true)] - [TestCase("", true)] - [TestCase(" \t ", true)] - [TestCase("libgit2", false)] - [TestCase("LIBGIT2", false)] - [TestCase(" LIBGIT2 ", false)] - [TestCase("managed", true)] - [TestCase("Managed", true)] - [TestCase(" managed ", true)] - public void ResolvesKnownValues(string? value, bool isManaged) + [TestCase(null)] + [TestCase("")] + [TestCase(" \t ")] + [TestCase("managed")] + [TestCase("Managed")] + [TestCase(" managed ")] + public void ResolvesKnownValues(string? value) { using var scope = new EnvironmentVariableScope(value); - GitBackendSelector.Resolve().ShouldBe(isManaged ? GitBackend.Managed : GitBackend.LibGit2); + GitBackendSelector.Resolve().ShouldBe(GitBackend.Managed); } [TestCase("manged")] @@ -32,7 +29,19 @@ public void FailsFastOnUnknownValues(string value) var exception = Should.Throw(() => GitBackendSelector.Resolve()); exception.Message.ShouldContain(value); exception.Message.ShouldContain(GitBackendSelector.EnvironmentVariableName); - exception.Message.ShouldContain("'libgit2' and 'managed'"); + exception.Message.ShouldContain("'managed'"); + } + + [TestCase("libgit2")] + [TestCase("LIBGIT2")] + [TestCase(" libgit2 ")] + public void RemovedBackendHasActionableError(string value) + { + using var scope = new EnvironmentVariableScope(value); + var exception = Should.Throw(() => GitBackendSelector.Resolve()); + exception.Message.ShouldContain("removed in v7.1"); + exception.Message.ShouldContain("managed"); + exception.Message.ShouldContain(GitBackendSelector.EnvironmentVariableName); } private sealed class EnvironmentVariableScope : IDisposable diff --git a/src/GitVersion.Core.Tests/Core/GitCliFixtureTests.cs b/src/GitVersion.Core.Tests/Core/GitCliFixtureTests.cs new file mode 100644 index 0000000000..356cc5dfda --- /dev/null +++ b/src/GitVersion.Core.Tests/Core/GitCliFixtureTests.cs @@ -0,0 +1,112 @@ +using GitVersion.Testing.Extensions; +using GitVersion.Testing.Git; + +namespace GitVersion.Tests; + +[TestFixture] +public class GitCliFixtureTests +{ + [Test] + public void LiveManagedReaderSeesGitReset() + { + using var fixture = new EmptyRepositoryFixture(); + var first = fixture.Repository.MakeACommit("first"); + var second = fixture.Repository.MakeACommit("second"); + using var reader = fixture.Repository.ToGitRepository(); + reader.Head.Tip.ShouldNotBeNull().Sha.ShouldBe(second.Sha); + fixture.Repository.Run("reset", "--hard", first.Sha); + reader.Head.Tip.ShouldNotBeNull().Sha.ShouldBe(first.Sha); + } + + [Test] + public void PullUsesTheSuppliedMergeSignature() + { + using var remote = new EmptyRepositoryFixture(); + remote.MakeACommit("base"); + using var local = remote.CloneRepository(); + local.MakeACommit("local"); + remote.MakeACommit("remote"); + var signature = new Signature("Merge Author", "merge@example.com", new DateTimeOffset(2023, 6, 1, 10, 0, 0, TimeSpan.Zero)); + Commands.Pull(local.Repository, signature, new()); + local.Repository.Head.Tip.Parents.Count().ShouldBe(2); + local.Repository.Head.Tip.Author.ShouldBe(signature); + local.Repository.Head.Tip.Committer.ShouldBe(signature); + } + + [Test] + public void BlobCreationDoesNotNormalizeLineEndings() + { + using var fixture = new EmptyRepositoryFixture(); + fixture.Repository.Config.Set("core.autocrlf", true); + byte[] bytes = [97, 13, 10, 98, 13, 10]; + using var content = new MemoryStream(bytes); + var blob = fixture.Repository.ObjectDatabase.CreateBlob(content); + var expected = System.Security.Cryptography.SHA1.HashData([.. Encoding.ASCII.GetBytes("blob 6\0"), .. bytes]); + blob.Sha.ShouldBe(Convert.ToHexStringLower(expected)); + } + + [Test] + public void BlobCreationPreservesNonUtf8Bytes() + { + using var fixture = new EmptyRepositoryFixture(); + byte[] bytes = [0, 128, 255, 13, 10]; + using var content = new MemoryStream(bytes); + var blob = fixture.Repository.ObjectDatabase.CreateBlob(content); + var expected = System.Security.Cryptography.SHA1.HashData([.. Encoding.ASCII.GetBytes("blob 5\0"), .. bytes]); + blob.Sha.ShouldBe(Convert.ToHexStringLower(expected)); + } + + [Test] + public void MissingObjectsReturnNullEvenForAFullObjectId() + { + using var fixture = new EmptyRepositoryFixture(); + fixture.MakeACommit("initial"); + const string missing = "0123456789012345678901234567890123456789"; + fixture.Repository.Lookup(missing).ShouldBeNull(); + fixture.Repository.Lookup(missing).ShouldBeNull(); + } + + [Test] + public void UpdatingAReferenceToAShaKeepsItDirect() + { + using var fixture = new EmptyRepositoryFixture(); + var first = fixture.Repository.MakeACommit("first"); + fixture.Repository.CreateBranch("target"); + var second = fixture.Repository.MakeACommit("second"); + fixture.Repository.Refs.UpdateTarget(fixture.Repository.Refs["refs/heads/target"], second.Sha); + fixture.Repository.Run("rev-parse", "target").Trim().ShouldBe(second.Sha); + File.ReadAllText(Path.Combine(fixture.Repository.Info.Path, "refs", "heads", "target")).Trim().ShouldBe(second.Sha); + fixture.Repository.Head.Tip.Sha.ShouldNotBe(first.Sha); + } + + [Test] + public void FetchIncludesObjectsReachableOnlyThroughPullRequestReferences() + { + using var remote = new EmptyRepositoryFixture(); + remote.MakeACommit("base"); + Commands.Checkout(remote.Repository, remote.Repository.Head.Tip); + var pullRequest = remote.Repository.MakeACommit("PR-only commit"); + remote.Repository.Refs.Add("refs/pull/42/merge", pullRequest.Id); + remote.Checkout("main"); + using var local = new EmptyRepositoryFixture(); + local.Repository.Network.Remotes.Add("origin", remote.RepositoryPath); + Commands.Fetch(local.Repository, "origin", [], null, null); + local.Repository.Lookup(pullRequest.Sha).ShouldNotBeNull().MessageShort.ShouldBe("PR-only commit"); + local.Repository.Branches["origin/main"].ShouldNotBeNull().Tip.Sha.ShouldBe(remote.Repository.Head.Tip.Sha); + } + + [Test] + public void CommitTreePreservesParentOrderAndSignatures() + { + using var fixture = new EmptyRepositoryFixture(); + var first = fixture.Repository.MakeACommit("first"); + var second = fixture.Repository.MakeACommit("second"); + var author = new Signature("Test Author", "author@example.com", new DateTimeOffset(2023, 6, 1, 10, 0, 0, TimeSpan.FromHours(2))); + var committer = author with { Name = "Test Committer", Email = "committer@example.com", When = author.When.AddMinutes(1) }; + var commit = fixture.Repository.ObjectDatabase.CreateCommit(author, committer, "synthetic merge", first.Tree, [second, first]); + commit.Parents.Select(parent => parent.Sha).ShouldBe([second.Sha, first.Sha]); + commit.Author.ShouldBe(author); + commit.Committer.ShouldBe(committer); + commit.Tree.Sha.ShouldBe(first.Tree.Sha); + } +} diff --git a/src/GitVersion.Core.Tests/Core/GitCliMutatorTests.cs b/src/GitVersion.Core.Tests/Core/GitCliMutatorTests.cs index 191ecd607b..233022993e 100644 --- a/src/GitVersion.Core.Tests/Core/GitCliMutatorTests.cs +++ b/src/GitVersion.Core.Tests/Core/GitCliMutatorTests.cs @@ -1,7 +1,7 @@ using GitVersion.Git; using GitVersion.Helpers; using GitVersion.Testing.Extensions; -using LibGit2Sharp; +using GitVersion.Testing.Git; namespace GitVersion.Tests; diff --git a/src/GitVersion.Core.Tests/Core/GitVersionExecutorTests.cs b/src/GitVersion.Core.Tests/Core/GitVersionExecutorTests.cs index 143e76287b..eb0a6e60a1 100644 --- a/src/GitVersion.Core.Tests/Core/GitVersionExecutorTests.cs +++ b/src/GitVersion.Core.Tests/Core/GitVersionExecutorTests.cs @@ -5,8 +5,8 @@ using GitVersion.Git; using GitVersion.Helpers; using GitVersion.Testing.Extensions; +using GitVersion.Testing.Git; using GitVersion.VersionCalculation.Caching; -using LibGit2Sharp; using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace GitVersion.Tests; @@ -166,7 +166,7 @@ public void CacheKeyForWorktree() { // create a branch and a new worktree for it var repo = new Repository(fixture.RepositoryPath); - repo.Worktrees.Add("worktree", worktreePath, false); + repo.Worktrees.AddNewBranch("worktree", worktreePath); const string targetUrl = "https://github.com/GitTools/GitVersion.git"; @@ -407,7 +407,7 @@ public void GetProjectRootDirectoryWorkingDirectoryWithWorktree() { // create a branch and a new worktree for it var repo = new Repository(fixture.RepositoryPath); - repo.Worktrees.Add("worktree", worktreePath, false); + repo.Worktrees.AddNewBranch("worktree", worktreePath); const string targetUrl = "https://github.com/GitTools/GitVersion.git"; @@ -463,7 +463,7 @@ public void GetDotGitDirectoryWorktree() { // create a branch and a new worktree for it var repo = new Repository(fixture.RepositoryPath); - repo.Worktrees.Add("worktree", worktreePath, false); + repo.Worktrees.AddNewBranch("worktree", worktreePath); var gitVersionOptions = new GitVersionOptions { WorkingDirectory = worktreePath }; @@ -493,7 +493,7 @@ public void CalculateVersionFromWorktreeHead() fixture.Repository.MakeATaggedCommit("v2.0.0"); - fixture.Repository.Worktrees.Add(branchV1.CanonicalName, "1.0", worktreePath, false); + fixture.Repository.Worktrees.Add(branchV1.CanonicalName, worktreePath); using var worktreeFixture = new LocalRepositoryFixture(new(worktreePath)); var gitVersionOptions = new GitVersionOptions { WorkingDirectory = worktreeFixture.RepositoryPath }; @@ -736,7 +736,7 @@ private static IServiceProvider GetServiceProvider(GitVersionOptions gitVersionO services.AddSingleton(sp => { var fs = sp.GetRequiredService(); - return new GitRepositoryInfo(fs, options); + return new GitVersion.Git.ManagedGitRepositoryInfo(fs, options); }); }); diff --git a/src/GitVersion.Core.Tests/Core/GitVersionToolDirectoryTests.cs b/src/GitVersion.Core.Tests/Core/GitVersionToolDirectoryTests.cs index 0edf5e2b0a..20ed11cbca 100644 --- a/src/GitVersion.Core.Tests/Core/GitVersionToolDirectoryTests.cs +++ b/src/GitVersion.Core.Tests/Core/GitVersionToolDirectoryTests.cs @@ -1,6 +1,6 @@ using System.IO.Abstractions; using GitVersion.Helpers; -using LibGit2Sharp; +using GitVersion.Testing.Git; namespace GitVersion.Tests; @@ -36,7 +36,7 @@ public void FindsGitDirectory() gitVersionCalculator.CalculateVersionVariables(); }); - exception.ShouldNotBeAssignableTo(); + exception.ShouldNotBeAssignableTo(); } [Test] @@ -55,6 +55,6 @@ public void FindsGitDirectoryInParent() gitVersionCalculator.CalculateVersionVariables(); }); - exception.ShouldNotBeAssignableTo(); + exception.ShouldNotBeAssignableTo(); } } diff --git a/src/GitVersion.Core.Tests/Core/ManagedRepositoryContractTests.cs b/src/GitVersion.Core.Tests/Core/ManagedRepositoryContractTests.cs new file mode 100644 index 0000000000..16d82fc7a5 --- /dev/null +++ b/src/GitVersion.Core.Tests/Core/ManagedRepositoryContractTests.cs @@ -0,0 +1,362 @@ +using System.IO.Abstractions; +using GitVersion.Git; +using GitVersion.Helpers; +using GitVersion.Testing.Extensions; +using FixtureGit = GitVersion.Testing.Git; + +namespace GitVersion.Tests; + +/// Retains the migration corpus using Git CLI or explicit scenario expectations. +[TestFixture] +public class ManagedRepositoryContractTests : TestBase +{ + [Test] + public void ReferencesMatchGitCli() + { + using var fixture = CreateComplexFixture(); + using var repository = fixture.Repository.ToGitRepository(); + AssertReferences(fixture.Repository, repository); + repository.References.Head.ShouldNotBeNull().TargetIdentifier.ShouldBe("refs/heads/main"); + repository.References.FromGlob("refs/heads/*").Select(r => r.Name.Canonical).Order() + .ShouldBe(Lines(fixture.Repository.Run("for-each-ref", "--format=%(refname)", "refs/heads")).Order()); + } + + [Test] + public void BranchesAndLookupsMatchGitCli() + { + using var fixture = CreateComplexFixture(); + using var repository = fixture.Repository.ToGitRepository(); + AssertBranches(fixture.Repository, repository); + repository.Branches["missing"].ShouldBeNull(); + repository.Branches["develop"].ShouldNotBeNull().Tip.ShouldNotBeNull().Sha + .ShouldBe(fixture.Repository.Run("rev-parse", "refs/heads/develop").Trim()); + repository.Branches["refs/heads/develop"].ShouldNotBeNull().Name.Canonical.ShouldBe("refs/heads/develop"); + repository.Head.Name.Canonical.ShouldBe("refs/heads/main"); + repository.IsHeadDetached.ShouldBeFalse(); + repository.IsShallow.ShouldBeFalse(); + } + + [Test] + public void TagsMatchGitCli() + { + using var fixture = CreateComplexFixture(); + using var repository = fixture.Repository.ToGitRepository(); + AssertTags(fixture.Repository, repository); + } + + [Test] + public void CommitLogsMatchGitObjects() + { + using var fixture = CreateComplexFixture(); + using var repository = fixture.Repository.ToGitRepository(); + AssertCommits(fixture.Repository, repository); + } + + [Test] + public void QueryByPreservesReachabilityFirstParentAndSortContracts() + { + using var fixture = CreateCrissCrossFixture(); + using var repository = fixture.Repository.ToGitRepository(); + var commits = CollectAllCommits(repository); + var sorts = new[] { CommitSortStrategies.None, CommitSortStrategies.Time, CommitSortStrategies.Topological, CommitSortStrategies.Topological | CommitSortStrategies.Time }; + foreach (var sort in sorts) + { + foreach (var firstParent in new[] { false, true }) + { + foreach (var excluded in (string?[])[null, .. commits.Keys.Order().Take(3)]) + { + var filter = new CommitFilter + { + IncludeReachableFrom = repository.Branches["main"], + ExcludeReachableFrom = excluded == null ? null : commits[excluded], + FirstParentOnly = firstParent, + SortBy = sort + }; + var actual = repository.Commits.QueryBy(filter).ToArray(); + var args = new List { "rev-list", "main" }; + if (firstParent) + { + args.Add("--first-parent"); + } + + if (excluded != null) + { + args.Add("^" + excluded); + } + + actual.Select(c => c.Sha).Order().ShouldBe(Lines(fixture.Repository.Run([.. args])).Order()); + if (sort.HasFlag(CommitSortStrategies.Topological)) + { + AssertTopological(actual); + } + + repository.Commits.QueryBy(filter with { SortBy = sort | CommitSortStrategies.Reverse }).Select(c => c.Sha).ShouldBe(actual.Reverse().Select(c => c.Sha)); + } + } + } + } + + [Test] + public void MergeBasesMatchGitCliForAllCommitPairs() + { + using var fixture = CreateCrissCrossFixture(); + using var repository = fixture.Repository.ToGitRepository(); + AssertMergeBases(fixture.Repository, repository); + } + + [Test] + public void DiffPathsMatchGitCliForAllCommits() + { + using var fixture = CreateComplexFixture(); + using var repository = fixture.Repository.ToGitRepository(); + AssertDiffs(fixture.Repository, repository); + } + + [Test] + public void CountsModifiedStagedAndUntrackedPaths() + { + using var fixture = CreateDirtyFixture(); + using var repository = fixture.Repository.ToGitRepository(); + repository.UncommittedChangesCount().ShouldBe(3); + } + + [Test] + public void EmptyRepositoryHasUnbornMainAndUntrackedStatus() + { + using var fixture = new EmptyRepositoryFixture(); + File.WriteAllText(Path.Combine(fixture.RepositoryPath, "new.txt"), "new"); + using var repository = fixture.Repository.ToGitRepository(); + repository.Head.Name.Canonical.ShouldBe("refs/heads/main"); + repository.Head.Tip.ShouldBeNull(); + repository.IsHeadDetached.ShouldBeFalse(); + repository.UncommittedChangesCount().ShouldBe(1); + } + + [Test] + public void DetachedHeadRetainsTheSelectedCommit() + { + using var fixture = CreateComplexFixture(); + var target = fixture.Repository.Head.Tip.Parents.First().Sha; + FixtureGit.Commands.Checkout(fixture.Repository, target); + using var repository = fixture.Repository.ToGitRepository(); + repository.IsHeadDetached.ShouldBeTrue(); + repository.Head.IsDetachedHead.ShouldBeTrue(); + repository.Head.Tip.ShouldNotBeNull().Sha.ShouldBe(target); + } + + [Test] + public void RemotesAndTrackingBranchesMatchCloneConfiguration() + { + using var fixture = new RemoteRepositoryFixture(); + var local = fixture.LocalRepositoryFixture; + using var repository = local.Repository.ToGitRepository(); + repository.Remotes.Select(r => r.Name).ShouldBe(["origin"]); + var remote = repository.Remotes["origin"].ShouldNotBeNull(); + remote.Url.ShouldBe(local.Repository.Run("remote", "get-url", "origin").Trim()); + remote.FetchRefSpecs.Select(s => s.Specification).ShouldBe(["+refs/heads/*:refs/remotes/origin/*"]); + remote.PushRefSpecs.ShouldBeEmpty(); + var fetch = remote.FetchRefSpecs.Single(); + fetch.Source.ShouldBe("refs/heads/*"); + fetch.Destination.ShouldBe("refs/remotes/origin/*"); + repository.Branches["main"].ShouldNotBeNull().IsTracking.ShouldBeTrue(); + repository.Branches["origin/main"].ShouldNotBeNull().IsRemote.ShouldBeTrue(); + AssertBranches(local.Repository, repository); + } + + [Test] + public void RepositoryPathsMatchGitCli() + { + using var fixture = CreateComplexFixture(); + using var repository = fixture.Repository.ToGitRepository(); + Path.GetFullPath(repository.Path).TrimEnd(Path.DirectorySeparatorChar).ShouldBe(Path.GetFullPath(fixture.Repository.Run("rev-parse", "--absolute-git-dir").Trim()).TrimEnd(Path.DirectorySeparatorChar)); + Path.GetFullPath(repository.WorkingDirectory).TrimEnd(Path.DirectorySeparatorChar).ShouldBe(Path.GetFullPath(fixture.Repository.Run("rev-parse", "--show-toplevel").Trim()).TrimEnd(Path.DirectorySeparatorChar)); + } + + [Test] + public void BareRepositoryHasNoProjectRoot() + { + var path = FileSystemHelper.Path.GetRepositoryTempPath(); + FixtureGit.Repository.Init(path, isBare: true); + try + { + var info = new ManagedGitRepositoryInfo(new FileSystem(), Options.Create(new GitVersionOptions { WorkingDirectory = path })); + info.ProjectRootDirectory.ShouldBeNull(); + info.DotGitDirectory.ShouldNotBeNull().TrimEnd(Path.DirectorySeparatorChar).ShouldBe(Path.GetFullPath(path)); + } + finally { FileSystemHelper.Directory.DeleteDirectory(path); } + } + + [Test] + public void OctopusMergeRetainsAllThreeParents() + { + using var fixture = new EmptyRepositoryFixture(); + fixture.MakeACommit("base"); + fixture.BranchTo("topic/one"); fixture.MakeACommit("one"); + fixture.Checkout("main"); fixture.BranchTo("topic/two"); fixture.MakeACommit("two"); + fixture.Checkout("main"); fixture.MakeACommit("main one"); + fixture.Repository.Run("merge", "topic/one", "topic/two", "-m", "octopus"); + using var repository = fixture.Repository.ToGitRepository(); + repository.Head.Tip.ShouldNotBeNull().Parents.Count.ShouldBe(3); + AssertCommits(fixture.Repository, repository); + AssertMergeBases(fixture.Repository, repository); + AssertDiffs(fixture.Repository, repository); + } + + [Test] + public void LooseReferencesOverridePackedReferences() + { + using var fixture = CreateComplexFixture(); + fixture.Repository.Run("pack-refs", "--all"); + fixture.MakeACommit("shadow packed main"); fixture.BranchTo("loose/branch"); fixture.ApplyTag("loose-tag"); + using var repository = fixture.Repository.ToGitRepository(); + AssertReferences(fixture.Repository, repository); + AssertBranches(fixture.Repository, repository); + AssertTags(fixture.Repository, repository); + } + + [Test] + public void LightweightAndAnnotatedTagsPeelToTheSameCommit() + { + using var fixture = new EmptyRepositoryFixture(); + fixture.MakeACommit("only commit"); fixture.ApplyTag("lightweight"); + fixture.Repository.Tags.Add("annotated", fixture.Repository.Head.Tip, Generate.SignatureNow(), "annotated"); + using var repository = fixture.Repository.ToGitRepository(); + AssertTags(fixture.Repository, repository); + repository.Tags.Select(t => t.Commit.Sha).Distinct().Count().ShouldBe(1); + } + + [Test] + public void ShallowCloneStopsAtItsBoundary() + { + using var fixture = new RemoteRepositoryFixture(); + using var clone = fixture.CloneRepository(); clone.MakeShallow(); + using var repository = clone.Repository.ToGitRepository(); + repository.IsShallow.ShouldBeTrue(); + repository.Commits.Count().ShouldBe(1); + AssertCommits(clone.Repository, repository); + } + + [Test] + public void LinkedWorktreeReadsItsOwnHeadAndSharedReferences() + { + using var fixture = CreateComplexFixture(); + var path = FileSystemHelper.Path.GetRepositoryTempPath(); + fixture.Repository.Run("worktree", "add", path, "develop"); + try + { + using var git = new FixtureGit.Repository(path); + using var repository = git.ToGitRepository(); + repository.Head.Name.Canonical.ShouldBe("refs/heads/develop"); + repository.Head.Tip.ShouldNotBeNull().Sha.ShouldBe(git.Run("rev-parse", "HEAD").Trim()); + repository.WorkingDirectory.TrimEnd(Path.DirectorySeparatorChar).ShouldBe(path.TrimEnd(Path.DirectorySeparatorChar)); + repository.UncommittedChangesCount().ShouldBe(0); + AssertReferences(git, repository); AssertCommits(git, repository); + } + finally { fixture.Repository.Run("worktree", "remove", "--force", path); } + } + + [Test] + public void IndexVersionFourRetainsDirtyPathCounts() + { + using var fixture = CreateDirtyFixture(); fixture.Repository.Run("update-index", "--index-version", "4"); + using var repository = fixture.Repository.ToGitRepository(); + repository.UncommittedChangesCount().ShouldBe(3); + } + + private static string[] Lines(string output) => output.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + private static void AssertReferences(FixtureGit.Repository git, IGitRepository repository) => repository.References.Select(r => $"{r.Name.Canonical} {r.TargetIdentifier}").Order() + .ShouldBe(Lines(git.Run("for-each-ref", "--format=%(refname) %(objectname)")).Order()); + private static void AssertBranches(FixtureGit.Repository git, IGitRepository repository) => repository.Branches.Select(b => $"{b.Name.Canonical} {b.Tip?.Sha}").Order() + .ShouldBe(Lines(git.Run("for-each-ref", "--format=%(refname) %(objectname)", "refs/heads", "refs/remotes")).Order()); + private static void AssertTags(FixtureGit.Repository git, IGitRepository repository) + { + repository.Tags.Select(t => t.Name.Canonical).Order().ShouldBe(Lines(git.Run("for-each-ref", "--format=%(refname)", "refs/tags")).Order()); + foreach (var tag in repository.Tags) + { + tag.TargetSha.ShouldBe(git.Run("rev-parse", tag.Name.Canonical + "^{}").Trim()); + tag.Commit.Sha.ShouldBe(git.Run("rev-parse", tag.Name.Canonical + "^{commit}").Trim()); + } + } + private static void AssertCommits(FixtureGit.Repository git, IGitRepository repository) + { + foreach (var branch in repository.Branches) + { + var actual = branch.Commits.ToArray(); + actual.Select(c => c.Sha).Order().ShouldBe(Lines(git.Run("rev-list", branch.Name.Canonical)).Order()); + foreach (var commit in actual) + { + commit.Message.TrimEnd().ShouldBe(git.Run("show", "-s", "--format=%B", commit.Sha).TrimEnd()); + commit.Parents.Select(p => p.Sha).ShouldBe(git.Run("show", "-s", "--format=%P", commit.Sha).Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries)); + commit.IsMergeCommit.ShouldBe(commit.Parents.Count > 1); + } + } + repository.Commits.Select(c => c.Sha).Order().ShouldBe(Lines(git.Run("rev-list", "HEAD")).Order()); + } + private static void AssertTopological(IReadOnlyList commits) + { + var positions = commits.Select((c, i) => (c.Sha, i)).ToDictionary(p => p.Sha, p => p.i); + foreach (var commit in commits) + { + foreach (var parent in commit.Parents.Where(p => positions.ContainsKey(p.Sha))) + { + positions[parent.Sha].ShouldBeGreaterThan(positions[commit.Sha]); + } + } + } + private static Dictionary CollectAllCommits(IGitRepository repository) => repository.Branches.SelectMany(b => b.Commits).DistinctBy(c => c.Sha).ToDictionary(c => c.Sha); + private static void AssertMergeBases(FixtureGit.Repository git, IGitRepository repository) + { + var commits = CollectAllCommits(repository); + foreach (var first in commits.Values) + { + foreach (var second in commits.Values) + { + var expected = Lines(git.Run("merge-base", "--all", first.Sha, second.Sha)); + expected.ShouldContain(repository.FindMergeBase(first, second).ShouldNotBeNull().Sha); + } + } + } + private static void AssertDiffs(FixtureGit.Repository git, IGitRepository repository) + { + foreach (var commit in CollectAllCommits(repository).Values) + { + var args = commit.Parents.FirstOrDefault() is { } parent + ? new[] { "diff", "--no-renames", "--name-only", parent.Sha, commit.Sha } + : new[] { "diff-tree", "--root", "--no-commit-id", "--no-renames", "--name-only", "-r", commit.Sha }; + commit.DiffPaths.ShouldBe(Lines(git.Run(args))); + } + } + private static EmptyRepositoryFixture CreateDirtyFixture() + { + var fixture = new EmptyRepositoryFixture(); + File.WriteAllText(Path.Combine(fixture.RepositoryPath, "tracked.txt"), "one"); + FixtureGit.Commands.Stage(fixture.Repository, "tracked.txt"); + fixture.Repository.Commit("tracked", Generate.SignatureNow(), Generate.SignatureNow()); + File.WriteAllText(Path.Combine(fixture.RepositoryPath, "tracked.txt"), "two"); + File.WriteAllText(Path.Combine(fixture.RepositoryPath, "untracked.txt"), "new"); + File.WriteAllText(Path.Combine(fixture.RepositoryPath, "staged.txt"), "new"); FixtureGit.Commands.Stage(fixture.Repository, "staged.txt"); + return fixture; + } + private static EmptyRepositoryFixture CreateComplexFixture() + { + var fixture = new EmptyRepositoryFixture(); + fixture.MakeACommit("initial"); fixture.ApplyTag("1.0.0"); + fixture.BranchTo("develop"); fixture.MakeACommit("develop one"); + fixture.Checkout("main"); fixture.MakeACommit("main two"); fixture.MergeNoFF("develop"); + fixture.Repository.Tags.Add("v2.0.0", fixture.Repository.Head.Tip, Generate.SignatureNow(), "annotated"); + fixture.BranchTo("feature/complex"); fixture.MakeACommit("feature one"); + fixture.Checkout("develop"); fixture.MakeACommit("develop two"); fixture.Checkout("main"); + return fixture; + } + private static EmptyRepositoryFixture CreateCrissCrossFixture() + { + var fixture = new EmptyRepositoryFixture(); + fixture.MakeACommit("base"); fixture.BranchTo("develop"); fixture.MakeACommit("develop one"); var develop = fixture.Repository.Head.Tip; + fixture.Checkout("main"); fixture.MakeACommit("main one"); var main = fixture.Repository.Head.Tip; + var options = new FixtureGit.MergeOptions { FastForwardStrategy = FixtureGit.FastForwardStrategy.NoFastForward }; + fixture.Repository.Merge(develop, Generate.SignatureNow(), options); + fixture.Checkout("develop"); fixture.Repository.Merge(main, Generate.SignatureNow(), options); + fixture.MakeACommit("develop two"); fixture.Checkout("main"); fixture.MakeACommit("main two"); + return fixture; + } +} diff --git a/src/GitVersion.Core.Tests/Core/RepositoryStoreEqualTimestampTests.cs b/src/GitVersion.Core.Tests/Core/RepositoryStoreEqualTimestampTests.cs index c25ed76710..efc090dd14 100644 --- a/src/GitVersion.Core.Tests/Core/RepositoryStoreEqualTimestampTests.cs +++ b/src/GitVersion.Core.Tests/Core/RepositoryStoreEqualTimestampTests.cs @@ -1,8 +1,7 @@ using GitVersion.Configuration; using GitVersion.Testing.Extensions; -using LgCommit = LibGit2Sharp.Commit; -using LgSignature = LibGit2Sharp.Signature; -using LgTreeDefinition = LibGit2Sharp.TreeDefinition; +using LgCommit = GitVersion.Testing.Git.Commit; +using LgSignature = GitVersion.Testing.Git.Signature; namespace GitVersion.Tests; @@ -93,7 +92,7 @@ public void DerivedCommitLogIsAValidTopologicalOrderWhenTimestampsAreEqual() /// Builds a reproducible, randomly shaped history in which every commit carries the same timestamp, so the /// walk has to break ties between commits which are not ordered by time. /// - private static LibGit2Sharp.IRepository CreateRandomDagWithEqualTimestamps(EmptyRepositoryFixture fixture, int seed) + private static GitVersion.Testing.Git.IRepository CreateRandomDagWithEqualTimestamps(EmptyRepositoryFixture fixture, int seed) { var repository = fixture.Repository; @@ -109,7 +108,7 @@ int Next(int exclusiveMaximum) } var signature = new LgSignature("test", "test@test.io", DateTimeOffset.Now.AddHours(-1)); - var tree = repository.ObjectDatabase.CreateTree(new LgTreeDefinition()); + var tree = repository.ObjectDatabase.CreateEmptyTree(); var created = new List(); for (var i = 0; i < 14; i++) @@ -119,11 +118,11 @@ int Next(int exclusiveMaximum) var parents = created.OrderBy(_ => Next(int.MaxValue)).Take(parentCount); created.Add(repository.ObjectDatabase.CreateCommit( - signature, signature, $"commit {i}", tree, parents, prettifyMessage: false)); + signature, signature, $"commit {i}", tree, parents)); } var tip = repository.ObjectDatabase.CreateCommit( - signature, signature, "tip", tree, created, prettifyMessage: false); + signature, signature, "tip", tree, created); repository.Refs.Add("refs/heads/main", tip.Id, allowOverwrite: true); return repository; diff --git a/src/GitVersion.Core.Tests/Extensions/GitRepositoryTestingExtensions.cs b/src/GitVersion.Core.Tests/Extensions/GitRepositoryTestingExtensions.cs index 3da7548f86..b3081ff6ca 100644 --- a/src/GitVersion.Core.Tests/Extensions/GitRepositoryTestingExtensions.cs +++ b/src/GitVersion.Core.Tests/Extensions/GitRepositoryTestingExtensions.cs @@ -5,8 +5,8 @@ using GitVersion.Helpers; using GitVersion.OutputVariables; using GitVersion.Testing.Extensions; +using GitVersion.Testing.Git; using GitVersion.VersionCalculation; -using LibGit2Sharp; namespace GitVersion.Tests; @@ -78,7 +78,7 @@ public void DumpGraph(Action? writer = null, int? maxCommits = null) => DumpGraph(repository.ToGitRepository().Path, writer, maxCommits); } - extension(LibGit2Sharp.RemoteCollection remotes) + extension(GitVersion.Testing.Git.RemoteCollection remotes) { public void RenameRemote(string oldName, string newName) { diff --git a/src/GitVersion.Core.Tests/GitVersion.Core.Tests.csproj b/src/GitVersion.Core.Tests/GitVersion.Core.Tests.csproj index 99d378847f..09f7b65115 100644 --- a/src/GitVersion.Core.Tests/GitVersion.Core.Tests.csproj +++ b/src/GitVersion.Core.Tests/GitVersion.Core.Tests.csproj @@ -11,7 +11,6 @@ - diff --git a/src/GitVersion.Core.Tests/Helpers/GitVersionCoreTestModule.cs b/src/GitVersion.Core.Tests/Helpers/GitVersionCoreTestModule.cs index 4067886393..293ee6fffc 100644 --- a/src/GitVersion.Core.Tests/Helpers/GitVersionCoreTestModule.cs +++ b/src/GitVersion.Core.Tests/Helpers/GitVersionCoreTestModule.cs @@ -11,9 +11,8 @@ public class GitVersionCoreTestModule : IGitVersionModule { public void RegisterTypes(IServiceCollection services) { - services.AddModule(GitBackendSelector.Resolve() == GitBackend.Managed - ? new GitVersionManagedGitModule() - : new GitVersionLibGit2SharpModule()); + GitBackendSelector.Resolve(); + services.AddModule(new GitVersionManagedGitModule()); services.AddModule(new GitVersionBuildAgentsModule()); services.AddModule(new GitVersionOutputModule()); services.AddModule(new GitVersionConfigurationModule()); diff --git a/src/GitVersion.Core.Tests/IntegrationTests/BranchContextTests.cs b/src/GitVersion.Core.Tests/IntegrationTests/BranchContextTests.cs index ac988e7acd..2cb9ba70f1 100644 --- a/src/GitVersion.Core.Tests/IntegrationTests/BranchContextTests.cs +++ b/src/GitVersion.Core.Tests/IntegrationTests/BranchContextTests.cs @@ -3,9 +3,9 @@ using GitVersion.Git; using GitVersion.Helpers; using GitVersion.Testing.Extensions; +using GitVersion.Testing.Git; using GitVersion.VersionCalculation; using GitVersion.VersionCalculation.Caching; -using LibGit2Sharp; using CommitFilter = GitVersion.Git.CommitFilter; namespace GitVersion.Tests.IntegrationTests; @@ -604,7 +604,7 @@ public void ContextualPullRequestPrefersTargetFromSuccessfulFetch(bool noFetch, var path = Path.Combine(scratch, "checkout"); Repository.Clone(remote.RepositoryPath, path); using var repository = new Repository(path); - var target = repository.CreateBranch("support/1.0", repository.Lookup(originalTarget.Sha)); + var target = repository.CreateBranch("support/1.0", repository.Lookup(originalTarget.Sha)); Commands.Checkout(repository, repository.CreateBranch("feature/work", target.Tip)); var source = repository.MakeACommit(); Commands.Checkout(repository, target); diff --git a/src/GitVersion.Core.Tests/IntegrationTests/BranchWithoutCommitScenarios.cs b/src/GitVersion.Core.Tests/IntegrationTests/BranchWithoutCommitScenarios.cs index 7bb9ae4283..12910f6c2b 100644 --- a/src/GitVersion.Core.Tests/IntegrationTests/BranchWithoutCommitScenarios.cs +++ b/src/GitVersion.Core.Tests/IntegrationTests/BranchWithoutCommitScenarios.cs @@ -1,5 +1,5 @@ using GitVersion.Testing.Extensions; -using LibGit2Sharp; +using GitVersion.Testing.Git; namespace GitVersion.Tests.IntegrationTests; diff --git a/src/GitVersion.Core.Tests/IntegrationTests/DevelopScenarios.cs b/src/GitVersion.Core.Tests/IntegrationTests/DevelopScenarios.cs index 5b9b7d1981..f9481d1a44 100644 --- a/src/GitVersion.Core.Tests/IntegrationTests/DevelopScenarios.cs +++ b/src/GitVersion.Core.Tests/IntegrationTests/DevelopScenarios.cs @@ -1,7 +1,7 @@ using GitVersion.Configuration; using GitVersion.Testing.Extensions; +using GitVersion.Testing.Git; using GitVersion.VersionCalculation; -using LibGit2Sharp; namespace GitVersion.Tests.IntegrationTests; diff --git a/src/GitVersion.Core.Tests/IntegrationTests/DocumentationSamplesForGitFlow.cs b/src/GitVersion.Core.Tests/IntegrationTests/DocumentationSamplesForGitFlow.cs index 912a928645..ed4e981dd5 100644 --- a/src/GitVersion.Core.Tests/IntegrationTests/DocumentationSamplesForGitFlow.cs +++ b/src/GitVersion.Core.Tests/IntegrationTests/DocumentationSamplesForGitFlow.cs @@ -1,7 +1,7 @@ using GitVersion.Configuration; using GitVersion.Testing.Extensions; +using GitVersion.Testing.Git; using GitVersion.VersionCalculation; -using LibGit2Sharp; namespace GitVersion.Tests.IntegrationTests; diff --git a/src/GitVersion.Core.Tests/IntegrationTests/DocumentationSamplesForGitHubFlow.cs b/src/GitVersion.Core.Tests/IntegrationTests/DocumentationSamplesForGitHubFlow.cs index fcc4700dc4..57f993fde0 100644 --- a/src/GitVersion.Core.Tests/IntegrationTests/DocumentationSamplesForGitHubFlow.cs +++ b/src/GitVersion.Core.Tests/IntegrationTests/DocumentationSamplesForGitHubFlow.cs @@ -1,7 +1,7 @@ using GitVersion.Configuration; using GitVersion.Testing.Extensions; +using GitVersion.Testing.Git; using GitVersion.VersionCalculation; -using LibGit2Sharp; namespace GitVersion.Tests.IntegrationTests; diff --git a/src/GitVersion.Core.Tests/IntegrationTests/FeatureBranchScenarios.cs b/src/GitVersion.Core.Tests/IntegrationTests/FeatureBranchScenarios.cs index 2dd69830da..bcf4a96ee2 100644 --- a/src/GitVersion.Core.Tests/IntegrationTests/FeatureBranchScenarios.cs +++ b/src/GitVersion.Core.Tests/IntegrationTests/FeatureBranchScenarios.cs @@ -1,7 +1,7 @@ using GitVersion.Configuration; using GitVersion.Testing.Extensions; +using GitVersion.Testing.Git; using GitVersion.VersionCalculation; -using LibGit2Sharp; namespace GitVersion.Tests.IntegrationTests; diff --git a/src/GitVersion.Core.Tests/IntegrationTests/HotfixBranchScenarios.cs b/src/GitVersion.Core.Tests/IntegrationTests/HotfixBranchScenarios.cs index f334ced802..283b4db495 100644 --- a/src/GitVersion.Core.Tests/IntegrationTests/HotfixBranchScenarios.cs +++ b/src/GitVersion.Core.Tests/IntegrationTests/HotfixBranchScenarios.cs @@ -1,6 +1,6 @@ using GitVersion.Configuration; using GitVersion.Testing.Extensions; -using LibGit2Sharp; +using GitVersion.Testing.Git; namespace GitVersion.Tests.IntegrationTests; diff --git a/src/GitVersion.Core.Tests/IntegrationTests/IgnoreCommitScenarios.cs b/src/GitVersion.Core.Tests/IntegrationTests/IgnoreCommitScenarios.cs index 8fa98f7b06..f32f268d0a 100644 --- a/src/GitVersion.Core.Tests/IntegrationTests/IgnoreCommitScenarios.cs +++ b/src/GitVersion.Core.Tests/IntegrationTests/IgnoreCommitScenarios.cs @@ -344,7 +344,7 @@ public void GivenTrunkBasedWorkflowWithIgnoreConfigurationForPathThenVersionShou fixture.MakeACommit("C"); fixture.MakeACommit("D"); - var ignoredPath = fixture.Repository.Diff.Compare(commitA.Tree, commitB.Tree).Select(element => element.Path).First(); + var ignoredPath = fixture.Repository.Diff.Compare(commitA.Tree, commitB.Tree).Select(element => element.Path).First(); var configuration = TrunkBasedConfigurationBuilder.New .WithIgnoreConfiguration(new IgnoreConfiguration { Paths = { ignoredPath } }) @@ -364,7 +364,7 @@ public void GivenTrunkBasedWorkflowWithIgnoreConfigurationForPathAndCommitParame var commitC = fixture.Repository.MakeACommit("C"); fixture.MakeACommit("D"); - var ignoredPath = fixture.Repository.Diff.Compare(commitA.Tree, commitC.Tree).Select(element => element.Path).First(); + var ignoredPath = fixture.Repository.Diff.Compare(commitA.Tree, commitC.Tree).Select(element => element.Path).First(); var configuration = TrunkBasedConfigurationBuilder.New .WithIgnoreConfiguration(new IgnoreConfiguration { Paths = { ignoredPath } }) @@ -384,7 +384,7 @@ public void GivenGitHubFlowWorkflowWithIgnoreConfigurationForPathThenVersionShou fixture.MakeACommit("C"); fixture.MakeACommit("D"); - var ignoredPath = fixture.Repository.Diff.Compare(commitA.Tree, commitB.Tree).Select(element => element.Path).First(); + var ignoredPath = fixture.Repository.Diff.Compare(commitA.Tree, commitB.Tree).Select(element => element.Path).First(); var configuration = GitHubFlowConfigurationBuilder.New .WithIgnoreConfiguration(new IgnoreConfiguration { Paths = { ignoredPath } }) @@ -404,7 +404,7 @@ public void GivenTrunkBasedWorkflowWithIgnoreConfigurationForTaggedCommitPathThe fixture.ApplyTag("1.0.0"); fixture.MakeACommit("C"); - var ignoredPath = fixture.Repository.Diff.Compare(commitA.Tree, commitB.Tree).Select(element => element.Path).First(); + var ignoredPath = fixture.Repository.Diff.Compare(commitA.Tree, commitB.Tree).Select(element => element.Path).First(); var configuration = TrunkBasedConfigurationBuilder.New .WithIgnoreConfiguration(new IgnoreConfiguration { Paths = { ignoredPath } }) @@ -424,7 +424,7 @@ public void GivenGitHubFlowWorkflowWithIgnoreConfigurationForTaggedCommitPathThe fixture.ApplyTag("1.0.0"); fixture.MakeACommit("C"); - var ignoredPath = fixture.Repository.Diff.Compare(commitA.Tree, commitB.Tree).Select(element => element.Path).First(); + var ignoredPath = fixture.Repository.Diff.Compare(commitA.Tree, commitB.Tree).Select(element => element.Path).First(); var configuration = GitHubFlowConfigurationBuilder.New .WithIgnoreConfiguration(new IgnoreConfiguration { Paths = { ignoredPath } }) diff --git a/src/GitVersion.Core.Tests/IntegrationTests/MainScenarios.cs b/src/GitVersion.Core.Tests/IntegrationTests/MainScenarios.cs index 9389e559fc..6b20f18cc9 100644 --- a/src/GitVersion.Core.Tests/IntegrationTests/MainScenarios.cs +++ b/src/GitVersion.Core.Tests/IntegrationTests/MainScenarios.cs @@ -1,7 +1,7 @@ using GitVersion.Configuration; using GitVersion.Testing.Extensions; +using GitVersion.Testing.Git; using GitVersion.VersionCalculation; -using LibGit2Sharp; namespace GitVersion.Tests.IntegrationTests; diff --git a/src/GitVersion.Core.Tests/IntegrationTests/OtherBranchScenarios.cs b/src/GitVersion.Core.Tests/IntegrationTests/OtherBranchScenarios.cs index f54fc69fe8..9aba0b5f24 100644 --- a/src/GitVersion.Core.Tests/IntegrationTests/OtherBranchScenarios.cs +++ b/src/GitVersion.Core.Tests/IntegrationTests/OtherBranchScenarios.cs @@ -1,7 +1,7 @@ using GitVersion.Configuration; using GitVersion.Testing.Extensions; +using GitVersion.Testing.Git; using GitVersion.VersionCalculation; -using LibGit2Sharp; namespace GitVersion.Tests.IntegrationTests; diff --git a/src/GitVersion.Core.Tests/IntegrationTests/OtherScenarios.cs b/src/GitVersion.Core.Tests/IntegrationTests/OtherScenarios.cs index ed352ef1fa..d7a59b5890 100644 --- a/src/GitVersion.Core.Tests/IntegrationTests/OtherScenarios.cs +++ b/src/GitVersion.Core.Tests/IntegrationTests/OtherScenarios.cs @@ -4,8 +4,8 @@ using GitVersion.Extensions; using GitVersion.Helpers; using GitVersion.Testing.Extensions; +using GitVersion.Testing.Git; using GitVersion.VersionCalculation; -using LibGit2Sharp; namespace GitVersion.Tests.IntegrationTests; diff --git a/src/GitVersion.Core.Tests/IntegrationTests/PullRequestScenarios.cs b/src/GitVersion.Core.Tests/IntegrationTests/PullRequestScenarios.cs index 17cba2fa5a..65ebde4b8a 100644 --- a/src/GitVersion.Core.Tests/IntegrationTests/PullRequestScenarios.cs +++ b/src/GitVersion.Core.Tests/IntegrationTests/PullRequestScenarios.cs @@ -1,7 +1,7 @@ using GitVersion.Configuration; using GitVersion.Testing.Extensions; +using GitVersion.Testing.Git; using GitVersion.VersionCalculation; -using LibGit2Sharp; namespace GitVersion.Tests.IntegrationTests; diff --git a/src/GitVersion.Core.Tests/IntegrationTests/ReleaseBranchScenarios.cs b/src/GitVersion.Core.Tests/IntegrationTests/ReleaseBranchScenarios.cs index f05a915d82..1cb6bfb997 100644 --- a/src/GitVersion.Core.Tests/IntegrationTests/ReleaseBranchScenarios.cs +++ b/src/GitVersion.Core.Tests/IntegrationTests/ReleaseBranchScenarios.cs @@ -1,6 +1,6 @@ using GitVersion.Configuration; using GitVersion.Testing.Extensions; -using LibGit2Sharp; +using GitVersion.Testing.Git; namespace GitVersion.Tests.IntegrationTests; diff --git a/src/GitVersion.Core.Tests/IntegrationTests/RemoteRepositoryScenarios.cs b/src/GitVersion.Core.Tests/IntegrationTests/RemoteRepositoryScenarios.cs index eeb2d82f60..017193b99c 100644 --- a/src/GitVersion.Core.Tests/IntegrationTests/RemoteRepositoryScenarios.cs +++ b/src/GitVersion.Core.Tests/IntegrationTests/RemoteRepositoryScenarios.cs @@ -1,7 +1,7 @@ using GitVersion.Agents; using GitVersion.Configuration; using GitVersion.Testing.Extensions; -using LibGit2Sharp; +using GitVersion.Testing.Git; namespace GitVersion.Tests.IntegrationTests; diff --git a/src/GitVersion.Core.Tests/IntegrationTests/ShaLabelScenarios.cs b/src/GitVersion.Core.Tests/IntegrationTests/ShaLabelScenarios.cs index 85271e08db..ac5383c719 100644 --- a/src/GitVersion.Core.Tests/IntegrationTests/ShaLabelScenarios.cs +++ b/src/GitVersion.Core.Tests/IntegrationTests/ShaLabelScenarios.cs @@ -10,9 +10,7 @@ namespace GitVersion.Tests.IntegrationTests; public class ShaLabelScenarios : TestBase { [TestCase("managed", "ShortSha")] - [TestCase("libgit2", "ShortSha")] [TestCase("managed", "Sha")] - [TestCase("libgit2", "Sha")] public void FeatureLabelUsesCurrentCommitAndChangesOnNextCommit(string backend, string placeholder) { using var backendScope = new BackendScope(backend); @@ -51,7 +49,6 @@ public void FeatureLabelUsesCurrentCommitAndChangesOnNextCommit(string backend, } [TestCase("managed")] - [TestCase("libgit2")] public void MainlineMergedHistoryUsesCalculationCommitForLabel(string backend) { using var backendScope = new BackendScope(backend); @@ -88,7 +85,6 @@ public void MainlineMergedHistoryUsesCalculationCommitForLabel(string backend) } [TestCase("managed")] - [TestCase("libgit2")] public void TaggedCurrentCommitPreservesStableVersionWithShaLabel(string backend) { using var backendScope = new BackendScope(backend); diff --git a/src/GitVersion.Core.Tests/IntegrationTests/SupportBranchScenarios.cs b/src/GitVersion.Core.Tests/IntegrationTests/SupportBranchScenarios.cs index 4ea353009f..539ffa0e49 100644 --- a/src/GitVersion.Core.Tests/IntegrationTests/SupportBranchScenarios.cs +++ b/src/GitVersion.Core.Tests/IntegrationTests/SupportBranchScenarios.cs @@ -1,6 +1,6 @@ using GitVersion.Configuration; using GitVersion.Testing.Extensions; -using LibGit2Sharp; +using GitVersion.Testing.Git; namespace GitVersion.Tests.IntegrationTests; diff --git a/src/GitVersion.Core.Tests/IntegrationTests/SwitchingToGitFlowScenarios.cs b/src/GitVersion.Core.Tests/IntegrationTests/SwitchingToGitFlowScenarios.cs index 71da44d23d..cba6f4d84b 100644 --- a/src/GitVersion.Core.Tests/IntegrationTests/SwitchingToGitFlowScenarios.cs +++ b/src/GitVersion.Core.Tests/IntegrationTests/SwitchingToGitFlowScenarios.cs @@ -1,5 +1,5 @@ using GitVersion.Testing.Extensions; -using LibGit2Sharp; +using GitVersion.Testing.Git; namespace GitVersion.Tests.IntegrationTests; diff --git a/src/GitVersion.Core.Tests/IntegrationTests/TrackReleaseBranchesScenarios.cs b/src/GitVersion.Core.Tests/IntegrationTests/TrackReleaseBranchesScenarios.cs index ab62439d3d..fef2150a11 100644 --- a/src/GitVersion.Core.Tests/IntegrationTests/TrackReleaseBranchesScenarios.cs +++ b/src/GitVersion.Core.Tests/IntegrationTests/TrackReleaseBranchesScenarios.cs @@ -1,6 +1,6 @@ using GitVersion.Configuration; +using GitVersion.Testing.Git; using GitVersion.VersionCalculation; -using LibGit2Sharp; namespace GitVersion.Tests.IntegrationTests; @@ -287,7 +287,7 @@ public void ReleaseWithUnrelatedHistoryDoesNotAdvanceMinor(string releaseBranch) fixture.MakeATaggedCommit("1.2.1"); var mainCommit = fixture.Repository.Head.Tip; var unrelatedCommit = fixture.Repository.ObjectDatabase.CreateCommit( - mainCommit.Author, mainCommit.Committer, "Unrelated release root", mainCommit.Tree, [], false); + mainCommit.Author, mainCommit.Committer, "Unrelated release root", mainCommit.Tree, []); fixture.Repository.CreateBranch(releaseBranch, unrelatedCommit); fixture.Repository.Tags.Add("9.0.0-beta.1", unrelatedCommit); fixture.BranchTo("develop"); diff --git a/src/GitVersion.Core.Tests/IntegrationTests/VersionBumpingScenarios.cs b/src/GitVersion.Core.Tests/IntegrationTests/VersionBumpingScenarios.cs index cb26b5441c..bcaf2cb9ef 100644 --- a/src/GitVersion.Core.Tests/IntegrationTests/VersionBumpingScenarios.cs +++ b/src/GitVersion.Core.Tests/IntegrationTests/VersionBumpingScenarios.cs @@ -1,7 +1,7 @@ using GitVersion.Configuration; using GitVersion.Testing.Extensions; +using GitVersion.Testing.Git; using GitVersion.VersionCalculation; -using LibGit2Sharp; namespace GitVersion.Tests.IntegrationTests; diff --git a/src/GitVersion.Core.Tests/IntegrationTests/VersionInCurrentBranchNameScenarios.cs b/src/GitVersion.Core.Tests/IntegrationTests/VersionInCurrentBranchNameScenarios.cs index bd286b7f4b..1a00a4a4ee 100644 --- a/src/GitVersion.Core.Tests/IntegrationTests/VersionInCurrentBranchNameScenarios.cs +++ b/src/GitVersion.Core.Tests/IntegrationTests/VersionInCurrentBranchNameScenarios.cs @@ -1,5 +1,5 @@ using GitVersion.Configuration; -using LibGit2Sharp; +using GitVersion.Testing.Git; namespace GitVersion.Tests.IntegrationTests; diff --git a/src/GitVersion.Core.Tests/IntegrationTests/VersionInMergedBranchNameScenarios.cs b/src/GitVersion.Core.Tests/IntegrationTests/VersionInMergedBranchNameScenarios.cs index 90830d7144..1531cceeab 100644 --- a/src/GitVersion.Core.Tests/IntegrationTests/VersionInMergedBranchNameScenarios.cs +++ b/src/GitVersion.Core.Tests/IntegrationTests/VersionInMergedBranchNameScenarios.cs @@ -1,5 +1,5 @@ using GitVersion.Configuration; -using LibGit2Sharp; +using GitVersion.Testing.Git; namespace GitVersion.Tests.IntegrationTests; diff --git a/src/GitVersion.Core.Tests/IntegrationTests/WorktreeScenarios.cs b/src/GitVersion.Core.Tests/IntegrationTests/WorktreeScenarios.cs index 81e4d58816..fa4a18db61 100644 --- a/src/GitVersion.Core.Tests/IntegrationTests/WorktreeScenarios.cs +++ b/src/GitVersion.Core.Tests/IntegrationTests/WorktreeScenarios.cs @@ -1,7 +1,7 @@ using System.IO.Abstractions; using GitVersion.Helpers; using GitVersion.Testing.Extensions; -using LibGit2Sharp; +using GitVersion.Testing.Git; namespace GitVersion.Tests.IntegrationTests; @@ -25,7 +25,7 @@ public void UseWorktreeRepositoryForVersion() fixture.Repository.MakeATaggedCommit("v2.0.0"); fixture.AssertFullSemver("2.0.0"); - fixture.Repository.Worktrees.Add(branchV1.CanonicalName, "1.0", worktreePath, false); + fixture.Repository.Worktrees.Add(branchV1.CanonicalName, worktreePath); using var worktreeFixture = new LocalRepositoryFixture(new(worktreePath)); worktreeFixture.AssertFullSemver("1.0.0"); } diff --git a/src/GitVersion.Core.Tests/VersionCalculation/NextVersionCalculatorTests.cs b/src/GitVersion.Core.Tests/VersionCalculation/NextVersionCalculatorTests.cs index 949999cc7e..b7152e8a84 100644 --- a/src/GitVersion.Core.Tests/VersionCalculation/NextVersionCalculatorTests.cs +++ b/src/GitVersion.Core.Tests/VersionCalculation/NextVersionCalculatorTests.cs @@ -1,6 +1,6 @@ using GitVersion.Configuration; +using GitVersion.Testing.Git; using GitVersion.VersionCalculation; -using LibGit2Sharp; namespace GitVersion.Tests.VersionCalculation; diff --git a/src/GitVersion.Core/Git/GitBackend.cs b/src/GitVersion.Core/Git/GitBackend.cs index 9aed34481e..74f6f472f9 100644 --- a/src/GitVersion.Core/Git/GitBackend.cs +++ b/src/GitVersion.Core/Git/GitBackend.cs @@ -1,44 +1,32 @@ namespace GitVersion.Git; -/// -/// The Git backend implementations selectable via the GITVERSION_GIT_BACKEND -/// environment variable during the dual-backend window (v7.0). -/// +/// The sole Git implementation since v7.1. internal enum GitBackend { - /// The temporary libgit2-based fallback, removed in v7.1. - LibGit2, - - /// The managed reader + git CLI mutator backend (the v7.0 default). Managed } -/// -/// The single place where GITVERSION_GIT_BACKEND is interpreted: every composition -/// root selects the backend through this class so production and tests cannot drift, and -/// the v7.0 default is consistent. This is configuration, not Git logic — it -/// names no backend internals, so it lives at the composition seam that every root already -/// shares rather than inside one of the backend assemblies. -/// +/// Validates the v7 compatibility setting; selector removal is reserved for v8. internal static class GitBackendSelector { public const string EnvironmentVariableName = "GITVERSION_GIT_BACKEND"; - private const string ManagedName = "managed"; - /// - /// The canonical configuration name of the resolved backend, as users set it - /// in GITVERSION_GIT_BACKEND: libgit2 or managed. - /// - public static string ResolveName() => Resolve() == GitBackend.Managed ? ManagedName : "libgit2"; + public static string ResolveName() + { + Resolve(); + return "managed"; + } + + public static GitBackend Resolve() + { + var value = SysEnv.GetEnvironmentVariable(EnvironmentVariableName)?.Trim(); + if (string.IsNullOrEmpty(value) || value.Equals("managed", StringComparison.OrdinalIgnoreCase)) + { + return GitBackend.Managed; + } - /// - /// Resolves the backend from the environment: managed (the default when unset) - /// or the temporary libgit2 fallback. Unknown values fail fast. - /// - /// The selected backend. - /// The variable is set to an unrecognized value. - public static GitBackend Resolve() => - FeatureSelector.Resolve(EnvironmentVariableName, ManagedName, "libgit2", ManagedName) == ManagedName - ? GitBackend.Managed - : GitBackend.LibGit2; + throw new WarningException(value.Equals("libgit2", StringComparison.OrdinalIgnoreCase) + ? $"{EnvironmentVariableName}=libgit2 is no longer supported: LibGit2Sharp was removed in v7.1. Unset {EnvironmentVariableName} or set it to 'managed'." + : $"Unrecognized {EnvironmentVariableName} value '{value}'. The supported value is 'managed'; unset the variable to use the default."); + } } diff --git a/src/GitVersion.Core/GitVersion.Core.csproj b/src/GitVersion.Core/GitVersion.Core.csproj index a3c6c699ce..d5508469ce 100644 --- a/src/GitVersion.Core/GitVersion.Core.csproj +++ b/src/GitVersion.Core/GitVersion.Core.csproj @@ -32,7 +32,6 @@ - diff --git a/src/GitVersion.Core/Helpers/FileSystemHelper.cs b/src/GitVersion.Core/Helpers/FileSystemHelper.cs index 4b331170d0..c76901317d 100644 --- a/src/GitVersion.Core/Helpers/FileSystemHelper.cs +++ b/src/GitVersion.Core/Helpers/FileSystemHelper.cs @@ -53,8 +53,8 @@ public static void DeleteDirectory(string directoryPath) Trace.WriteLine(string.Format("{0}The directory '{1}' could not be deleted!" + "{0}Most of the time, this is due to an external process accessing the files in the temporary repositories created during the test runs, and keeping a handle on the directory, thus preventing the deletion of those files." + "{0}Known and common causes include:" + - "{0}- Windows Search Indexer (go to the Indexing Options, in the Windows Control Panel, and exclude the bin folder of LibGit2Sharp.Tests)" + - "{0}- Antivirus (exclude the bin folder of LibGit2Sharp.Tests from the paths scanned by your real-time antivirus){0}", + "{0}- Windows Search Indexer (go to the Indexing Options, in the Windows Control Panel, and exclude the bin folder of GitVersion test projects)" + + "{0}- Antivirus (exclude the bin folder of GitVersion test projects from the paths scanned by your real-time antivirus){0}", Path.NewLine, fileSystem.Path.GetFullPath(directoryPath))); } } diff --git a/src/GitVersion.Git.Managed.Tests/Approved/RevisionWalks.json b/src/GitVersion.Git.Managed.Tests/Approved/RevisionWalks.json new file mode 100644 index 0000000000..c60566bfba --- /dev/null +++ b/src/GitVersion.Git.Managed.Tests/Approved/RevisionWalks.json @@ -0,0 +1,555 @@ +{ + "Linear/0/False/False": [ + "commit 5", + "commit 4", + "commit 3", + "commit 2", + "commit 1", + "commit 0" + ], + "Linear/0/True/False": [ + "commit 5", + "commit 4", + "commit 3", + "commit 2", + "commit 1", + "commit 0" + ], + "Linear/2/False/False": [ + "commit 5", + "commit 4", + "commit 3", + "commit 2", + "commit 1", + "commit 0" + ], + "Linear/2/True/False": [ + "commit 5", + "commit 4", + "commit 3", + "commit 2", + "commit 1", + "commit 0" + ], + "Linear/1/False/False": [ + "commit 5", + "commit 4", + "commit 3", + "commit 2", + "commit 1", + "commit 0" + ], + "Linear/1/True/False": [ + "commit 5", + "commit 4", + "commit 3", + "commit 2", + "commit 1", + "commit 0" + ], + "Linear/3/False/False": [ + "commit 5", + "commit 4", + "commit 3", + "commit 2", + "commit 1", + "commit 0" + ], + "Linear/3/True/False": [ + "commit 5", + "commit 4", + "commit 3", + "commit 2", + "commit 1", + "commit 0" + ], + "Linear/5/False/False": [ + "commit 0", + "commit 1", + "commit 2", + "commit 3", + "commit 4", + "commit 5" + ], + "Linear/5/True/False": [ + "commit 0", + "commit 1", + "commit 2", + "commit 3", + "commit 4", + "commit 5" + ], + "Linear/6/False/False": [ + "commit 0", + "commit 1", + "commit 2", + "commit 3", + "commit 4", + "commit 5" + ], + "Linear/6/True/False": [ + "commit 0", + "commit 1", + "commit 2", + "commit 3", + "commit 4", + "commit 5" + ], + "Linear/7/False/False": [ + "commit 0", + "commit 1", + "commit 2", + "commit 3", + "commit 4", + "commit 5" + ], + "Linear/7/True/False": [ + "commit 0", + "commit 1", + "commit 2", + "commit 3", + "commit 4", + "commit 5" + ], + "Merged/0/False/False": [ + "Merge branch \u0027feature\u0027", + "main 2", + "main 1", + "feature 2", + "feature 1", + "main 0" + ], + "Merged/0/False/True": [ + "Merge branch \u0027feature\u0027", + "main 2", + "main 1", + "feature 2", + "feature 1" + ], + "Merged/0/True/False": [ + "Merge branch \u0027feature\u0027", + "main 2", + "main 1", + "main 0" + ], + "Merged/0/True/True": [ + "Merge branch \u0027feature\u0027", + "main 2", + "main 1" + ], + "Merged/2/False/False": [ + "Merge branch \u0027feature\u0027", + "main 2", + "main 1", + "feature 2", + "feature 1", + "main 0" + ], + "Merged/2/False/True": [ + "Merge branch \u0027feature\u0027", + "main 2", + "main 1", + "feature 2", + "feature 1" + ], + "Merged/2/True/False": [ + "Merge branch \u0027feature\u0027", + "main 2", + "main 1", + "main 0" + ], + "Merged/2/True/True": [ + "Merge branch \u0027feature\u0027", + "main 2", + "main 1" + ], + "Merged/1/False/False": [ + "Merge branch \u0027feature\u0027", + "feature 2", + "feature 1", + "main 2", + "main 1", + "main 0" + ], + "Merged/1/False/True": [ + "Merge branch \u0027feature\u0027", + "feature 2", + "feature 1", + "main 2", + "main 1" + ], + "Merged/1/True/False": [ + "Merge branch \u0027feature\u0027", + "main 2", + "main 1", + "main 0" + ], + "Merged/1/True/True": [ + "Merge branch \u0027feature\u0027", + "main 2", + "main 1" + ], + "Merged/3/False/False": [ + "Merge branch \u0027feature\u0027", + "main 2", + "main 1", + "feature 2", + "feature 1", + "main 0" + ], + "Merged/3/False/True": [ + "Merge branch \u0027feature\u0027", + "main 2", + "main 1", + "feature 2", + "feature 1" + ], + "Merged/3/True/False": [ + "Merge branch \u0027feature\u0027", + "main 2", + "main 1", + "main 0" + ], + "Merged/3/True/True": [ + "Merge branch \u0027feature\u0027", + "main 2", + "main 1" + ], + "Merged/5/False/False": [ + "main 0", + "main 1", + "main 2", + "feature 1", + "feature 2", + "Merge branch \u0027feature\u0027" + ], + "Merged/5/False/True": [ + "main 1", + "main 2", + "feature 1", + "feature 2", + "Merge branch \u0027feature\u0027" + ], + "Merged/5/True/False": [ + "main 0", + "main 1", + "main 2", + "Merge branch \u0027feature\u0027" + ], + "Merged/5/True/True": [ + "main 1", + "main 2", + "Merge branch \u0027feature\u0027" + ], + "Merged/6/False/False": [ + "main 0", + "feature 1", + "feature 2", + "main 1", + "main 2", + "Merge branch \u0027feature\u0027" + ], + "Merged/6/False/True": [ + "feature 1", + "feature 2", + "main 1", + "main 2", + "Merge branch \u0027feature\u0027" + ], + "Merged/6/True/False": [ + "main 0", + "main 1", + "main 2", + "Merge branch \u0027feature\u0027" + ], + "Merged/6/True/True": [ + "main 1", + "main 2", + "Merge branch \u0027feature\u0027" + ], + "Merged/7/False/False": [ + "main 0", + "feature 1", + "feature 2", + "main 1", + "main 2", + "Merge branch \u0027feature\u0027" + ], + "Merged/7/False/True": [ + "feature 1", + "feature 2", + "main 1", + "main 2", + "Merge branch \u0027feature\u0027" + ], + "Merged/7/True/False": [ + "main 0", + "main 1", + "main 2", + "Merge branch \u0027feature\u0027" + ], + "Merged/7/True/True": [ + "main 1", + "main 2", + "Merge branch \u0027feature\u0027" + ], + "EqualTimestamp/0/False/False": [ + "Merge branch \u0027feature\u0027", + "main 2", + "feature 2", + "main 1", + "feature 1", + "main 0" + ], + "EqualTimestamp/0/True/False": [ + "Merge branch \u0027feature\u0027", + "main 2", + "main 1", + "main 0" + ], + "EqualTimestamp/2/False/False": [ + "Merge branch \u0027feature\u0027", + "main 0", + "feature 1", + "main 1", + "feature 2", + "main 2" + ], + "EqualTimestamp/2/True/False": [ + "Merge branch \u0027feature\u0027", + "main 0", + "main 1", + "main 2" + ], + "EqualTimestamp/1/False/False": [ + "Merge branch \u0027feature\u0027", + "feature 2", + "feature 1", + "main 2", + "main 1", + "main 0" + ], + "EqualTimestamp/1/True/False": [ + "Merge branch \u0027feature\u0027", + "main 2", + "main 1", + "main 0" + ], + "EqualTimestamp/3/False/False": [ + "Merge branch \u0027feature\u0027", + "main 2", + "feature 2", + "main 1", + "feature 1", + "main 0" + ], + "EqualTimestamp/3/True/False": [ + "Merge branch \u0027feature\u0027", + "main 2", + "main 1", + "main 0" + ], + "EqualTimestamp/5/False/False": [ + "main 0", + "main 1", + "main 2", + "feature 1", + "feature 2", + "Merge branch \u0027feature\u0027" + ], + "EqualTimestamp/5/True/False": [ + "main 0", + "main 1", + "main 2", + "Merge branch \u0027feature\u0027" + ], + "EqualTimestamp/6/False/False": [ + "main 2", + "feature 2", + "main 1", + "feature 1", + "main 0", + "Merge branch \u0027feature\u0027" + ], + "EqualTimestamp/6/True/False": [ + "main 2", + "main 1", + "main 0", + "Merge branch \u0027feature\u0027" + ], + "EqualTimestamp/7/False/False": [ + "main 0", + "feature 1", + "main 1", + "feature 2", + "main 2", + "Merge branch \u0027feature\u0027" + ], + "EqualTimestamp/7/True/False": [ + "main 0", + "main 1", + "main 2", + "Merge branch \u0027feature\u0027" + ], + "CrissCross/0/False/False": [ + "Merge commit \u0027f7ad7beb31a7ab0a53c4a8ec27aba4ecde8e0fff\u0027 into dev", + "main 1", + "dev 1", + "root" + ], + "CrissCross/0/True/False": [ + "Merge commit \u0027f7ad7beb31a7ab0a53c4a8ec27aba4ecde8e0fff\u0027 into dev", + "dev 1", + "root" + ], + "CrissCross/2/False/False": [ + "Merge commit \u0027f7ad7beb31a7ab0a53c4a8ec27aba4ecde8e0fff\u0027 into dev", + "main 1", + "dev 1", + "root" + ], + "CrissCross/2/True/False": [ + "Merge commit \u0027f7ad7beb31a7ab0a53c4a8ec27aba4ecde8e0fff\u0027 into dev", + "dev 1", + "root" + ], + "CrissCross/1/False/False": [ + "Merge commit \u0027f7ad7beb31a7ab0a53c4a8ec27aba4ecde8e0fff\u0027 into dev", + "main 1", + "dev 1", + "root" + ], + "CrissCross/1/True/False": [ + "Merge commit \u0027f7ad7beb31a7ab0a53c4a8ec27aba4ecde8e0fff\u0027 into dev", + "dev 1", + "root" + ], + "CrissCross/3/False/False": [ + "Merge commit \u0027f7ad7beb31a7ab0a53c4a8ec27aba4ecde8e0fff\u0027 into dev", + "main 1", + "dev 1", + "root" + ], + "CrissCross/3/True/False": [ + "Merge commit \u0027f7ad7beb31a7ab0a53c4a8ec27aba4ecde8e0fff\u0027 into dev", + "dev 1", + "root" + ], + "CrissCross/5/False/False": [ + "root", + "dev 1", + "main 1", + "Merge commit \u0027f7ad7beb31a7ab0a53c4a8ec27aba4ecde8e0fff\u0027 into dev" + ], + "CrissCross/5/True/False": [ + "root", + "dev 1", + "Merge commit \u0027f7ad7beb31a7ab0a53c4a8ec27aba4ecde8e0fff\u0027 into dev" + ], + "CrissCross/6/False/False": [ + "root", + "dev 1", + "main 1", + "Merge commit \u0027f7ad7beb31a7ab0a53c4a8ec27aba4ecde8e0fff\u0027 into dev" + ], + "CrissCross/6/True/False": [ + "root", + "dev 1", + "Merge commit \u0027f7ad7beb31a7ab0a53c4a8ec27aba4ecde8e0fff\u0027 into dev" + ], + "CrissCross/7/False/False": [ + "root", + "dev 1", + "main 1", + "Merge commit \u0027f7ad7beb31a7ab0a53c4a8ec27aba4ecde8e0fff\u0027 into dev" + ], + "CrissCross/7/True/False": [ + "root", + "dev 1", + "Merge commit \u0027f7ad7beb31a7ab0a53c4a8ec27aba4ecde8e0fff\u0027 into dev" + ], + "Octopus/0/False/False": [ + "Merge branches \u0027b1\u0027 and \u0027b2\u0027", + "main 1", + "b2 1", + "b1 1", + "root" + ], + "Octopus/0/True/False": [ + "Merge branches \u0027b1\u0027 and \u0027b2\u0027", + "main 1", + "root" + ], + "Octopus/2/False/False": [ + "Merge branches \u0027b1\u0027 and \u0027b2\u0027", + "main 1", + "b2 1", + "b1 1", + "root" + ], + "Octopus/2/True/False": [ + "Merge branches \u0027b1\u0027 and \u0027b2\u0027", + "main 1", + "root" + ], + "Octopus/1/False/False": [ + "Merge branches \u0027b1\u0027 and \u0027b2\u0027", + "b2 1", + "b1 1", + "main 1", + "root" + ], + "Octopus/1/True/False": [ + "Merge branches \u0027b1\u0027 and \u0027b2\u0027", + "main 1", + "root" + ], + "Octopus/3/False/False": [ + "Merge branches \u0027b1\u0027 and \u0027b2\u0027", + "main 1", + "b2 1", + "b1 1", + "root" + ], + "Octopus/3/True/False": [ + "Merge branches \u0027b1\u0027 and \u0027b2\u0027", + "main 1", + "root" + ], + "Octopus/5/False/False": [ + "root", + "main 1", + "b1 1", + "b2 1", + "Merge branches \u0027b1\u0027 and \u0027b2\u0027" + ], + "Octopus/5/True/False": [ + "root", + "main 1", + "Merge branches \u0027b1\u0027 and \u0027b2\u0027" + ], + "Octopus/6/False/False": [ + "root", + "b1 1", + "b2 1", + "main 1", + "Merge branches \u0027b1\u0027 and \u0027b2\u0027" + ], + "Octopus/6/True/False": [ + "root", + "main 1", + "Merge branches \u0027b1\u0027 and \u0027b2\u0027" + ], + "Octopus/7/False/False": [ + "root", + "b1 1", + "b2 1", + "main 1", + "Merge branches \u0027b1\u0027 and \u0027b2\u0027" + ], + "Octopus/7/True/False": [ + "root", + "main 1", + "Merge branches \u0027b1\u0027 and \u0027b2\u0027" + ] +} diff --git a/src/GitVersion.Git.Managed.Tests/GitRevisionWalkerTests.cs b/src/GitVersion.Git.Managed.Tests/GitRevisionWalkerTests.cs index 32b584c5e5..bb045e28df 100644 --- a/src/GitVersion.Git.Managed.Tests/GitRevisionWalkerTests.cs +++ b/src/GitVersion.Git.Managed.Tests/GitRevisionWalkerTests.cs @@ -1,4 +1,4 @@ -using LibGit2Sharp; +using System.Runtime.CompilerServices; namespace GitVersion.Git.Managed.Tests; @@ -22,7 +22,7 @@ public class GitRevisionWalkerTests ]; [TestCaseSource(nameof(SortCombinations))] - public void LinearHistoryMatchesLibGit2(string sortName) + public void LinearHistoryMatchesRecordedWalks(string sortName) { var sort = Enum.Parse(sortName); using var repository = CreateLinearRepository(); @@ -31,7 +31,7 @@ public void LinearHistoryMatchesLibGit2(string sortName) } [TestCaseSource(nameof(SortCombinations))] - public void MergedHistoryMatchesLibGit2(string sortName) + public void MergedHistoryMatchesRecordedWalks(string sortName) { var sort = Enum.Parse(sortName); using var repository = CreateMergedRepository(); @@ -40,7 +40,7 @@ public void MergedHistoryMatchesLibGit2(string sortName) } [TestCaseSource(nameof(SortCombinations))] - public void MergedHistoryWithExcludesMatchesLibGit2(string sortName) + public void MergedHistoryWithExcludesMatchesRecordedWalks(string sortName) { var sort = Enum.Parse(sortName); using var repository = CreateMergedRepository(); @@ -54,7 +54,7 @@ public void MergedHistoryWithExcludesMatchesLibGit2(string sortName) } [TestCaseSource(nameof(SortCombinations))] - public void EqualCommitterTimestampsMatchLibGit2(string sortName) + public void EqualCommitterTimestampsMatchRecordedWalks(string sortName) { var sort = Enum.Parse(sortName); using var repository = CreateEqualTimestampRepository(); @@ -63,7 +63,7 @@ public void EqualCommitterTimestampsMatchLibGit2(string sortName) } [TestCaseSource(nameof(SortCombinations))] - public void CrissCrossHistoryMatchesLibGit2(string sortName) + public void CrissCrossHistoryMatchesRecordedWalks(string sortName) { var sort = Enum.Parse(sortName); using var repository = CreateCrissCrossRepository(); @@ -72,7 +72,7 @@ public void CrissCrossHistoryMatchesLibGit2(string sortName) } [TestCaseSource(nameof(SortCombinations))] - public void OctopusMergeMatchesLibGit2(string sortName) + public void OctopusMergeMatchesRecordedWalks(string sortName) { var sort = Enum.Parse(sortName); using var repository = CreateOctopusRepository(); @@ -82,7 +82,7 @@ public void OctopusMergeMatchesLibGit2(string sortName) [TestCase(false)] [TestCase(true)] - public void FirstParentWalksMatchLibGit2(bool withExclude) + public void FirstParentWalksMatchRecordedWalks(bool withExclude) { using var repository = CreateMergedRepository(); @@ -100,7 +100,7 @@ public void FirstParentWalksMatchLibGit2(bool withExclude) } [Test] - public void MultipleIncludesMatchLibGit2() + public void MultipleIncludesMatchRecordedWalks() { using var repository = CreateMergedRepository(); // Include two historic points rather than the merged tip. @@ -114,15 +114,8 @@ public void MultipleIncludesMatchLibGit2() using var store = repository.OpenObjectStore(); var actual = new GitRevisionWalker(store).Walk(options).Select(commit => commit.Sha.ToString()).ToList(); - using var libgit2 = new Repository(repository.RepositoryPath); - var filter = new global::LibGit2Sharp.CommitFilter - { - IncludeReachableFrom = new[] { first, second }, - SortBy = global::LibGit2Sharp.CommitSortStrategies.Topological | global::LibGit2Sharp.CommitSortStrategies.Time - }; - var expected = libgit2.Commits.QueryBy(filter).Select(commit => commit.Sha).ToList(); - - actual.ShouldBe(expected); + var expected = repository.Run("rev-list", first, second).Split('\n', StringSplitOptions.RemoveEmptyEntries); + actual.Order().ShouldBe(expected.Order()); } [Test] @@ -246,68 +239,57 @@ public void WalkingFromAMissingCommitFails() Should.Throw(() => new GitRevisionWalker(store).Walk(options)); } - private static void AssertWalkParity(GitTestRepository repository, GitRevisionWalkOptions options, bool head, string? excludeSha = null) + // Captured from LibGit2Sharp 0.32.0 before removal. Messages are unique in these + // deterministic histories, so this pins exact walk ordering without a native oracle. + private static readonly Dictionary RecordedWalks = LoadRecordedWalks(); + + private static Dictionary LoadRecordedWalks() + { + using var stream = typeof(GitRevisionWalkerTests).Assembly.GetManifestResourceStream("GitVersion.Approved.RevisionWalks.json")!; + return JsonSerializer.Deserialize>(stream)!; + } + + private static void AssertWalkParity(GitTestRepository repository, GitRevisionWalkOptions options, bool head, string? excludeSha = null, [CallerMemberName] string scenario = "") { if (head) { options.Include.Add(repository.ResolveId("HEAD")); } - using var store = repository.OpenObjectStore(); - var actual = new GitRevisionWalker(store).Walk(options).Select(commit => commit.Sha.ToString()).ToList(); - - using var libgit2 = new Repository(repository.RepositoryPath); - var filter = new global::LibGit2Sharp.CommitFilter + var fixture = scenario switch { - IncludeReachableFrom = options.Include[0].ToString(), - SortBy = ToLibGit2Sort(options.Sort), - FirstParentOnly = options.FirstParentOnly + _ when scenario.StartsWith("Linear", StringComparison.Ordinal) => "Linear", + _ when scenario.StartsWith("Equal", StringComparison.Ordinal) => "EqualTimestamp", + _ when scenario.StartsWith("CrissCross", StringComparison.Ordinal) => "CrissCross", + _ when scenario.StartsWith("Octopus", StringComparison.Ordinal) => "Octopus", + _ => "Merged" }; - - if (excludeSha is not null) + using var store = repository.OpenObjectStore(); + var commits = new GitRevisionWalker(store).Walk(options); + var key = $"{fixture}/{(int)options.Sort}/{options.FirstParentOnly}/{excludeSha != null}"; + commits.Select(c => c.Message.TrimEnd()).ShouldBe(RecordedWalks[key], key); + var arguments = new List { "rev-list", options.Include[0].ToString() }; + if (options.FirstParentOnly) { - filter.ExcludeReachableFrom = excludeSha; + arguments.Add("--first-parent"); } - var expected = libgit2.Commits.QueryBy(filter).Select(commit => commit.Sha).ToList(); + if (excludeSha != null) + { + arguments.Add("^" + excludeSha); + } - actual.ShouldBe(expected, $"sort: {options.Sort}, firstParent: {options.FirstParentOnly}, exclude: {excludeSha}"); - actual.ShouldNotBeEmpty(); + commits.Select(c => c.Sha.ToString()).Order().ShouldBe(repository.Run([.. arguments]).Split('\n', StringSplitOptions.RemoveEmptyEntries).Order()); + commits.ShouldNotBeEmpty(); } private static void AssertMergeBaseParity(GitTestRepository repository, string first, string second) { using var store = repository.OpenObjectStore(); var actual = new GitRevisionWalker(store).FindMergeBase(GitObjectId.Parse(first), GitObjectId.Parse(second)); - - using var libgit2 = new Repository(repository.RepositoryPath); - var expected = libgit2.ObjectDatabase.FindMergeBase(libgit2.Lookup(first), libgit2.Lookup(second)); - - expected.ShouldNotBeNull(); + var expected = repository.Run("merge-base", "--all", first, second).Split('\n', StringSplitOptions.RemoveEmptyEntries); actual.ShouldNotBeNull(); - actual.Value.ToString().ShouldBe(expected.Sha); - } - - private static global::LibGit2Sharp.CommitSortStrategies ToLibGit2Sort(GitRevisionSortStrategies sort) - { - var result = global::LibGit2Sharp.CommitSortStrategies.None; - - if (sort.HasFlag(GitRevisionSortStrategies.Topological)) - { - result |= global::LibGit2Sharp.CommitSortStrategies.Topological; - } - - if (sort.HasFlag(GitRevisionSortStrategies.Time)) - { - result |= global::LibGit2Sharp.CommitSortStrategies.Time; - } - - if (sort.HasFlag(GitRevisionSortStrategies.Reverse)) - { - result |= global::LibGit2Sharp.CommitSortStrategies.Reverse; - } - - return result; + expected.ShouldContain(actual.Value.ToString()); } private static void DeleteLooseObject(GitTestRepository repository, string sha) diff --git a/src/GitVersion.Git.Managed.Tests/GitStatusCalculatorTests.cs b/src/GitVersion.Git.Managed.Tests/GitStatusCalculatorTests.cs index bf1141d34d..c0b8e5bdc9 100644 --- a/src/GitVersion.Git.Managed.Tests/GitStatusCalculatorTests.cs +++ b/src/GitVersion.Git.Managed.Tests/GitStatusCalculatorTests.cs @@ -1,10 +1,9 @@ -using LibGit2Sharp; namespace GitVersion.Git.Managed.Tests; /// -/// Validates against the exact expression the -/// LibGit2Sharp adapter uses for IGitRepository.UncommittedChangesCount. +/// Validates against explicit scenario counts +/// and Git CLI status, counting each dirty path once. /// [TestFixture] public class GitStatusCalculatorTests @@ -206,9 +205,9 @@ public void WorksAgainstAnIndexInVersion4Format() private static void AssertParity(GitTestRepository repository, int expectedCount) { var actual = ManagedCount(repository); - var expected = LibGit2Count(repository); + var expected = GitCliCount(repository); - actual.ShouldBe(expected, "managed count should match the libgit2 adapter"); + actual.ShouldBe(expected, "managed count should match Git CLI status"); actual.ShouldBe(expectedCount, "scenario expectation"); } @@ -223,19 +222,6 @@ private static int ManagedCount(GitTestRepository repository) : calculator.CountChangesInEmptyRepository(); } - private static int LibGit2Count(GitTestRepository repository) - { - // The exact expression from GitVersion.LibGit2Sharp's GetUncommittedChangesCountInternal. - using var libgit2 = new Repository(repository.RepositoryPath); - - if (libgit2.Head?.Tip == null) - { - var status = libgit2.RetrieveStatus(); - return status.Untracked.Count() + status.Staged.Count(); - } - - return libgit2.Diff - .Compare(libgit2.Head.Tip.Tree, DiffTargets.Index | DiffTargets.WorkingDirectory) - .Count; - } + private static int GitCliCount(GitTestRepository repository) => repository.Run("status", "--porcelain=v1", "-z", "--untracked-files=all", "--no-renames") + .Split('\0', StringSplitOptions.RemoveEmptyEntries).Length; } diff --git a/src/GitVersion.Git.Managed.Tests/GitTreeDiffTests.cs b/src/GitVersion.Git.Managed.Tests/GitTreeDiffTests.cs index 6756958f40..37452275ab 100644 --- a/src/GitVersion.Git.Managed.Tests/GitTreeDiffTests.cs +++ b/src/GitVersion.Git.Managed.Tests/GitTreeDiffTests.cs @@ -1,4 +1,3 @@ -using LibGit2Sharp; namespace GitVersion.Git.Managed.Tests; @@ -10,7 +9,7 @@ namespace GitVersion.Git.Managed.Tests; public class GitTreeDiffTests { [Test] - public void ChangedPathsMatchLibGit2ForEveryCommit() + public void ChangedPathsMatchGitCliForEveryCommit() { using var repository = new GitTestRepository(); repository.WriteFile("a.txt", "one\n"); @@ -33,7 +32,7 @@ public void ChangedPathsMatchLibGit2ForEveryCommit() } [Test] - public void ChangedPathsMatchLibGit2WhenAFileBecomesADirectory() + public void ChangedPathsMatchGitCliWhenAFileBecomesADirectory() { using var repository = new GitTestRepository(); repository.WriteFile("thing", "file\n"); @@ -135,7 +134,6 @@ private static void AssertDiffPathsParityForAllCommits(GitTestRepository reposit var options = new GitRevisionWalkOptions(); options.Include.Add(repository.ResolveId("HEAD")); - using var libgit2 = new Repository(repository.RepositoryPath); foreach (var commit in walker.Walk(options)) { @@ -145,12 +143,10 @@ private static void AssertDiffPathsParityForAllCommits(GitTestRepository reposit var actual = treeDiff.GetChangedPaths(commit.Tree, parentTree); - // The adapter's DiffPaths: Compare(commit.Tree, firstParent?.Tree).Paths - var libgit2Commit = libgit2.Lookup(commit.Sha.ToString())!; - var expected = libgit2.Diff - .Compare(libgit2Commit.Tree, libgit2Commit.Parents.FirstOrDefault()?.Tree) - .Select(element => element.Path) - .ToList(); + var arguments = commit.Parents.Count > 0 + ? new[] { "diff", "--no-renames", "--name-only", "-z", commit.Parents[0].ToString(), commit.Sha.ToString() } + : new[] { "diff-tree", "--root", "--no-commit-id", "--no-renames", "--name-only", "-z", "-r", commit.Sha.ToString() }; + var expected = repository.Run(arguments).Split('\0', StringSplitOptions.RemoveEmptyEntries); actual.ShouldBe(expected, $"commit: {commit.Message.TrimEnd()}"); } diff --git a/src/GitVersion.Git.Managed.Tests/GitVersion.Git.Managed.Tests.csproj b/src/GitVersion.Git.Managed.Tests/GitVersion.Git.Managed.Tests.csproj index fd5fab9235..11686efb90 100644 --- a/src/GitVersion.Git.Managed.Tests/GitVersion.Git.Managed.Tests.csproj +++ b/src/GitVersion.Git.Managed.Tests/GitVersion.Git.Managed.Tests.csproj @@ -8,4 +8,7 @@ + + + diff --git a/src/GitVersion.LibGit2Sharp/Git/Branch.cs b/src/GitVersion.LibGit2Sharp/Git/Branch.cs deleted file mode 100644 index 5975922edf..0000000000 --- a/src/GitVersion.LibGit2Sharp/Git/Branch.cs +++ /dev/null @@ -1,39 +0,0 @@ -using GitVersion.Extensions; -using GitVersion.Helpers; - -namespace GitVersion.Git; - -internal sealed class Branch : IBranch -{ - private static readonly LambdaEqualityHelper equalityHelper = new(x => x.Name.Canonical); - private static readonly LambdaKeyComparer comparerHelper = new(x => x.Name.Canonical); - - private readonly LibGit2Sharp.Branch innerBranch; - - internal Branch(LibGit2Sharp.Branch branch, LibGit2Sharp.Diff diff, GitRepositoryCache repositoryCache) - { - diff.NotNull(); - repositoryCache.NotNull(); - this.innerBranch = branch.NotNull(); - Name = new(branch.CanonicalName); - - var commit = this.innerBranch.Tip; - Tip = commit is null ? null : repositoryCache.GetOrWrap(commit, diff); - - var commits = this.innerBranch.Commits; - Commits = new CommitCollection(commits, diff, repositoryCache); - } - - public ReferenceName Name { get; } - public ICommit? Tip { get; } - public ICommitCollection Commits { get; } - public int CompareTo(IBranch? other) => comparerHelper.Compare(this, other); - public bool IsDetachedHead => Name.Canonical.Equals("(no branch)", StringComparison.OrdinalIgnoreCase); - public bool IsRemote => this.innerBranch.IsRemote; - public bool IsTracking => this.innerBranch.IsTracking; - public bool Equals(IBranch? other) => equalityHelper.Equals(this, other); - public override bool Equals(object? obj) => Equals(obj as IBranch); - public override int GetHashCode() => equalityHelper.GetHashCode(this); - public override string ToString() => Name.ToString(); - public static implicit operator LibGit2Sharp.Branch(Branch d) => d.innerBranch; -} diff --git a/src/GitVersion.LibGit2Sharp/Git/BranchCollection.cs b/src/GitVersion.LibGit2Sharp/Git/BranchCollection.cs deleted file mode 100644 index 5154ee5191..0000000000 --- a/src/GitVersion.LibGit2Sharp/Git/BranchCollection.cs +++ /dev/null @@ -1,54 +0,0 @@ -using GitVersion.Extensions; -using LibGit2Sharp; - -namespace GitVersion.Git; - -internal sealed class BranchCollection : IBranchCollection -{ - private readonly LibGit2Sharp.BranchCollection innerCollection; - private readonly Lazy> branches; - private readonly Diff diff; - private readonly GitRepositoryCache repositoryCache; - - internal BranchCollection(LibGit2Sharp.BranchCollection collection, Diff diff, GitRepositoryCache repositoryCache) - { - this.innerCollection = collection.NotNull(); - this.branches = new Lazy>(() => [.. this.innerCollection.Select(branch => repositoryCache.GetOrWrap(branch, diff))]); - this.diff = diff.NotNull(); - this.repositoryCache = repositoryCache.NotNull(); - } - - public IEnumerator GetEnumerator() => this.branches.Value.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - public IBranch? this[string name] - { - get - { - name = name.NotNull(); - var branch = this.innerCollection[name]; - return branch is null ? null : this.repositoryCache.GetOrWrap(branch, this.diff); - } - } - - public IEnumerable ExcludeBranches(IEnumerable branchesToExclude) - { - var toExclude = branchesToExclude as IBranch[] ?? [.. branchesToExclude]; - - return this.Where(BranchIsNotExcluded); - - bool BranchIsNotExcluded(IBranch branch) => toExclude.All(branchToExclude => !branch.Equals(branchToExclude)); - } - - public void UpdateTrackedBranch(IBranch branch, string remoteTrackingReferenceName) - { - var branchToUpdate = (Branch)branch.NotNull(); - - this.innerCollection.Update(branchToUpdate, Updater); - return; - - void Updater(BranchUpdater branchUpdater) => - branchUpdater.TrackedBranch = remoteTrackingReferenceName; - } -} diff --git a/src/GitVersion.LibGit2Sharp/Git/Commit.cs b/src/GitVersion.LibGit2Sharp/Git/Commit.cs deleted file mode 100644 index bd21f0b193..0000000000 --- a/src/GitVersion.LibGit2Sharp/Git/Commit.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections.Concurrent; -using GitVersion.Extensions; -using GitVersion.Helpers; - -namespace GitVersion.Git; - -internal sealed class Commit : ICommit -{ - private static readonly ConcurrentDictionary> pathsCache = new(); - private static readonly LambdaEqualityHelper equalityHelper = new(x => x.Id); - private static readonly LambdaKeyComparer comparerHelper = new(x => x.Sha); - private readonly Lazy> parentsLazy; - - private readonly LibGit2Sharp.Commit innerCommit; - private readonly LibGit2Sharp.Diff repoDiff; - - internal Commit(LibGit2Sharp.Commit innerCommit, LibGit2Sharp.Diff repoDiff, GitRepositoryCache repositoryCache) - { - repoDiff.NotNull(); - repositoryCache.NotNull(); - this.innerCommit = innerCommit.NotNull(); - this.parentsLazy = new(() => [.. innerCommit.Parents.Select(parent => repositoryCache.GetOrWrap(parent, repoDiff))]); - Id = new ObjectId(innerCommit.Id); - Sha = innerCommit.Sha; - When = innerCommit.Committer.When; - this.repoDiff = repoDiff; - } - - public int CompareTo(ICommit? other) => comparerHelper.Compare(this, other); - public IReadOnlyList Parents => this.parentsLazy.Value; - public IObjectId Id { get; } - public string Sha { get; } - public DateTimeOffset When { get; } - public string Message => this.innerCommit.Message; - public bool IsMergeCommit => Parents.Count >= 2; - - public IReadOnlyList DiffPaths - { - get - { - if (!pathsCache.TryGetValue(Sha, out var paths)) - { - paths = CommitChanges.Paths; - pathsCache[Sha] = paths; - } - return paths; - } - } - public bool Equals(ICommit? other) => equalityHelper.Equals(this, other); - public override bool Equals(object? obj) => Equals(obj as ICommit); - public override int GetHashCode() => equalityHelper.GetHashCode(this); - public override string ToString() => $"'{Id.ToString(7)}' - {this.innerCommit.MessageShort}"; - public static implicit operator LibGit2Sharp.Commit(Commit d) => d.innerCommit; - private TreeChanges CommitChanges => new(this.repoDiff.Compare(this.innerCommit.Tree, this.innerCommit.Parents.FirstOrDefault()?.Tree)); -} diff --git a/src/GitVersion.LibGit2Sharp/Git/CommitCollection.cs b/src/GitVersion.LibGit2Sharp/Git/CommitCollection.cs deleted file mode 100644 index 1270fad214..0000000000 --- a/src/GitVersion.LibGit2Sharp/Git/CommitCollection.cs +++ /dev/null @@ -1,72 +0,0 @@ -using GitVersion.Extensions; -using LibGit2Sharp; - -namespace GitVersion.Git; - -internal sealed class CommitCollection : ICommitCollection -{ - private readonly ICommitLog innerCollection; - private readonly Lazy> commits; - private readonly Diff diff; - private readonly GitRepositoryCache repositoryCache; - - internal CommitCollection(ICommitLog collection, Diff diff, GitRepositoryCache repositoryCache) - { - this.innerCollection = collection.NotNull(); - this.commits = new Lazy>(() => [.. this.innerCollection.Select(commit => repositoryCache.GetOrWrap(commit, diff))]); - this.diff = diff.NotNull(); - this.repositoryCache = repositoryCache.NotNull(); - } - - public IEnumerator GetEnumerator() => this.commits.Value.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - public IEnumerable GetCommitsPriorTo(DateTimeOffset olderThan) - => this.SkipWhile(c => c.When > olderThan); - - public IEnumerable QueryBy(CommitFilter commitFilter) - { - var includeReachableFrom = GetReacheableFrom(commitFilter.IncludeReachableFrom); - var excludeReachableFrom = GetReacheableFrom(commitFilter.ExcludeReachableFrom); - var filter = new LibGit2Sharp.CommitFilter - { - IncludeReachableFrom = includeReachableFrom, - ExcludeReachableFrom = excludeReachableFrom, - FirstParentOnly = commitFilter.FirstParentOnly, - SortBy = GetSortStrategies(commitFilter.SortBy) - }; - var commitLog = ((IQueryableCommitLog)this.innerCollection).QueryBy(filter); - return new CommitCollection(commitLog, this.diff, this.repositoryCache); - - static object? GetReacheableFrom(ICommitish? item) => - item switch - { - Commit c => (LibGit2Sharp.Commit)c, - Branch b => (LibGit2Sharp.Branch)b, - IBranch b => GetReacheableFrom(b.Tip), - _ => null - }; - - static LibGit2Sharp.CommitSortStrategies GetSortStrategies(CommitSortStrategies sortBy) - { - var result = LibGit2Sharp.CommitSortStrategies.None; - if (sortBy.HasFlag(CommitSortStrategies.Topological)) - { - result |= LibGit2Sharp.CommitSortStrategies.Topological; - } - - if (sortBy.HasFlag(CommitSortStrategies.Time)) - { - result |= LibGit2Sharp.CommitSortStrategies.Time; - } - - if (sortBy.HasFlag(CommitSortStrategies.Reverse)) - { - result |= LibGit2Sharp.CommitSortStrategies.Reverse; - } - - return result; - } - } -} diff --git a/src/GitVersion.LibGit2Sharp/Git/GitRepository.cs b/src/GitVersion.LibGit2Sharp/Git/GitRepository.cs deleted file mode 100644 index ebfbd881f2..0000000000 --- a/src/GitVersion.LibGit2Sharp/Git/GitRepository.cs +++ /dev/null @@ -1,110 +0,0 @@ -using GitVersion.Extensions; -using GitVersion.Helpers; -using LibGit2Sharp; - -namespace GitVersion.Git; - -internal sealed partial class GitRepository -{ - private Lazy? repositoryLazy; - private readonly GitRepositoryCache repositoryCache = new(); - - private IRepository RepositoryInstance - { - get - { - var lazy = this.repositoryLazy ?? throw new InvalidOperationException("Repository not initialized. Call DiscoverRepository() first."); - return lazy.Value; - } - } - public string Path => RepositoryInstance.Info.Path; - public string WorkingDirectory => RepositoryInstance.Info.WorkingDirectory; - public bool IsHeadDetached => RepositoryInstance.Info.IsHeadDetached; - public bool IsShallow => RepositoryInstance.Info.IsShallow; - public IBranch Head => this.repositoryCache.GetOrWrap(RepositoryInstance.Head, RepositoryInstance.Diff); - - private ITagCollection? tags; - public ITagCollection Tags => this.tags ??= new TagCollection(RepositoryInstance.Tags, RepositoryInstance.Diff, this.repositoryCache); - - public IBranchCollection Branches => new BranchCollection(RepositoryInstance.Branches, RepositoryInstance.Diff, this.repositoryCache); - - private ICommitCollection? commits; - public ICommitCollection Commits => this.commits ??= new CommitCollection(RepositoryInstance.Commits, RepositoryInstance.Diff, this.repositoryCache); - - private IRemoteCollection? remotes; - public IRemoteCollection Remotes => this.remotes ??= new RemoteCollection(RepositoryInstance.Network.Remotes, this.repositoryCache); - - private IReferenceCollection? references; - public IReferenceCollection References => this.references ??= new ReferenceCollection(RepositoryInstance.Refs, this.repositoryCache); - - public void DiscoverRepository(string? gitDirectory) - { - if (gitDirectory?.EndsWith(".git") == false) - { - gitDirectory = Repository.Discover(gitDirectory); - } - this.repositoryLazy = new(() => new Repository(gitDirectory)); - } - - public ICommit? FindMergeBase(ICommit commit, ICommit otherCommit) - { - commit = commit.NotNull(); - otherCommit = otherCommit.NotNull(); - - var retryAction = new RetryAction(); - return retryAction.Execute(() => - { - var first = (Commit)commit; - var second = (Commit)otherCommit; - var mergeBase = RepositoryInstance.ObjectDatabase.FindMergeBase(first, second); - return mergeBase == null ? null : this.repositoryCache.GetOrWrap(mergeBase, RepositoryInstance.Diff); - }); - } - - public int UncommittedChangesCount() - { - var retryAction = new RetryAction(); - return retryAction.Execute(GetUncommittedChangesCountInternal); - } - - public void Dispose() - { - if (this.repositoryLazy is not { IsValueCreated: true }) - { - return; - } - - RepositoryInstance.Dispose(); - this.repositoryLazy = null; - } - - private int GetUncommittedChangesCountInternal() - { - // check if we have a branch tip at all to behave properly with empty repos - // => return that we have actually un-committed changes because we are apparently - // running GitVersion on something which lives inside this brand-new repo _/\Ö/\_ - if (RepositoryInstance.Head?.Tip == null || RepositoryInstance.Diff == null) - { - // this is a somewhat cumbersome way of figuring out the number of changes in the repo - // which is more expensive than to use the Diff as it gathers more info, but - // we can't use the other method when we are dealing with a new/empty repo - try - { - var status = RepositoryInstance.RetrieveStatus(); - return status.Untracked.Count() + status.Staged.Count(); - } - catch (Exception) - { - return int.MaxValue; // this should be somewhat puzzling to see, - // so we may have reached our goal to show that - // that repo is really "Dirty"... - } - } - - // gets all changes of the last commit vs Staging area and WT - var changes = RepositoryInstance.Diff.Compare(RepositoryInstance.Head.Tip.Tree, - DiffTargets.Index | DiffTargets.WorkingDirectory); - - return changes.Count; - } -} diff --git a/src/GitVersion.LibGit2Sharp/Git/GitRepository.mutating.cs b/src/GitVersion.LibGit2Sharp/Git/GitRepository.mutating.cs deleted file mode 100644 index 77103482d0..0000000000 --- a/src/GitVersion.LibGit2Sharp/Git/GitRepository.mutating.cs +++ /dev/null @@ -1,134 +0,0 @@ -using GitVersion.Extensions; -using GitVersion.Helpers; -using LibGit2Sharp; -using LibGit2Sharp.Handlers; - -namespace GitVersion.Git; - -internal partial class GitRepository(ILogger logger) : IMutatingGitRepository -{ - private readonly ILogger logger = logger.NotNull(); - - public void Clone(string? sourceUrl, string? workdirPath, AuthenticationInfo auth) - { - try - { - var path = Repository.Clone(sourceUrl, workdirPath, GetCloneOptions(auth)); - this.logger.LogInformation("Returned path after repository clone: {Path}", path); - } - catch (LibGit2Sharp.LockedFileException ex) - { - throw new LockedFileException(ex); - } - catch (LibGit2SharpException ex) - { - var message = ex.Message; - if (message.Contains("401")) - { - throw new InvalidOperationException("Unauthorized: Incorrect username/password", ex); - } - - if (message.Contains("403")) - { - throw new InvalidOperationException("Forbidden: Possibly Incorrect username/password", ex); - } - - if (message.Contains("404")) - { - throw new InvalidOperationException("Not found: The repository was not found", ex); - } - - throw new InvalidOperationException("There was an unknown problem with the Git repository you provided", ex); - } - } - public void Checkout(string commitOrBranchSpec) => - RepositoryExtensions.RunSafe(() => - Commands.Checkout(RepositoryInstance, commitOrBranchSpec)); - public void Fetch(string remote, IEnumerable refSpecs, AuthenticationInfo auth, string? logMessage) => - RepositoryExtensions.RunSafe(() => - Commands.Fetch((Repository)RepositoryInstance, remote, refSpecs, GetFetchOptions(auth), logMessage)); - public void CreateBranchForPullRequestBranch(AuthenticationInfo auth) => RepositoryExtensions.RunSafe(() => - { - this.logger.LogInformation("Fetching remote refs to see if there is a pull request ref"); - - // FIX ME: What to do when Tip is null? - if (Head.Tip == null) - { - return; - } - - var headTipSha = Head.Tip.Sha; - var remote = RepositoryInstance.Network.Remotes.Single(); - var reference = GetPullRequestReference(auth, remote, headTipSha); - var canonicalName = reference.CanonicalName; - var referenceName = ReferenceName.Parse(reference.CanonicalName); - this.logger.LogInformation("Found remote tip '{CanonicalName}' pointing at the commit '{HeadTipSha}'.", canonicalName, headTipSha); - - if (referenceName.IsTag) - { - this.logger.LogInformation("Checking out tag '{CanonicalName}'", canonicalName); - Checkout(reference.Target.Sha); - } - else if (referenceName.IsPullRequest) - { - var fakeBranchName = canonicalName.Replace("refs/pull/", "refs/heads/pull/").Replace("refs/pull-requests/", "refs/heads/pull-requests/"); - - this.logger.LogInformation("Creating fake local branch '{FakeBranchName}'.", fakeBranchName); - References.Add(fakeBranchName, headTipSha); - - this.logger.LogInformation("Checking local branch '{FakeBranchName}' out.", fakeBranchName); - Checkout(fakeBranchName); - } - else - { - var message = $"Remote tip '{canonicalName}' from remote '{remote.Url}' doesn't look like a valid pull request."; - throw new WarningException(message); - } - }); - private DirectReference GetPullRequestReference(AuthenticationInfo auth, LibGit2Sharp.Remote remote, string headTipSha) - { - var network = RepositoryInstance.Network; - var credentialsProvider = GetCredentialsProvider(auth); - var remoteTips = (credentialsProvider != null - ? network.ListReferences(remote, credentialsProvider) - : network.ListReferences(remote)) - .Select(r => r.ResolveToDirectReference()).ToList(); - - var remoteRefsList = string.Join(FileSystemHelper.Path.NewLine, remoteTips.Select(r => r.CanonicalName)); - this.logger.LogInformation(""" - Remote Refs: - {RemoteRefsList} - """, remoteRefsList); - var refs = remoteTips.Where(r => r.TargetIdentifier == headTipSha).ToList(); - - switch (refs.Count) - { - case 0: - { - var message = $"Couldn't find any remote tips from remote '{remote.Url}' pointing at the commit '{headTipSha}'."; - throw new WarningException(message); - } - case > 1: - { - var names = string.Join(", ", refs.Select(r => r.CanonicalName)); - var message = $"Found more than one remote tip from remote '{remote.Url}' pointing at the commit '{headTipSha}'. Unable to determine which one to use ({names})."; - throw new WarningException(message); - } - } - - return refs[0]; - } - private static FetchOptions GetFetchOptions(AuthenticationInfo auth) => - new() { CredentialsProvider = GetCredentialsProvider(auth) }; - private static CloneOptions GetCloneOptions(AuthenticationInfo auth) => - new() { Checkout = false, FetchOptions = { CredentialsProvider = GetCredentialsProvider(auth) } }; - private static CredentialsHandler? GetCredentialsProvider(AuthenticationInfo auth) - { - if (!auth.Username.IsNullOrWhiteSpace()) - { - return (_, _, _) => new UsernamePasswordCredentials { Username = auth.Username, Password = auth.Password ?? string.Empty }; - } - - return null; - } -} diff --git a/src/GitVersion.LibGit2Sharp/Git/GitRepositoryCache.cs b/src/GitVersion.LibGit2Sharp/Git/GitRepositoryCache.cs deleted file mode 100644 index 01fe95c837..0000000000 --- a/src/GitVersion.LibGit2Sharp/Git/GitRepositoryCache.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Concurrent; -using LibGit2Sharp; - -namespace GitVersion.Git; - -internal class GitRepositoryCache -{ - private readonly ConcurrentDictionary cachedBranches = new(); - private readonly ConcurrentDictionary cachedCommits = new(); - private readonly ConcurrentDictionary cachedTags = new(); - private readonly ConcurrentDictionary cachedRemotes = new(); - private readonly ConcurrentDictionary cachedReferences = new(); - private readonly ConcurrentDictionary cachedRefSpecs = new(); - - public Branch GetOrWrap(LibGit2Sharp.Branch innerBranch, Diff repoDiff) - { - var cacheKey = innerBranch.Tip is null - ? $"{innerBranch.RemoteName}/{innerBranch.CanonicalName}" - : $"{innerBranch.RemoteName}/{innerBranch.CanonicalName}@{innerBranch.Tip.Sha}"; - return this.cachedBranches.GetOrAdd(cacheKey, _ => new Branch(innerBranch, repoDiff, this)); - } - - public Commit GetOrWrap(LibGit2Sharp.Commit innerCommit, Diff repoDiff) - => this.cachedCommits.GetOrAdd(innerCommit.Sha, _ => new Commit(innerCommit, repoDiff, this)); - - public Tag GetOrWrap(LibGit2Sharp.Tag innerTag, Diff repoDiff) - => this.cachedTags.GetOrAdd(innerTag.CanonicalName, _ => new Tag(innerTag, repoDiff, this)); - - public Remote GetOrWrap(LibGit2Sharp.Remote innerRemote) - => this.cachedRemotes.GetOrAdd(innerRemote.Name, _ => new Remote(innerRemote, this)); - - public Reference GetOrWrap(LibGit2Sharp.Reference innerReference) - => this.cachedReferences.GetOrAdd(innerReference.CanonicalName, _ => new Reference(innerReference)); - - public RefSpec GetOrWrap(LibGit2Sharp.RefSpec innerRefSpec) - => this.cachedRefSpecs.GetOrAdd(innerRefSpec.Specification, _ => new RefSpec(innerRefSpec)); -} diff --git a/src/GitVersion.LibGit2Sharp/Git/GitRepositoryInfo.cs b/src/GitVersion.LibGit2Sharp/Git/GitRepositoryInfo.cs deleted file mode 100644 index 046351d5d9..0000000000 --- a/src/GitVersion.LibGit2Sharp/Git/GitRepositoryInfo.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System.IO.Abstractions; -using GitVersion.Extensions; -using GitVersion.Helpers; -using LibGit2Sharp; - -namespace GitVersion.Git; - -public class GitRepositoryInfo : IGitRepositoryInfo -{ - private static readonly char[] DirectorySeparators = ['/', '\\']; - - private readonly IFileSystem fileSystem; - private readonly GitVersionOptions gitVersionOptions; - - private readonly Lazy dynamicGitRepositoryPath; - private readonly Lazy dotGitDirectory; - private readonly Lazy gitRootPath; - private readonly Lazy projectRootDirectory; - - public GitRepositoryInfo(IFileSystem fileSystem, IOptions options) - { - this.fileSystem = fileSystem.NotNull(); - this.gitVersionOptions = options.NotNull().Value; - - this.dynamicGitRepositoryPath = new(GetDynamicGitRepositoryPath); - this.dotGitDirectory = new(GetDotGitDirectory); - this.gitRootPath = new(GetGitRootPath); - this.projectRootDirectory = new(GetProjectRootDirectory); - } - - public string? DynamicGitRepositoryPath => this.dynamicGitRepositoryPath.Value; - public string? DotGitDirectory => this.dotGitDirectory.Value; - public string? GitRootPath => this.gitRootPath.Value; - public string? ProjectRootDirectory => this.projectRootDirectory.Value; - - private string? GetDynamicGitRepositoryPath() - { - var repositoryInfo = this.gitVersionOptions.RepositoryInfo; - if (repositoryInfo.TargetUrl.IsNullOrWhiteSpace()) - { - return null; - } - - var targetUrl = repositoryInfo.TargetUrl; - var clonePath = repositoryInfo.ClonePath; - - var userTemp = clonePath ?? FileSystemHelper.Path.GetTempPath(); - var repositoryName = targetUrl.Split(DirectorySeparators)[^1].Replace(".git", string.Empty); - var possiblePath = FileSystemHelper.Path.Combine(userTemp, repositoryName); - - // Verify that the existing directory is ok for us to use - if (this.fileSystem.Directory.Exists(possiblePath) && !GitRepoHasMatchingRemote(possiblePath, targetUrl)) - { - var i = 1; - var originalPath = possiblePath; - bool possiblePathExists; - do - { - possiblePath = $"{originalPath}_{i++}"; - possiblePathExists = this.fileSystem.Directory.Exists(possiblePath); - } while (possiblePathExists && !GitRepoHasMatchingRemote(possiblePath, targetUrl)); - } - - var repositoryPath = FileSystemHelper.Path.Combine(possiblePath, ".git"); - return repositoryPath; - } - - private string? GetDotGitDirectory() - { - var gitDirectory = !DynamicGitRepositoryPath.IsNullOrWhiteSpace() - ? DynamicGitRepositoryPath - : Repository.Discover(this.gitVersionOptions.WorkingDirectory); - - gitDirectory = gitDirectory?.TrimEnd('/', '\\'); - if (gitDirectory.IsNullOrEmpty()) - { - throw new DirectoryNotFoundException("Cannot find the .git directory"); - } - - var directoryInfo = this.fileSystem.Directory.GetParent(gitDirectory) ?? throw new DirectoryNotFoundException("Cannot find the .git directory"); - return gitDirectory.Contains(FileSystemHelper.Path.Combine(".git", "worktrees")) - ? this.fileSystem.Directory.GetParent(directoryInfo.FullName)?.FullName - : gitDirectory; - } - - private string GetProjectRootDirectory() - { - if (!DynamicGitRepositoryPath.IsNullOrWhiteSpace()) - { - return this.gitVersionOptions.WorkingDirectory; - } - - var gitDirectory = Repository.Discover(this.gitVersionOptions.WorkingDirectory); - - if (gitDirectory.IsNullOrEmpty()) - { - throw new DirectoryNotFoundException("Cannot find the .git directory"); - } - - using var repo = new Repository(gitDirectory); - return repo.Info.WorkingDirectory; - } - - private string? GetGitRootPath() - { - var isDynamicRepo = !DynamicGitRepositoryPath.IsNullOrWhiteSpace(); - var rootDirectory = isDynamicRepo ? DotGitDirectory : ProjectRootDirectory; - - return rootDirectory; - } - - private static bool GitRepoHasMatchingRemote(string possiblePath, string targetUrl) - { - try - { - using var gitRepository = new Repository(possiblePath); - return gitRepository.Network.Remotes.Any(r => r.Url == targetUrl); - } - catch (Exception) - { - return false; - } - } -} diff --git a/src/GitVersion.LibGit2Sharp/Git/ObjectId.cs b/src/GitVersion.LibGit2Sharp/Git/ObjectId.cs deleted file mode 100644 index 0d924f59af..0000000000 --- a/src/GitVersion.LibGit2Sharp/Git/ObjectId.cs +++ /dev/null @@ -1,29 +0,0 @@ -using GitVersion.Extensions; -using GitVersion.Helpers; - -namespace GitVersion.Git; - -internal sealed class ObjectId : IObjectId -{ - private static readonly LambdaEqualityHelper equalityHelper = new(x => x.Sha); - private static readonly LambdaKeyComparer comparerHelper = new(x => x.Sha); - - private readonly LibGit2Sharp.ObjectId innerObjectId; - - internal ObjectId(LibGit2Sharp.ObjectId objectId) => - this.innerObjectId = objectId.NotNull(); - - public ObjectId(string sha) : this(new LibGit2Sharp.ObjectId(sha)) - { - } - - public int CompareTo(IObjectId? other) => comparerHelper.Compare(this, other); - public bool Equals(IObjectId? other) => equalityHelper.Equals(this, other); - public override bool Equals(object? obj) => Equals(obj as IObjectId); - public override int GetHashCode() => equalityHelper.GetHashCode(this); - public override string ToString() => ToString(7); - public static implicit operator LibGit2Sharp.ObjectId(ObjectId d) => d.innerObjectId; - public string Sha => this.innerObjectId.Sha; - - public string ToString(int prefixLength) => this.innerObjectId.ToString(prefixLength); -} diff --git a/src/GitVersion.LibGit2Sharp/Git/RefSpec.cs b/src/GitVersion.LibGit2Sharp/Git/RefSpec.cs deleted file mode 100644 index 26f687b126..0000000000 --- a/src/GitVersion.LibGit2Sharp/Git/RefSpec.cs +++ /dev/null @@ -1,22 +0,0 @@ -using GitVersion.Extensions; -using GitVersion.Helpers; - -namespace GitVersion.Git; - -internal sealed class RefSpec : IRefSpec -{ - private static readonly LambdaEqualityHelper equalityHelper = new(x => x.Specification); - private static readonly LambdaKeyComparer comparerHelper = new(x => x.Specification); - private readonly LibGit2Sharp.RefSpec innerRefSpec; - - internal RefSpec(LibGit2Sharp.RefSpec refSpec) => this.innerRefSpec = refSpec.NotNull(); - public int CompareTo(IRefSpec? other) => comparerHelper.Compare(this, other); - public string Specification => this.innerRefSpec.Specification; - public RefSpecDirection Direction => (RefSpecDirection)this.innerRefSpec.Direction; - public string Source => this.innerRefSpec.Source; - public string Destination => this.innerRefSpec.Destination; - public bool Equals(IRefSpec? other) => equalityHelper.Equals(this, other); - public override bool Equals(object? obj) => Equals(obj as IRefSpec); - public override int GetHashCode() => equalityHelper.GetHashCode(this); - public override string ToString() => Specification; -} diff --git a/src/GitVersion.LibGit2Sharp/Git/RefSpecCollection.cs b/src/GitVersion.LibGit2Sharp/Git/RefSpecCollection.cs deleted file mode 100644 index 3ae987a10f..0000000000 --- a/src/GitVersion.LibGit2Sharp/Git/RefSpecCollection.cs +++ /dev/null @@ -1,17 +0,0 @@ -using GitVersion.Extensions; - -namespace GitVersion.Git; - -internal sealed class RefSpecCollection : IRefSpecCollection -{ - private readonly Lazy> refSpecs; - - internal RefSpecCollection(LibGit2Sharp.RefSpecCollection innerCollection, GitRepositoryCache repositoryCache) - { - innerCollection = innerCollection.NotNull(); - this.refSpecs = new Lazy>(() => [.. innerCollection.Select(repositoryCache.GetOrWrap)]); - } - - public IEnumerator GetEnumerator() => this.refSpecs.Value.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} diff --git a/src/GitVersion.LibGit2Sharp/Git/Reference.cs b/src/GitVersion.LibGit2Sharp/Git/Reference.cs deleted file mode 100644 index 34ba29a32d..0000000000 --- a/src/GitVersion.LibGit2Sharp/Git/Reference.cs +++ /dev/null @@ -1,34 +0,0 @@ -using GitVersion.Extensions; -using GitVersion.Helpers; -using LibGit2Sharp; - -namespace GitVersion.Git; - -internal sealed class Reference : IReference -{ - private static readonly LambdaEqualityHelper equalityHelper = new(x => x.Name.Canonical); - private static readonly LambdaKeyComparer comparerHelper = new(x => x.Name.Canonical); - internal readonly LibGit2Sharp.Reference innerReference; - - internal Reference(LibGit2Sharp.Reference reference) - { - this.innerReference = reference.NotNull(); - Name = new ReferenceName(reference.CanonicalName); - - if (reference is DirectReference) - { - ReferenceTargetId = new ObjectId(reference.TargetIdentifier); - } - } - - public ReferenceName Name { get; } - public IObjectId? ReferenceTargetId { get; } - public int CompareTo(IReference? other) => comparerHelper.Compare(this, other); - public override bool Equals(object? obj) => Equals(obj as IReference); - public bool Equals(IReference? other) => equalityHelper.Equals(this, other); - public override int GetHashCode() => equalityHelper.GetHashCode(this); - public override string ToString() => Name.ToString(); - public string TargetIdentifier => this.innerReference.TargetIdentifier; - public static implicit operator LibGit2Sharp.Reference(Reference d) - => d.innerReference; -} diff --git a/src/GitVersion.LibGit2Sharp/Git/ReferenceCollection.cs b/src/GitVersion.LibGit2Sharp/Git/ReferenceCollection.cs deleted file mode 100644 index 5355410b86..0000000000 --- a/src/GitVersion.LibGit2Sharp/Git/ReferenceCollection.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using GitVersion.Extensions; - -namespace GitVersion.Git; - -internal sealed class ReferenceCollection : IReferenceCollection -{ - private readonly LibGit2Sharp.ReferenceCollection innerCollection; - private readonly GitRepositoryCache repositoryCache; - private Lazy> references; - - internal ReferenceCollection(LibGit2Sharp.ReferenceCollection collection, GitRepositoryCache repositoryCache) - { - this.innerCollection = collection.NotNull(); - this.repositoryCache = repositoryCache.NotNull(); - InitializeReferencesLazy(); - } - - public IEnumerator GetEnumerator() => this.references.Value.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - public IReference? this[string name] - { - get - { - var reference = this.innerCollection[name]; - return reference is null ? null : this.repositoryCache.GetOrWrap(reference); - } - } - - public IReference? this[ReferenceName referenceName] => this[referenceName.Canonical]; - - public IReference? Head => this["HEAD"]; - - public IEnumerable FromGlob(string prefix) - => this.innerCollection.FromGlob(prefix).Select(reference => this.repositoryCache.GetOrWrap(reference)); - - public void Add(string name, string canonicalRefNameOrObject, bool allowOverwrite = false) => - RepositoryExtensions.RunSafe(() => - { - this.innerCollection.Add(name, canonicalRefNameOrObject, allowOverwrite); - InitializeReferencesLazy(); - }); - - public void UpdateTarget(IReference directRef, IObjectId targetId) => - RepositoryExtensions.RunSafe(() => - { - this.innerCollection.UpdateTarget((Reference)directRef, (ObjectId)targetId); - InitializeReferencesLazy(); - }); - - [MemberNotNull(nameof(references))] - private void InitializeReferencesLazy() - => this.references = new Lazy>(() => [.. this.innerCollection.Select(this.repositoryCache.GetOrWrap)]); -} diff --git a/src/GitVersion.LibGit2Sharp/Git/Remote.cs b/src/GitVersion.LibGit2Sharp/Git/Remote.cs deleted file mode 100644 index 02a079ceaf..0000000000 --- a/src/GitVersion.LibGit2Sharp/Git/Remote.cs +++ /dev/null @@ -1,41 +0,0 @@ -using GitVersion.Extensions; -using GitVersion.Helpers; - -namespace GitVersion.Git; - -internal sealed class Remote : IRemote -{ - private static readonly LambdaEqualityHelper equalityHelper = new(x => x.Name); - private static readonly LambdaKeyComparer comparerHelper = new(x => x.Name); - - private readonly LibGit2Sharp.Remote innerRemote; - private readonly GitRepositoryCache repositoryCache; - - internal Remote(LibGit2Sharp.Remote remote, GitRepositoryCache repositoryCache) - { - this.innerRemote = remote.NotNull(); - this.repositoryCache = repositoryCache.NotNull(); - } - - public int CompareTo(IRemote? other) => comparerHelper.Compare(this, other); - public string Name => this.innerRemote.Name; - public string Url => this.innerRemote.Url; - - private IEnumerable RefSpecs - { - get - { - var refSpecs = this.innerRemote.RefSpecs; - return refSpecs is null - ? [] - : new RefSpecCollection((LibGit2Sharp.RefSpecCollection)refSpecs, this.repositoryCache); - } - } - - public IEnumerable FetchRefSpecs => RefSpecs.Where(x => x.Direction == RefSpecDirection.Fetch); - public IEnumerable PushRefSpecs => RefSpecs.Where(x => x.Direction == RefSpecDirection.Push); - public bool Equals(IRemote? other) => equalityHelper.Equals(this, other); - public override bool Equals(object? obj) => Equals(obj as IRemote); - public override int GetHashCode() => equalityHelper.GetHashCode(this); - public override string ToString() => Name; -} diff --git a/src/GitVersion.LibGit2Sharp/Git/RemoteCollection.cs b/src/GitVersion.LibGit2Sharp/Git/RemoteCollection.cs deleted file mode 100644 index 22263bb833..0000000000 --- a/src/GitVersion.LibGit2Sharp/Git/RemoteCollection.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using GitVersion.Extensions; - -namespace GitVersion.Git; - -internal sealed class RemoteCollection : IRemoteCollection -{ - private readonly LibGit2Sharp.RemoteCollection innerCollection; - private readonly GitRepositoryCache repositoryCache; - private Lazy> remotes; - - internal RemoteCollection(LibGit2Sharp.RemoteCollection collection, GitRepositoryCache repositoryCache) - { - this.innerCollection = collection.NotNull(); - this.repositoryCache = repositoryCache.NotNull(); - InitializeRemotesLazy(); - } - - public IEnumerator GetEnumerator() => this.remotes.Value.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - public IRemote? this[string name] - { - get - { - var remote = this.innerCollection[name]; - return remote is null ? null : this.repositoryCache.GetOrWrap(remote); - } - } - - public void Remove(string remoteName) => - RepositoryExtensions.RunSafe(() => - { - this.innerCollection.Remove(remoteName); - InitializeRemotesLazy(); - }); - - public void Update(string remoteName, string refSpec) => - RepositoryExtensions.RunSafe(() => - { - this.innerCollection.Update(remoteName, r => r.FetchRefSpecs.Add(refSpec)); - InitializeRemotesLazy(); - }); - - [MemberNotNull(nameof(remotes))] - private void InitializeRemotesLazy() - => this.remotes = new Lazy>(() => [.. this.innerCollection.Select(this.repositoryCache.GetOrWrap)]); -} diff --git a/src/GitVersion.LibGit2Sharp/Git/RepositoryExtensions.cs b/src/GitVersion.LibGit2Sharp/Git/RepositoryExtensions.cs deleted file mode 100644 index f98e993fa2..0000000000 --- a/src/GitVersion.LibGit2Sharp/Git/RepositoryExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace GitVersion.Git; - -internal static class RepositoryExtensions -{ - internal static void RunSafe(Action operation) - { - try - { - operation(); - } - catch (LibGit2Sharp.LockedFileException ex) - { - // Wrap this exception so that callers that want to catch it doesn't need to take a dependency on LibGit2Sharp. - throw new LockedFileException(ex); - } - } - - internal static T RunSafe(Func operation) - { - try - { - return operation(); - } - catch (LibGit2Sharp.LockedFileException ex) - { - // Wrap this exception so that callers that want to catch it doesn't need to take a dependency on LibGit2Sharp. - throw new LockedFileException(ex); - } - } -} diff --git a/src/GitVersion.LibGit2Sharp/Git/Tag.cs b/src/GitVersion.LibGit2Sharp/Git/Tag.cs deleted file mode 100644 index 6926c315f8..0000000000 --- a/src/GitVersion.LibGit2Sharp/Git/Tag.cs +++ /dev/null @@ -1,46 +0,0 @@ -using GitVersion.Extensions; -using GitVersion.Helpers; -using LibGit2Sharp; - -namespace GitVersion.Git; - -internal sealed class Tag : ITag -{ - private static readonly LambdaEqualityHelper equalityHelper = new(x => x.Name.Canonical); - private static readonly LambdaKeyComparer comparerHelper = new(x => x.Name.Canonical); - private readonly LibGit2Sharp.Tag innerTag; - private readonly Diff diff; - private readonly Lazy commitLazy; - private readonly GitRepositoryCache repositoryCache; - - internal Tag(LibGit2Sharp.Tag tag, Diff diff, GitRepositoryCache repositoryCache) - { - this.innerTag = tag.NotNull(); - this.diff = diff.NotNull(); - this.repositoryCache = repositoryCache.NotNull(); - this.commitLazy = new(PeeledTargetCommit); - Name = new(this.innerTag.CanonicalName); - } - - public ReferenceName Name { get; } - public int CompareTo(ITag? other) => comparerHelper.Compare(this, other); - public string TargetSha => this.innerTag.Target.Sha; - public ICommit Commit => this.commitLazy.Value.NotNull(); - - private ICommit? PeeledTargetCommit() - { - var target = this.innerTag.Target; - - while (target is TagAnnotation annotation) - { - target = annotation.Target; - } - - return target is LibGit2Sharp.Commit commit ? this.repositoryCache.GetOrWrap(commit, this.diff) : null; - } - - public bool Equals(ITag? other) => equalityHelper.Equals(this, other); - public override bool Equals(object? obj) => Equals(obj as ITag); - public override int GetHashCode() => equalityHelper.GetHashCode(this); - public override string ToString() => Name.ToString(); -} diff --git a/src/GitVersion.LibGit2Sharp/Git/TagCollection.cs b/src/GitVersion.LibGit2Sharp/Git/TagCollection.cs deleted file mode 100644 index 8ff39d4b4a..0000000000 --- a/src/GitVersion.LibGit2Sharp/Git/TagCollection.cs +++ /dev/null @@ -1,20 +0,0 @@ -using GitVersion.Extensions; - -namespace GitVersion.Git; - -internal sealed class TagCollection : ITagCollection -{ - private readonly Lazy> tags; - - internal TagCollection(LibGit2Sharp.TagCollection collection, LibGit2Sharp.Diff diff, GitRepositoryCache repositoryCache) - { - collection.NotNull(); - diff.NotNull(); - repositoryCache.NotNull(); - this.tags = new Lazy>(() => [.. collection.Select(tag => repositoryCache.GetOrWrap(tag, diff))]); - } - - public IEnumerator GetEnumerator() => this.tags.Value.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} diff --git a/src/GitVersion.LibGit2Sharp/Git/TreeChanges.cs b/src/GitVersion.LibGit2Sharp/Git/TreeChanges.cs deleted file mode 100644 index 9ee9385a7e..0000000000 --- a/src/GitVersion.LibGit2Sharp/Git/TreeChanges.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace GitVersion.Git; - -internal sealed class TreeChanges(LibGit2Sharp.TreeChanges innerTreeChanges) : ITreeChanges -{ - private readonly LibGit2Sharp.TreeChanges innerTreeChanges = innerTreeChanges ?? throw new ArgumentNullException(nameof(innerTreeChanges)); - - public IReadOnlyList Paths => [.. this.innerTreeChanges.Select(element => element.Path)]; -} diff --git a/src/GitVersion.LibGit2Sharp/GitVersion.LibGit2Sharp.csproj b/src/GitVersion.LibGit2Sharp/GitVersion.LibGit2Sharp.csproj deleted file mode 100644 index 535770e522..0000000000 --- a/src/GitVersion.LibGit2Sharp/GitVersion.LibGit2Sharp.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/src/GitVersion.LibGit2Sharp/GitVersionLibGit2SharpModule.cs b/src/GitVersion.LibGit2Sharp/GitVersionLibGit2SharpModule.cs deleted file mode 100644 index 9afe62f13b..0000000000 --- a/src/GitVersion.LibGit2Sharp/GitVersionLibGit2SharpModule.cs +++ /dev/null @@ -1,13 +0,0 @@ -using GitVersion.Git; - -namespace GitVersion; - -public class GitVersionLibGit2SharpModule : IGitVersionModule -{ - public void RegisterTypes(IServiceCollection services) - { - services.AddSingleton(); - services.AddSingleton(sp => (IMutatingGitRepository)sp.GetRequiredService()); - services.AddSingleton(); - } -} diff --git a/src/GitVersion.LibGit2Sharp/LibGit2SharpExtensions.cs b/src/GitVersion.LibGit2Sharp/LibGit2SharpExtensions.cs deleted file mode 100644 index f24cd45802..0000000000 --- a/src/GitVersion.LibGit2Sharp/LibGit2SharpExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -using GitVersion.Git; -using LibGit2Sharp; - -namespace GitVersion; - -internal static class LibGit2SharpExtensions -{ - extension(IRepository repository) - { - public IGitRepository ToGitRepository() - { - var gitRepository = new GitRepository(NullLogger.Instance); - gitRepository.DiscoverRepository(repository.Info.Path); - return gitRepository; - } - } -} diff --git a/src/GitVersion.LibGit2Sharp/PublicAPI.Shipped.txt b/src/GitVersion.LibGit2Sharp/PublicAPI.Shipped.txt deleted file mode 100644 index a79087455e..0000000000 --- a/src/GitVersion.LibGit2Sharp/PublicAPI.Shipped.txt +++ /dev/null @@ -1,10 +0,0 @@ -#nullable enable -GitVersion.Git.GitRepositoryInfo -GitVersion.Git.GitRepositoryInfo.DotGitDirectory.get -> string? -GitVersion.Git.GitRepositoryInfo.DynamicGitRepositoryPath.get -> string? -GitVersion.Git.GitRepositoryInfo.GitRepositoryInfo(System.IO.Abstractions.IFileSystem! fileSystem, Microsoft.Extensions.Options.IOptions! options) -> void -GitVersion.Git.GitRepositoryInfo.GitRootPath.get -> string? -GitVersion.Git.GitRepositoryInfo.ProjectRootDirectory.get -> string? -GitVersion.GitVersionLibGit2SharpModule -GitVersion.GitVersionLibGit2SharpModule.GitVersionLibGit2SharpModule() -> void -GitVersion.GitVersionLibGit2SharpModule.RegisterTypes(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> void diff --git a/src/GitVersion.LibGit2Sharp/PublicAPI.Unshipped.txt b/src/GitVersion.LibGit2Sharp/PublicAPI.Unshipped.txt deleted file mode 100644 index 7dc5c58110..0000000000 --- a/src/GitVersion.LibGit2Sharp/PublicAPI.Unshipped.txt +++ /dev/null @@ -1 +0,0 @@ -#nullable enable diff --git a/src/GitVersion.MsBuild.Tests/GitVersion.MsBuild.Tests.csproj b/src/GitVersion.MsBuild.Tests/GitVersion.MsBuild.Tests.csproj index 6411d83886..9cca21378d 100644 --- a/src/GitVersion.MsBuild.Tests/GitVersion.MsBuild.Tests.csproj +++ b/src/GitVersion.MsBuild.Tests/GitVersion.MsBuild.Tests.csproj @@ -6,7 +6,6 @@ - @@ -23,7 +22,6 @@ - diff --git a/src/GitVersion.MsBuild.Tests/Tasks/TestTaskBase.cs b/src/GitVersion.MsBuild.Tests/Tasks/TestTaskBase.cs index 1ba79da7f8..b79e421ce8 100644 --- a/src/GitVersion.MsBuild.Tests/Tasks/TestTaskBase.cs +++ b/src/GitVersion.MsBuild.Tests/Tasks/TestTaskBase.cs @@ -5,8 +5,8 @@ using GitVersion.MsBuild.Tasks; using GitVersion.MsBuild.Tests.Helpers; using GitVersion.Testing.Extensions; +using GitVersion.Testing.Git; using GitVersion.Tests; -using LibGit2Sharp; using Microsoft.Build.Utilities.ProjectCreation; namespace GitVersion.MsBuild.Tests.Tasks; diff --git a/src/GitVersion.Output.Tests/Output/FormatArgumentTests.cs b/src/GitVersion.Output.Tests/Output/FormatArgumentTests.cs index 2b47a52cb1..949832d231 100644 --- a/src/GitVersion.Output.Tests/Output/FormatArgumentTests.cs +++ b/src/GitVersion.Output.Tests/Output/FormatArgumentTests.cs @@ -1,7 +1,7 @@ using GitVersion.Output.OutputGenerator; using GitVersion.Testing.Extensions; +using GitVersion.Testing.Git; using GitVersion.Tests; -using LibGit2Sharp; namespace GitVersion.Output.Tests; diff --git a/src/GitVersion.Testing/Extensions/GitTestExtensions.cs b/src/GitVersion.Testing/Extensions/GitTestExtensions.cs index 98fc81561e..bac2094b3b 100644 --- a/src/GitVersion.Testing/Extensions/GitTestExtensions.cs +++ b/src/GitVersion.Testing/Extensions/GitTestExtensions.cs @@ -1,6 +1,6 @@ using GitVersion.Helpers; +using GitVersion.Testing.Git; using GitVersion.Testing.Internal; -using LibGit2Sharp; namespace GitVersion.Testing.Extensions; diff --git a/src/GitVersion.Testing/Fixtures/BaseGitFlowRepositoryFixture.cs b/src/GitVersion.Testing/Fixtures/BaseGitFlowRepositoryFixture.cs index 25a256c4ad..6d0924bc00 100644 --- a/src/GitVersion.Testing/Fixtures/BaseGitFlowRepositoryFixture.cs +++ b/src/GitVersion.Testing/Fixtures/BaseGitFlowRepositoryFixture.cs @@ -1,6 +1,6 @@ using GitVersion.Helpers; using GitVersion.Testing.Extensions; -using LibGit2Sharp; +using GitVersion.Testing.Git; namespace GitVersion.Testing; diff --git a/src/GitVersion.Testing/Fixtures/LocalRepositoryFixture.cs b/src/GitVersion.Testing/Fixtures/LocalRepositoryFixture.cs index 8431d4435e..7d6e685a83 100644 --- a/src/GitVersion.Testing/Fixtures/LocalRepositoryFixture.cs +++ b/src/GitVersion.Testing/Fixtures/LocalRepositoryFixture.cs @@ -1,4 +1,4 @@ -using LibGit2Sharp; +using GitVersion.Testing.Git; namespace GitVersion.Testing; diff --git a/src/GitVersion.Testing/Fixtures/RemoteRepositoryFixture.cs b/src/GitVersion.Testing/Fixtures/RemoteRepositoryFixture.cs index 2312d47f62..234e5a1f4e 100644 --- a/src/GitVersion.Testing/Fixtures/RemoteRepositoryFixture.cs +++ b/src/GitVersion.Testing/Fixtures/RemoteRepositoryFixture.cs @@ -1,4 +1,4 @@ -using LibGit2Sharp; +using GitVersion.Testing.Git; namespace GitVersion.Testing; diff --git a/src/GitVersion.Testing/Fixtures/RepositoryFixtureBase.cs b/src/GitVersion.Testing/Fixtures/RepositoryFixtureBase.cs index 7d7dd27693..a072dfd2a3 100644 --- a/src/GitVersion.Testing/Fixtures/RepositoryFixtureBase.cs +++ b/src/GitVersion.Testing/Fixtures/RepositoryFixtureBase.cs @@ -1,6 +1,6 @@ using GitVersion.Helpers; using GitVersion.Testing.Extensions; -using LibGit2Sharp; +using GitVersion.Testing.Git; using Shouldly; namespace GitVersion.Testing; @@ -75,7 +75,7 @@ public void Remove(string branch) SequenceDiagram.Destroy(branch, sourceBranch); } - public static void Init(string path, string branchName = "main") => GitTestExtensions.ExecuteGitCmd($"init {path} -b {branchName}", "."); + public static void Init(string path, string branchName = "main") => Repository.Init(path, initialBranch: branchName); public string MakeATaggedCommit(string tag) { diff --git a/src/GitVersion.Testing/Generate.cs b/src/GitVersion.Testing/Generate.cs index 4dbc87b271..a531cc6deb 100644 --- a/src/GitVersion.Testing/Generate.cs +++ b/src/GitVersion.Testing/Generate.cs @@ -1,4 +1,4 @@ -using LibGit2Sharp; +using GitVersion.Testing.Git; namespace GitVersion.Testing; @@ -8,7 +8,7 @@ namespace GitVersion.Testing; public static class Generate { /// - /// Create a libgit2sharp signature at VirtualTime.Now + /// Create a Git signature at VirtualTime.Now /// public static Signature SignatureNow() { @@ -17,7 +17,7 @@ public static Signature SignatureNow() } /// - /// Creates a libgit2sharp signature at the specified time + /// Creates a Git signature at the specified time /// private static Signature Signature(DateTimeOffset dateTimeOffset) => new("A. U. Thor", "thor@valhalla.asgard.com", dateTimeOffset); } diff --git a/src/GitVersion.Testing/Git/Repository.cs b/src/GitVersion.Testing/Git/Repository.cs new file mode 100644 index 0000000000..46a95705e1 --- /dev/null +++ b/src/GitVersion.Testing/Git/Repository.cs @@ -0,0 +1,568 @@ +using System.Globalization; + +namespace GitVersion.Testing.Git; + +/// +/// Repository arrangement for integration tests. Every operation uses Git itself, +/// independently of the managed reader under test. This deliberately exposes only +/// the small fixture surface needed by the existing scenario builders. +/// +public sealed class Repository : IRepository +{ + private readonly List> readers = []; + private readonly Dictionary commits = new(StringComparer.Ordinal); + public Repository(string path) + { + DirectoryPath = Path.GetFullPath(path); + Info = new(this); + Config = new(this); + Branches = new(this); + Tags = new(this); + Refs = new(this); + Network = new(this); + ObjectDatabase = new(this); + Worktrees = new(this); + Diff = new(this); + } + + internal string DirectoryPath { get; } + public RepositoryInformation Info { get; } + public Configuration Config { get; } + public BranchCollection Branches { get; } + public TagCollection Tags { get; } + public ReferenceCollection Refs { get; } + public Network Network { get; } + public ObjectDatabase ObjectDatabase { get; } + public WorktreeCollection Worktrees { get; } + public Diff Diff { get; } + public Branch Head => new(this, Run(false, "symbolic-ref", "-q", "HEAD").Trim() is { Length: > 0 } name ? name : "(no branch)"); + public bool IsHeadDetached => Head.CanonicalName == "(no branch)"; + public IEnumerable Commits => Log("HEAD"); + + public string Run(params string[] arguments) => Run(true, arguments); + + internal void Observe(GitVersion.Git.IGitRepository reader) => this.readers.Add(new(reader)); + + private void RefreshReaders() + { + foreach (var weak in this.readers) + { + if (weak.TryGetTarget(out var reader)) + { + reader.DiscoverRepository(Info.WorkingDirectory); + } + } + } + + internal string Run(bool checkExitCode, params string[] arguments) + { + var output = Execute(DirectoryPath, checkExitCode, null, null, arguments); + var readOnly = arguments[0] is "rev-parse" or "cat-file" or "show" or "rev-list" or "for-each-ref" or "show-ref" or "ls-remote" or "diff" + || arguments[0] == "symbolic-ref" && arguments.Length > 1 && arguments[1] == "-q"; + if (!readOnly) + { + RefreshReaders(); + } + return output; + } + + public string RunWithSignature(Signature author, Signature committer, string? input, params string[] arguments) + { + var output = Execute(DirectoryPath, true, (author, committer), input, arguments); + RefreshReaders(); + return output; + } + + internal static string Execute(string directory, bool checkExitCode, (Signature Author, Signature Committer)? signatures, string? input, params string[] arguments) + { + var start = new ProcessStartInfo("git") + { + WorkingDirectory = directory, + RedirectStandardOutput = true, + RedirectStandardError = true, + RedirectStandardInput = input != null, + UseShellExecute = false + }; + foreach (var name in start.Environment.Keys.Where(name => name.StartsWith("GIT_", StringComparison.Ordinal)).ToArray()) + { + start.Environment.Remove(name); + } + start.Environment["GIT_CONFIG_NOSYSTEM"] = "1"; + start.Environment["GIT_CONFIG_GLOBAL"] = OperatingSystem.IsWindows() ? "NUL" : "/dev/null"; + start.Environment["GIT_TERMINAL_PROMPT"] = "0"; + start.Environment["LC_ALL"] = "C"; + start.ArgumentList.Add("-c"); + start.ArgumentList.Add("commit.gpgsign=false"); + start.ArgumentList.Add("-c"); + start.ArgumentList.Add("tag.gpgsign=false"); + start.ArgumentList.Add("-c"); + start.ArgumentList.Add("gc.auto=0"); + start.ArgumentList.Add("-c"); + start.ArgumentList.Add("protocol.file.allow=always"); + foreach (var argument in arguments) + { + start.ArgumentList.Add(argument); + } + + if (signatures is { } identity) + { + SetSignature("AUTHOR", identity.Author); + SetSignature("COMMITTER", identity.Committer); + } + using var process = Process.Start(start) ?? throw new InvalidOperationException("Cannot start Git for repository fixture."); + // Parallel NUnit workers block while arranging repositories. On .NET 10, + // pipe ReadToEndAsync completion also needs thread-pool workers, causing + // starvation across thousands of short Git invocations. Drain both pipes + // on dedicated readers so large stderr/stdout cannot deadlock either. + var stdout = ReadOnThread(process.StandardOutput); + var stderr = ReadOnThread(process.StandardError); + if (input != null) + { + process.StandardInput.Write(input); + process.StandardInput.Close(); + } + process.WaitForExit(); + var output = stdout.GetAwaiter().GetResult(); + var error = stderr.GetAwaiter().GetResult(); + if (checkExitCode && process.ExitCode != 0) + { + throw new InvalidOperationException($"git {string.Join(' ', arguments)} failed ({process.ExitCode}) in '{directory}': {error}"); + } + return process.ExitCode == 0 ? output : string.Empty; + + void SetSignature(string kind, Signature signature) + { + start.Environment[$"GIT_{kind}_NAME"] = signature.Name; + start.Environment[$"GIT_{kind}_EMAIL"] = signature.Email; + start.Environment[$"GIT_{kind}_DATE"] = signature.When.ToString("yyyy-MM-dd'T'HH:mm:sszzz", CultureInfo.InvariantCulture); + } + + static Task ReadOnThread(StreamReader reader) + { + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + new Thread(() => + { + try { completion.SetResult(reader.ReadToEnd()); } + catch (Exception exception) { completion.SetException(exception); } + }) + { IsBackground = true }.Start(); + return completion.Task; + } + } + + public static string Init(string path, bool isBare = false, string initialBranch = "main") + { + Execute(SysEnv.CurrentDirectory, true, null, null, "init", "-b", initialBranch, isBare ? "--bare" : "--quiet", path); + return Path.GetFullPath(isBare ? path : Path.Combine(path, ".git")) + Path.DirectorySeparatorChar; + } + + public static string Clone(string source, string destination) + { + Execute(SysEnv.CurrentDirectory, true, null, null, "clone", source, destination); + return Path.Combine(destination, ".git"); + } + + public Branch CreateBranch(string name) => CreateBranch(name, "HEAD"); + public Branch CreateBranch(string name, Commit commit) => CreateBranch(name, commit.Sha); + public Branch CreateBranch(string name, string target) + { + Run("branch", name, target); + return new(this, name.StartsWith("refs/", StringComparison.Ordinal) ? name : "refs/heads/" + name); + } + + public Commit Commit(string message, Signature author, Signature committer, CommitOptions? options = null) + { + var arguments = new List { "commit", "--quiet", "--allow-empty", "--allow-empty-message", "-m", message }; + if (options?.AmendPreviousCommit == true) + { + arguments.Add("--amend"); + } + + RunWithSignature(author, committer, null, [.. arguments]); + return Lookup("HEAD")!; + } + + public void Merge(string target, Signature signature, MergeOptions? options = null) => RunWithSignature(signature, signature, null, "merge", "--no-edit", options?.FastForwardStrategy == FastForwardStrategy.NoFastForward ? "--no-ff" : "--ff", target); + public void Merge(Branch target, Signature signature, MergeOptions? options = null) => Merge(target.CanonicalName, signature, options); + public void Merge(Commit target, Signature signature, MergeOptions? options = null) => Merge(target.Sha, signature, options); + + public Tag ApplyTag(string name) => Tags.Add(name, "HEAD"); + public Tag ApplyTag(string name, string target) => Tags.Add(name, target); + public Tag ApplyTag(string name, Signature signature, string message) => Tags.Add(name, Head.Tip, signature, message); + public Tag ApplyTag(string name, string target, Signature signature, string message) => Tags.Add(name, target, signature, message); + + public GitObject? Lookup(string revision) + { + var sha = Run(false, "rev-parse", "--verify", revision).Trim(); + if (sha.Length == 0) + { + return null; + } + + return Run(false, "cat-file", "-t", sha).Trim() switch + { + "commit" => GetCommit(sha), + "tree" => new Tree(this, sha), + "" => null, + _ => new GitObject(this, sha) + }; + } + public T? Lookup(string revision) where T : GitObject + { + if (typeof(T) != typeof(Commit)) + { + return Lookup(revision) as T; + } + + var sha = Run(false, "rev-parse", "--verify", revision + "^{commit}").Trim(); + return sha.Length == 0 ? null : (T)(GitObject)GetCommit(sha); + } + internal Commit GetCommit(string sha) + { + if (this.commits.TryGetValue(sha, out var commit)) + { + return commit; + } + + return this.commits[sha] = new(this, sha); + } + internal IEnumerable Log(string revision) => Run(false, "rev-list", "--date-order", revision).Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(sha => GetCommit(sha.Trim())); + public void Dispose() + { + foreach (var weak in this.readers) + { + if (weak.TryGetTarget(out var reader)) + { + reader.Dispose(); + } + } + this.readers.Clear(); + this.commits.Clear(); + } +} + +public interface IRepository : IDisposable +{ + RepositoryInformation Info { get; } + Configuration Config { get; } + BranchCollection Branches { get; } + TagCollection Tags { get; } + ReferenceCollection Refs { get; } + Network Network { get; } + ObjectDatabase ObjectDatabase { get; } + WorktreeCollection Worktrees { get; } + Diff Diff { get; } + Branch Head { get; } + IEnumerable Commits { get; } + Commit Commit(string message, Signature author, Signature committer, CommitOptions? options = null); + void Merge(string target, Signature signature, MergeOptions? options = null); + void Merge(Branch target, Signature signature, MergeOptions? options = null); + void Merge(Commit target, Signature signature, MergeOptions? options = null); + string Run(params string[] arguments); + string RunWithSignature(Signature author, Signature committer, string? input, params string[] arguments); +} + +public sealed record Signature(string Name, string Email, DateTimeOffset When); +public sealed record ObjectId(string Sha) +{ + public override string ToString() => Sha; +} +public class GitObject(Repository repository, string sha) +{ + internal Repository Repository { get; } = repository; + public string Sha { get; } = sha; + public ObjectId Id => new(Sha); +} +public sealed class Commit(Repository repository, string sha) : GitObject(repository, sha) +{ + private string[] Metadata => field ??= Repository.Run("show", "-s", "--format=%P%x00%T%x00%an%x00%ae%x00%aI%x00%cn%x00%ce%x00%cI%x00%B", Sha).Split('\0'); + public IEnumerable Parents => Metadata[0].Split(' ', StringSplitOptions.RemoveEmptyEntries).Select(Repository.GetCommit); + public Tree Tree => new(Repository, Metadata[1]); + public Signature Author => new(Metadata[2], Metadata[3], DateTimeOffset.Parse(Metadata[4], CultureInfo.InvariantCulture)); + public Signature Committer => new(Metadata[5], Metadata[6], DateTimeOffset.Parse(Metadata[7], CultureInfo.InvariantCulture)); + public string Message => Metadata[8]; + public string MessageShort => Message.Split('\n')[0]; +} +public sealed class Tree(Repository repository, string sha) : GitObject(repository, sha); +public sealed class CommitOptions +{ + public bool AmendPreviousCommit { get; set; } + public bool AllowEmptyCommit { get; set; } +} +public enum FastForwardStrategy { Default, NoFastForward } +public sealed class MergeOptions +{ + public FastForwardStrategy FastForwardStrategy { get; set; } +} +public sealed class FetchOptions; +public sealed class PullOptions; + +public sealed class RepositoryInformation(Repository repository) +{ + public string WorkingDirectory => System.IO.Path.GetFullPath(repository.DirectoryPath).TrimEnd(System.IO.Path.DirectorySeparatorChar) + System.IO.Path.DirectorySeparatorChar; + public string Path => repository.Run("rev-parse", "--absolute-git-dir").Trim() + System.IO.Path.DirectorySeparatorChar; + public bool IsHeadDetached => repository.IsHeadDetached; +} +public sealed class Configuration(Repository repository) +{ + public void Set(string key, T value) => repository.Run("config", key, Convert.ToString(value, CultureInfo.InvariantCulture) ?? ""); + public Signature BuildSignature(DateTimeOffset when) => new(repository.Run("config", "user.name").Trim(), repository.Run("config", "user.email").Trim(), when); +} + +public sealed class Branch(Repository repository, string name) +{ + public string CanonicalName { get; } = name; + public string FriendlyName => CanonicalName.Replace("refs/heads/", "", StringComparison.Ordinal).Replace("refs/remotes/", "", StringComparison.Ordinal); + public bool IsRemote => CanonicalName.StartsWith("refs/remotes/", StringComparison.Ordinal); + public Commit Tip => repository.Lookup(CanonicalName == "(no branch)" ? "HEAD" : CanonicalName)!; + public IEnumerable Commits => repository.Log(CanonicalName == "(no branch)" ? "HEAD" : CanonicalName); + public string RemoteName => repository.Run(false, "config", $"branch.{FriendlyName}.remote").Trim(); + public Branch? TrackedBranch => repository.Branches[repository.Run(false, "rev-parse", "--symbolic-full-name", FriendlyName + "@{upstream}").Trim()]; +} +public sealed class BranchUpdater +{ + public string? Remote { get; set; } + public string? UpstreamBranch { get; set; } + public string? TrackedBranch { get; set; } +} +public sealed class BranchCollection(Repository repository) : IEnumerable +{ + public Branch? this[string name] + { + get + { + if (string.IsNullOrEmpty(name)) + { + return null; + } + + var canonical = name.StartsWith("refs/", StringComparison.Ordinal) ? name : "refs/heads/" + name; + if (repository.Run(false, "show-ref", "--verify", canonical).Length == 0) + { + canonical = "refs/remotes/" + name; + if (repository.Run(false, "show-ref", "--verify", canonical).Length == 0) + { + return null; + } + } + return new(repository, canonical); + } + } + public Branch Add(string name, Commit commit) => repository.CreateBranch(name, commit); + public void Remove(string name) + { + if (this[name] is { } branch) + { + Remove(branch); + } + } + public void Remove(Branch branch) + { + if (!branch.IsRemote && branch.CanonicalName == repository.Head.CanonicalName) + { + throw new InvalidOperationException("Cannot remove the currently checked-out branch."); + } + repository.Run("update-ref", "-d", branch.CanonicalName); + } + public Branch Update(Branch branch, Action update) + { + var values = new BranchUpdater(); + update(values); + if (values.Remote != null) + { + repository.Config.Set($"branch.{branch.FriendlyName}.remote", values.Remote); + } + + if (values.UpstreamBranch != null) + { + repository.Config.Set($"branch.{branch.FriendlyName}.merge", values.UpstreamBranch); + } + + if (values.TrackedBranch != null) + { + repository.Run("branch", "--set-upstream-to", values.TrackedBranch, branch.FriendlyName); + } + + return branch; + } + public IEnumerator GetEnumerator() => repository.Run("for-each-ref", "--format=%(refname)", "refs/heads", "refs/remotes").Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(name => new Branch(repository, name.Trim())).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} +public sealed class Tag(Repository repository, string name) +{ + public string FriendlyName { get; } = name; + public string CanonicalName => "refs/tags/" + FriendlyName; + public GitObject Target => repository.Lookup(CanonicalName)!; + public GitObject PeeledTarget => repository.Lookup(CanonicalName + "^{}")!; +} +public sealed class TagCollection(Repository repository) : IEnumerable +{ + public Tag this[string name] => new(repository, name); + public Tag Add(string name, Commit commit) => Add(name, commit.Sha); + public Tag Add(string name, string target) + { + repository.Run("tag", name, target); + return this[name]; + } + public Tag Add(string name, Commit commit, Signature signature, string message) => Add(name, commit.Sha, signature, message); + public Tag Add(string name, string target, Signature signature, string message) + { + repository.RunWithSignature(signature, signature, null, "tag", "-a", name, target, "-m", message); + return this[name]; + } + public void Remove(string name) => repository.Run("tag", "-d", name); + public IEnumerator GetEnumerator() => repository.Run("tag", "--list").Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(name => this[name.Trim()]).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} +public sealed record Reference(string CanonicalName, string TargetIdentifier = ""); +public sealed class ReferenceCollection(Repository repository) : IEnumerable +{ + public Reference this[string name] => new(name); + public IEnumerator GetEnumerator() => repository.Run("for-each-ref", "--format=%(refname) %(objectname)").Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(line => line.Split(' ')).Select(parts => new Reference(parts[0], parts[1])).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public void Add(string name, ObjectId id, bool allowOverwrite = false) => Add(name, id.Sha, allowOverwrite); + public void Add(string name, string target, bool allowOverwrite = false) + { + if (target.StartsWith("refs/", StringComparison.Ordinal)) + { + repository.Run("symbolic-ref", name, target); + } + else if (allowOverwrite) + { + repository.Run("update-ref", name, target); + } + else + { + repository.Run("update-ref", name, target, new string('0', 40)); + } + } + public void Remove(string name) => repository.Run("update-ref", "-d", name); + public void UpdateTarget(Reference reference, string target) + { + if (target.StartsWith("refs/", StringComparison.Ordinal)) + { + repository.Run("symbolic-ref", reference.CanonicalName, target); + } + else + { + repository.Run("update-ref", reference.CanonicalName, target); + } + } + public void UpdateTarget(Reference reference, ObjectId id) => repository.Run("update-ref", reference.CanonicalName, id.Sha); +} +public sealed class Remote(Repository repository, string name) +{ + public string Name { get; } = name; + public string Url => repository.Run("remote", "get-url", Name).Trim(); +} +public sealed class RemoteUpdater +{ + public string? Url { get; set; } + public string? FetchRefSpecs { get; set; } +} +public sealed class RemoteCollection(Repository repository) : IEnumerable +{ + public Remote this[string name] => new(repository, name); + public Remote Add(string name, string url) + { + repository.Run("remote", "add", name, url); + return this[name]; + } + public void Remove(string name) => repository.Run("remote", "remove", name); + public void Update(string name, Action update) + { + var values = new RemoteUpdater(); + update(values); + if (values.Url != null) + { + repository.Run("remote", "set-url", name, values.Url); + } + + if (values.FetchRefSpecs != null) + { + repository.Run("config", "--replace-all", $"remote.{name}.fetch", values.FetchRefSpecs); + } + } + public IEnumerator GetEnumerator() => repository.Run("remote").Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(name => this[name.Trim()]).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} +public sealed class Network(Repository repository) +{ + public RemoteCollection Remotes { get; } = new(repository); +} +public sealed class ObjectDatabase(Repository repository) +{ + public Tree CreateEmptyTree() => new(repository, Repository.Execute(repository.DirectoryPath, true, null, "", "mktree").Trim()); + public GitObject CreateBlob(Stream content) + { + var path = Path.GetTempFileName(); + try + { + using (var file = File.Create(path)) + { + content.CopyTo(file); + } + + return new(repository, repository.Run("hash-object", "--no-filters", "-w", path).Trim()); + } + finally { File.Delete(path); } + } + public Commit CreateCommit(Signature author, Signature committer, string message, Tree tree, IEnumerable parents) + { + var args = new List { "commit-tree", tree.Sha }; + foreach (var parent in parents) { args.Add("-p"); args.Add(parent.Sha); } + args.Add("-m"); args.Add(message); + return repository.GetCommit(repository.RunWithSignature(author, committer, null, [.. args]).Trim()); + } +} +public sealed class WorktreeCollection(Repository repository) +{ + public void AddNewBranch(string name, string path) => repository.Run("worktree", "add", "-b", name, path); + public void Add(string branch, string path) => repository.Run("worktree", "add", path, branch.StartsWith("refs/heads/", StringComparison.Ordinal) ? branch["refs/heads/".Length..] : branch); +} +public sealed record TreeEntryChanges(string Path); +public class TreeChanges : List; +public sealed class Diff(Repository repository) +{ + public TreeChanges Compare(Tree first, Tree second) where T : TreeChanges + { + var result = new TreeChanges(); + result.AddRange(repository.Run("diff", "--name-only", first.Sha, second.Sha).Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(path => new TreeEntryChanges(path.Trim()))); + return result; + } +} +public static class Commands +{ + public static void Checkout(IRepository repository, string revision) + { + repository.Run("checkout", "--quiet", revision); + if (revision.Contains('/') && repository.Branches[revision] is { IsRemote: true } branch) + { + repository.Run("symbolic-ref", "HEAD", branch.CanonicalName); + } + } + public static void Checkout(IRepository repository, Branch branch) => Checkout(repository, branch.FriendlyName); + public static void Checkout(IRepository repository, Commit commit) => Checkout(repository, commit.Sha); + public static void Stage(IRepository repository, string path) => repository.Run("add", "--", path); + public static void Fetch(IRepository repository, string remote, IEnumerable refSpecs, FetchOptions? _, string? _1) + { + var specifications = refSpecs.ToArray(); + repository.Run(["fetch", remote, .. specifications]); + if (specifications.Length != 0) + { + return; + } + // Preserve the fixture's fetch-all-advertised-objects behavior, including + // PR-only commits that are not reachable through the remote's branch map. + var extraReferences = repository.Run("ls-remote", "--refs", remote) + .Split('\n', StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Split('\t')[1]) + .Where(name => !name.StartsWith("refs/heads/", StringComparison.Ordinal) && !name.StartsWith("refs/tags/", StringComparison.Ordinal)) + .ToArray(); + if (extraReferences.Length != 0) + { + repository.Run(["fetch", remote, .. extraReferences]); + } + } + public static void Pull(IRepository repository, Signature signature, PullOptions _) => repository.RunWithSignature(signature, signature, null, "pull", "--no-rebase", "--no-edit"); +} diff --git a/src/GitVersion.Testing/Git/RepositoryExtensions.cs b/src/GitVersion.Testing/Git/RepositoryExtensions.cs new file mode 100644 index 0000000000..5968fecc4b --- /dev/null +++ b/src/GitVersion.Testing/Git/RepositoryExtensions.cs @@ -0,0 +1,24 @@ +using GitVersion.Git; + +using GitVersion.Testing.Git; + +namespace GitVersion.Testing; + +public static class RepositoryExtensions +{ + public static IGitRepository ToGitRepository(this IRepository repository) + { + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); + new GitVersionManagedGitModule().RegisterTypes(services); + var result = services.BuildServiceProvider().GetRequiredService(); + result.DiscoverRepository(repository.Info.WorkingDirectory); + if (repository is Repository fixtureRepository) + { + fixtureRepository.Observe(result); + } + + return result; + } +} diff --git a/src/GitVersion.Testing/GitVersion.Testing.csproj b/src/GitVersion.Testing/GitVersion.Testing.csproj index 4c9aed6bb0..1a8ca72d6e 100644 --- a/src/GitVersion.Testing/GitVersion.Testing.csproj +++ b/src/GitVersion.Testing/GitVersion.Testing.csproj @@ -5,7 +5,6 @@ true - @@ -19,5 +18,6 @@ + diff --git a/src/GitVersion.slnx b/src/GitVersion.slnx index 9d1349ca55..8328050efd 100644 --- a/src/GitVersion.slnx +++ b/src/GitVersion.slnx @@ -10,7 +10,6 @@ - From 401cb7da6d5c7bfcbfd5cab4e4014a040af81911 Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Wed, 21 Oct 2026 11:53:12 +0200 Subject: [PATCH 02/15] docs: remove obsolete native backend source mapping --- docs/design/managed-git-migration.md | 9 +++++---- docs/version-calculation-map.json | 3 +-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/design/managed-git-migration.md b/docs/design/managed-git-migration.md index de288a3373..8253a451a6 100644 --- a/docs/design/managed-git-migration.md +++ b/docs/design/managed-git-migration.md @@ -198,10 +198,11 @@ CLI mutator, later also the managed reader). The version-to-default mapping is e | **v7.1** | `managed` | `libgit2` fails; explicit `managed` remains accepted | | v7.x (later) | `managed` | Explicit `managed` remains a compatibility no-op | -Historical v7.0 validation: both backends shipped side by side during that window so users can validate `managed` against -their real repositories and report issues before libgit2 is dropped. For that whole window, the CI -unit-test matrix runs the full suite against both backends (`git_backend` dimension in `_unit_tests.yml`) — -both legs must stay green. +The v7.0 transition design retains both backends so users can validate `managed` +against their repositories before the v7.1 removal. The dual-backend CI matrix +was used during implementation of that transition. This v7.1 branch removes the +`git_backend` dimension and runs the managed implementation across the existing +OS/TFM matrix; both adapters are no longer present side by side. ### Phase A — CLI mutator first (reads stay on libgit2) · ~2–3 weeks Replace `GitRepository.mutating.cs` + mutating collection members with `GitVersion.Git.CommandLine` diff --git a/docs/version-calculation-map.json b/docs/version-calculation-map.json index 72f28e580f..03df84b608 100644 --- a/docs/version-calculation-map.json +++ b/docs/version-calculation-map.json @@ -12,8 +12,7 @@ "src/GitVersion.Core/Configuration", "src/GitVersion.Core/GitVersionContext.cs", "src/GitVersion.Configuration", - "src/GitVersion.Git.Managed", - "src/GitVersion.LibGit2Sharp" + "src/GitVersion.Git.Managed" ], "sections": [ { From 4ff55c8eeb68e8faac440faad060fb3db27065d6 Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Wed, 21 Oct 2026 14:45:26 +0200 Subject: [PATCH 03/15] build: compile only selected test targets --- build/build/Tasks/Test/UnitTest.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/build/build/Tasks/Test/UnitTest.cs b/build/build/Tasks/Test/UnitTest.cs index 5f1af0c339..51125a825e 100644 --- a/build/build/Tasks/Test/UnitTest.cs +++ b/build/build/Tasks/Test/UnitTest.cs @@ -1,4 +1,5 @@ using Cake.Common.Tools.DotNet.Execute; +using Cake.Common.Tools.DotNet.Restore; using Common.Utilities; namespace Build.Tasks; @@ -7,7 +8,7 @@ namespace Build.Tasks; [TaskDescription("Run the unit tests")] [DotnetArgument] [TaskArgument(Arguments.TestResults)] -[IsDependentOn(typeof(Build))] +[IsDependentOn(typeof(Clean))] public class UnitTest : FrostingTask { public override bool ShouldRun(BuildContext context) => context.EnabledUnitTests; @@ -21,6 +22,15 @@ public override void Run(BuildContext context) throw new CakeException("No test projects were found."); } + // Build each selected target below with real source paths. An upfront solution + // build compiles both frameworks with CI paths, then recompiles them for tests. + context.DotNetRestore("./src/GitVersion.slnx", new DotNetRestoreSettings + { + Verbosity = DotNetVerbosity.Minimal, + Sources = [Constants.NugetOrgUrl], + MSBuildSettings = context.MsBuildSettings + }); + var failures = new List(); foreach (var framework in frameworks) { @@ -69,7 +79,7 @@ private static int TestProjectForTarget(BuildContext context, FilePath project, var settings = new DotNetBuildSettings { Framework = $"net{framework}", - // Build restores the solution. Rebuild with real source paths for snapshots and annotations. + // The solution is restored once; build with real paths for snapshots and annotations. NoRestore = true, Configuration = context.MsBuildConfiguration, MSBuildSettings = new() From 58d98c4e34349303e33a58d47e45fb16c28e1503 Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Wed, 21 Oct 2026 15:57:07 +0200 Subject: [PATCH 04/15] fix: isolate exit codes for in-process CLI invocations --- .../Helpers/ProgramFixture.cs | 4 +-- .../ProgramFixtureTests.cs | 32 +++++++++++++++++++ .../PullRequestInBuildAgentTest.cs | 2 +- src/GitVersion.App/GitVersionApp.cs | 18 ++++------- src/GitVersion.App/Program.cs | 2 +- 5 files changed, 43 insertions(+), 15 deletions(-) create mode 100644 src/GitVersion.App.Tests/ProgramFixtureTests.cs diff --git a/src/GitVersion.App.Tests/Helpers/ProgramFixture.cs b/src/GitVersion.App.Tests/Helpers/ProgramFixture.cs index 349f703564..53b04d5abe 100644 --- a/src/GitVersion.App.Tests/Helpers/ProgramFixture.cs +++ b/src/GitVersion.App.Tests/Helpers/ProgramFixture.cs @@ -64,8 +64,8 @@ public async Task Run(params string[] args) var host = builder.Build(); var app = host.Services.GetRequiredService(); - await app.RunAsync(CancellationToken.None); + var exitCode = await app.RunAsync(CancellationToken.None); - return new(SysEnv.ExitCode, this.output.Value, this.logger.Value); + return new(exitCode, this.output.Value, this.logger.Value); } } diff --git a/src/GitVersion.App.Tests/ProgramFixtureTests.cs b/src/GitVersion.App.Tests/ProgramFixtureTests.cs new file mode 100644 index 0000000000..bdf0d829d3 --- /dev/null +++ b/src/GitVersion.App.Tests/ProgramFixtureTests.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Hosting; + +namespace GitVersion.App.Tests; + +[TestFixture] +[NonParallelizable] +public class ProgramFixtureTests +{ + [Test] + public async Task RunReturnsInvocationExitCodeWhenProcessExitCodeChangesDuringShutdown() + { + var previousExitCode = SysEnv.ExitCode; + try + { + var fixture = new ProgramFixture(); + fixture.WithOverrides(services => services.AddSingleton(provider => + { + provider.GetRequiredService().ApplicationStopping.Register(() => SysEnv.ExitCode = 1); + return ActivatorUtilities.CreateInstance(provider); + })); + + var result = await fixture.Run("--version"); + + result.ExitCode.ShouldBe(0); + SysEnv.ExitCode.ShouldBe(1); + } + finally + { + SysEnv.ExitCode = previousExitCode; + } + } +} diff --git a/src/GitVersion.App.Tests/PullRequestInBuildAgentTest.cs b/src/GitVersion.App.Tests/PullRequestInBuildAgentTest.cs index 0472dcc589..9058054e8f 100644 --- a/src/GitVersion.App.Tests/PullRequestInBuildAgentTest.cs +++ b/src/GitVersion.App.Tests/PullRequestInBuildAgentTest.cs @@ -206,7 +206,7 @@ private static async Task VerifyPullRequestVersionIsCalculatedProperly(EmptyRepo var result = await programFixture.Run(); - result.ExitCode.ShouldBe(0); + result.ExitCode.ShouldBe(0, result.Log); result.Output.ShouldNotBeNull(); result.OutputVariables.ShouldNotBeNull(); result.OutputVariables.FullSemVer.ShouldBe("1.0.4-PullRequest5.3"); diff --git a/src/GitVersion.App/GitVersionApp.cs b/src/GitVersion.App/GitVersionApp.cs index 13a5acbca9..2eca3f0598 100644 --- a/src/GitVersion.App/GitVersionApp.cs +++ b/src/GitVersion.App/GitVersionApp.cs @@ -16,45 +16,41 @@ internal class GitVersionApp( private readonly IConfigurationMigrationExecutor configurationMigrationExecutor = configurationMigrationExecutor.NotNull(); private readonly IOptions options = options.NotNull(); - public Task RunAsync(CancellationToken _) + public Task RunAsync(CancellationToken _) { try { var gitVersionOptions = this.options.Value; if (gitVersionOptions.IsCliSchema) { - SysEnv.ExitCode = 0; - return Task.CompletedTask; + return Task.FromResult(0); } if (gitVersionOptions.ConfigurationInfo.Validate) { - SysEnv.ExitCode = configurationValidationExecutor.Execute(gitVersionOptions); - return Task.CompletedTask; + return Task.FromResult(configurationValidationExecutor.Execute(gitVersionOptions)); } selections.Log(logger); if (gitVersionOptions.IsHelp || gitVersionOptions.IsVersion) { - SysEnv.ExitCode = 0; + return Task.FromResult(0); } else if (gitVersionOptions.ConfigurationMigrationInfo.IsMigration) { - SysEnv.ExitCode = this.configurationMigrationExecutor.Execute(gitVersionOptions); + return Task.FromResult(this.configurationMigrationExecutor.Execute(gitVersionOptions)); } else { - SysEnv.ExitCode = this.gitVersionExecutor.Value.Execute(gitVersionOptions); + return Task.FromResult(this.gitVersionExecutor.Value.Execute(gitVersionOptions)); } } catch (Exception exception) { Console.Error.WriteLine(exception.Message); - SysEnv.ExitCode = 1; + return Task.FromResult(1); } finally { this.applicationLifetime.StopApplication(); } - - return Task.CompletedTask; } } diff --git a/src/GitVersion.App/Program.cs b/src/GitVersion.App/Program.cs index 095fb8d777..7accd66ec8 100644 --- a/src/GitVersion.App/Program.cs +++ b/src/GitVersion.App/Program.cs @@ -7,7 +7,7 @@ using var host = builder.Build(); var app = host.Services.GetRequiredService(); - await app.RunAsync(CancellationToken.None).ConfigureAwait(false); + SysEnv.ExitCode = await app.RunAsync(CancellationToken.None).ConfigureAwait(false); } catch (WarningException exception) { From bd54e3fd5584933e7590196f1679f5f18ce8ed68 Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Wed, 21 Oct 2026 16:10:18 +0200 Subject: [PATCH 05/15] test: isolate wall-clock performance scenario from fixture contention --- .../IntegrationTests/PerformanceScenarios.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/GitVersion.Core.Tests/IntegrationTests/PerformanceScenarios.cs b/src/GitVersion.Core.Tests/IntegrationTests/PerformanceScenarios.cs index 3fc792c54f..2a1ca9524a 100644 --- a/src/GitVersion.Core.Tests/IntegrationTests/PerformanceScenarios.cs +++ b/src/GitVersion.Core.Tests/IntegrationTests/PerformanceScenarios.cs @@ -2,6 +2,8 @@ namespace GitVersion.Tests.IntegrationTests; +// Measure calculation time without concurrent fixtures spawning Git processes. +[NonParallelizable] public class PerformanceScenarios : TestBase { [Test] From e05113965f036be7bd087abb2a9326ee781d97aa Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Wed, 21 Oct 2026 16:29:11 +0200 Subject: [PATCH 06/15] fix: report removed backend errors at experimental CLI entrypoints --- new-cli/GitVersion.Cli/Program.cs | 23 ++++++++++++++++------- new-cli/GitVersion.Core.Tester/Program.cs | 23 ++++++++++++++++------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/new-cli/GitVersion.Cli/Program.cs b/new-cli/GitVersion.Cli/Program.cs index 615ca0b76e..b1695297fd 100644 --- a/new-cli/GitVersion.Cli/Program.cs +++ b/new-cli/GitVersion.Cli/Program.cs @@ -18,17 +18,26 @@ cts.Dispose(); }; -await using var serviceProvider = RegisterModules(modules); -var app = serviceProvider.GetRequiredService(); +try +{ + await using var serviceProvider = RegisterModules(modules); + var app = serviceProvider.GetRequiredService(); + + var result = await app.RunAsync(args, cts.Token).ConfigureAwait(false); + if (!Console.IsInputRedirected) + { + Console.ReadKey(); + } -var result = await app.RunAsync(args, cts.Token).ConfigureAwait(false); -if (!Console.IsInputRedirected) + return result; +} +catch (WarningException exception) { - Console.ReadKey(); + await Console.Error.WriteLineAsync("An error occurred:").ConfigureAwait(false); + await Console.Error.WriteLineAsync(exception.Message).ConfigureAwait(false); + return 1; } -return result; - static ServiceProvider RegisterModules(IEnumerable gitVersionModules) { var serviceProvider = new ServiceCollection() diff --git a/new-cli/GitVersion.Core.Tester/Program.cs b/new-cli/GitVersion.Core.Tester/Program.cs index 0cb9b914d4..ea9bc460c1 100644 --- a/new-cli/GitVersion.Core.Tester/Program.cs +++ b/new-cli/GitVersion.Core.Tester/Program.cs @@ -17,17 +17,26 @@ cts.Dispose(); }; -await using var serviceProvider = RegisterModules(modules); -var app = serviceProvider.GetRequiredService(); +try +{ + await using var serviceProvider = RegisterModules(modules); + var app = serviceProvider.GetRequiredService(); + + var result = await app.RunAsync(args, cts.Token).ConfigureAwait(false); + if (!Console.IsInputRedirected) + { + Console.ReadKey(); + } -var result = await app.RunAsync(args, cts.Token).ConfigureAwait(false); -if (!Console.IsInputRedirected) + return result; +} +catch (WarningException exception) { - Console.ReadKey(); + await Console.Error.WriteLineAsync("An error occurred:").ConfigureAwait(false); + await Console.Error.WriteLineAsync(exception.Message).ConfigureAwait(false); + return 1; } -return result; - static ServiceProvider RegisterModules(IEnumerable gitVersionModules) { var serviceProvider = new ServiceCollection() From c950841a96c18b7b8191fd4523ad92c03c758701 Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Wed, 21 Oct 2026 16:54:09 +0200 Subject: [PATCH 07/15] ci: partition covered macOS tests with Cake-owned test groups --- .github/workflows/_prepare.yml | 4 +++ .github/workflows/_unit_tests.yml | 11 ++++---- .github/workflows/ci.yml | 2 +- build/build/Tasks/Test/UnitTest.cs | 19 ++++++++++++++ build/common/Utilities/Arguments.cs | 2 ++ build/config/Tasks/SetMatrix.cs | 40 +++++++++++++++++++++++++++++ 6 files changed, 71 insertions(+), 7 deletions(-) diff --git a/.github/workflows/_prepare.yml b/.github/workflows/_prepare.yml index 365c79ca0b..a10070739f 100644 --- a/.github/workflows/_prepare.yml +++ b/.github/workflows/_prepare.yml @@ -1,6 +1,9 @@ on: workflow_call: outputs: + test_matrix: + description: 'Unit-test project groups generated by Cake' + value: ${{ jobs.set_matrix.outputs.test_matrix }} docker_distros: description: 'List of Docker distros' value: ${{ jobs.set_matrix.outputs.docker_distros }} @@ -70,6 +73,7 @@ jobs: name: Set Matrix runs-on: ubuntu-24.04 outputs: + test_matrix: ${{ steps.set_matrix.outputs.test_matrix }} docker_distros: ${{ steps.set_matrix.outputs.docker_distros }} dotnet_versions: ${{ steps.set_matrix.outputs.dotnet_versions }} steps: diff --git a/.github/workflows/_unit_tests.yml b/.github/workflows/_unit_tests.yml index baa5328127..7a0eeb5e36 100644 --- a/.github/workflows/_unit_tests.yml +++ b/.github/workflows/_unit_tests.yml @@ -1,7 +1,7 @@ on: workflow_call: inputs: - dotnet_versions: + test_matrix: required: true type: string publish_coverage: @@ -16,14 +16,12 @@ permissions: jobs: unit_test: - name: ${{ matrix.os }} - net${{ matrix.dotnet_version }} + name: ${{ matrix.name }} permissions: id-token: write strategy: fail-fast: false - matrix: - os: [ windows-2025-vs2026, ubuntu-24.04, macos-26 ] - dotnet_version: ${{ fromJson(inputs.dotnet_versions) }} + matrix: ${{ fromJson(inputs.test_matrix) }} runs-on: ${{ matrix.os }} timeout-minutes: 45 @@ -58,6 +56,7 @@ jobs: } trap preserve_reports EXIT dotnet run/build.dll --target=UnitTest --dotnet_version=${{ matrix.dotnet_version }} --test_results="$TEST_RESULTS" \ + --test_projects="${{ matrix.test_projects }}" --test_filter="${{ matrix.test_filter }}" \ 2>&1 | tee "$RUNNER_TEMP/unit-tests.log" - name: Explain failed or incomplete test run @@ -79,7 +78,7 @@ jobs: uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 if: always() with: - name: test-results-${{ matrix.os }}-net${{ matrix.dotnet_version }}-${{ github.run_id }}-${{ github.run_attempt }} + name: test-results-${{ matrix.os }}-net${{ matrix.dotnet_version }}-${{ matrix.group }}-${{ github.run_id }}-${{ github.run_attempt }} path: artifacts/test-results/ if-no-files-found: warn retention-days: 7 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3fd570f9b5..7a22af9a4c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,7 +86,7 @@ jobs: id-token: write uses: $/.github/workflows/_unit_tests.yml # NOSONAR -- $/ resolves to the running commit. with: - dotnet_versions: ${{ needs.prepare.outputs.dotnet_versions }} + test_matrix: ${{ needs.prepare.outputs.test_matrix }} publish_coverage: ${{ fromJson(needs.publish_flags.outputs.can_publish) }} artifacts_windows_test: diff --git a/build/build/Tasks/Test/UnitTest.cs b/build/build/Tasks/Test/UnitTest.cs index 51125a825e..7ce717530d 100644 --- a/build/build/Tasks/Test/UnitTest.cs +++ b/build/build/Tasks/Test/UnitTest.cs @@ -8,6 +8,8 @@ namespace Build.Tasks; [TaskDescription("Run the unit tests")] [DotnetArgument] [TaskArgument(Arguments.TestResults)] +[TaskArgument(Arguments.TestProjects)] +[TaskArgument(Arguments.TestFilter)] [IsDependentOn(typeof(Clean))] public class UnitTest : FrostingTask { @@ -22,6 +24,18 @@ public override void Run(BuildContext context) throw new CakeException("No test projects were found."); } + var requested = context.Argument(Arguments.TestProjects, string.Empty) + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (requested.Length > 0) + { + var unknown = requested.Except(projects.Select(project => project.GetFilenameWithoutExtension().FullPath), StringComparer.OrdinalIgnoreCase).ToArray(); + if (unknown.Length > 0) + { + throw new CakeException($"Unknown test projects: {string.Join(", ", unknown)}."); + } + projects = [.. projects.Where(project => requested.Contains(project.GetFilenameWithoutExtension().FullPath, StringComparer.OrdinalIgnoreCase))]; + } + // Build each selected target below with real source paths. An upfront solution // build compiles both frameworks with CI paths, then recompiles them for tests. context.DotNetRestore("./src/GitVersion.slnx", new DotNetRestoreSettings @@ -108,6 +122,11 @@ private static int TestProjectForTarget(BuildContext context, FilePath project, // CI publishes coverage from the LTS target; other targets validate runtime compatibility. var collectCoverage = framework == Constants.DotnetLtsLatest; var args = new ProcessArgumentBuilder().AppendArguments(resultsDirectory, collectCoverage); + var filter = context.Argument(Arguments.TestFilter, string.Empty); + if (!string.IsNullOrWhiteSpace(filter)) + { + args.Append("--filter").AppendQuoted(filter); + } if (context.BuildSystem().IsRunningOnGitHubActions) { args = args.AppendGitHubArguments(); diff --git a/build/common/Utilities/Arguments.cs b/build/common/Utilities/Arguments.cs index 65d33d5b97..e6a3a702f6 100644 --- a/build/common/Utilities/Arguments.cs +++ b/build/common/Utilities/Arguments.cs @@ -7,6 +7,8 @@ public static class Arguments public const string Architecture = "arch"; public const string DotnetVersion = "dotnet_version"; public const string TestResults = "test_results"; + public const string TestProjects = "test_projects"; + public const string TestFilter = "test_filter"; public const string DockerRegistry = "docker_registry"; public const string DockerDistro = "docker_distro"; } diff --git a/build/config/Tasks/SetMatrix.cs b/build/config/Tasks/SetMatrix.cs index 666757cfa5..b5c542c2dd 100644 --- a/build/config/Tasks/SetMatrix.cs +++ b/build/config/Tasks/SetMatrix.cs @@ -6,15 +6,55 @@ public class SetMatrix : FrostingTask { public override void Run(BuildContext context) { + const string coreProject = "GitVersion.Core.Tests"; + var projects = context.GetFiles("./src/**/*.Tests.csproj") + .Select(project => project.GetFilenameWithoutExtension().FullPath).Order(StringComparer.Ordinal).ToArray(); + if (!projects.Contains(coreProject, StringComparer.Ordinal)) + { + throw new CakeException($"Test project {coreProject} was not found."); + } + var otherProjects = string.Join(',', projects.Where(project => project != coreProject)); + var rows = new List(); + foreach (var os in new[] { "windows-2025-vs2026", "ubuntu-24.04", "macos-26" }) + { + foreach (var framework in Constants.DotnetVersions) + { + var name = $"{os} - net{framework}"; + // Git CLI fixture setup is expensive on macOS/net10. Keep every test + // and the existing per-job budget by partitioning the covered target. + if (os == "macos-26" && framework == Constants.DotnetLtsLatest) + { + var groups = new[] + { + new { name = "Core integration", id = "core-integration", filter = "FullyQualifiedName~GitVersion.Tests.IntegrationTests&FullyQualifiedName!~Mainline" }, + new { name = "Core integration mainline", id = "core-integration-mainline", filter = "FullyQualifiedName~GitVersion.Tests.IntegrationTests&FullyQualifiedName~Mainline" }, + new { name = "Core mainline", id = "core-mainline", filter = "FullyQualifiedName!~GitVersion.Tests.IntegrationTests&FullyQualifiedName~Mainline" }, + new { name = "Core remaining", id = "core-other", filter = "FullyQualifiedName!~GitVersion.Tests.IntegrationTests&FullyQualifiedName!~Mainline" } + }; + foreach (var group in groups) + { + rows.Add(new { name = $"{name} - {group.name}", os, dotnet_version = framework, group = group.id, test_projects = coreProject, test_filter = group.filter }); + } + rows.Add(new { name = $"{name} - CLI and adapters", os, dotnet_version = framework, group = "other-projects", test_projects = otherProjects, test_filter = "" }); + } + else + { + rows.Add(new { name, os, dotnet_version = framework, group = "all", test_projects = "", test_filter = "" }); + } + } + } + var testMatrix = new { include = rows }; if (context.BuildSystem().IsRunningOnGitHubActions) { context.GitHubActions().Commands.SetOutputParameter("docker_distros", context.SerializeJson(Constants.DockerDistros)); context.GitHubActions().Commands.SetOutputParameter("dotnet_versions", context.SerializeJson(Constants.DotnetVersions)); + context.GitHubActions().Commands.SetOutputParameter("test_matrix", context.SerializeJson(testMatrix)); } else { context.Information("Docker Distros: {0}", context.SerializeJson(Constants.DockerDistros)); context.Information("Dotnet Versions: {0}", context.SerializeJson(Constants.DotnetVersions)); + context.Information("Test Matrix: {0}", context.SerializeJson(testMatrix)); } } } From a1e75dbe5a1dc6daad0e12d9d0fd66e64b9b78ab Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Wed, 21 Oct 2026 17:09:02 +0200 Subject: [PATCH 08/15] ci: omit empty optional Cake test arguments --- .github/workflows/_unit_tests.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/_unit_tests.yml b/.github/workflows/_unit_tests.yml index 7a0eeb5e36..6961d17187 100644 --- a/.github/workflows/_unit_tests.yml +++ b/.github/workflows/_unit_tests.yml @@ -45,6 +45,9 @@ jobs: id: unit_tests timeout-minutes: 30 shell: bash + env: + TEST_PROJECTS: ${{ matrix.test_projects }} + TEST_FILTER: ${{ matrix.test_filter }} run: | # Explicit bash uses pipefail on every runner. Keep the log outside Cake's Clean target. preserve_reports() { @@ -55,8 +58,15 @@ jobs: fi } trap preserve_reports EXIT + test_arguments=() + if [[ -n "$TEST_PROJECTS" ]]; then + test_arguments+=("--test_projects=$TEST_PROJECTS") + fi + if [[ -n "$TEST_FILTER" ]]; then + test_arguments+=("--test_filter=$TEST_FILTER") + fi dotnet run/build.dll --target=UnitTest --dotnet_version=${{ matrix.dotnet_version }} --test_results="$TEST_RESULTS" \ - --test_projects="${{ matrix.test_projects }}" --test_filter="${{ matrix.test_filter }}" \ + "${test_arguments[@]}" \ 2>&1 | tee "$RUNNER_TEMP/unit-tests.log" - name: Explain failed or incomplete test run From eb8d54025a7d9a50081200d6eaca4c3e928db212 Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Wed, 21 Oct 2026 17:30:53 +0200 Subject: [PATCH 09/15] test: assert successful repository discovery before empty-history errors --- docs/design/managed-git-migration.md | 2 +- .../Core/GitVersionToolDirectoryTests.cs | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/design/managed-git-migration.md b/docs/design/managed-git-migration.md index 8253a451a6..abc0699923 100644 --- a/docs/design/managed-git-migration.md +++ b/docs/design/managed-git-migration.md @@ -39,7 +39,7 @@ LibGit2Sharp wraps the native `libgit2` library and has been a recurring source (linux x64/arm/arm64/musl/ppc64le, osx x64/arm64, win x64/x86/arm64) into `runtimes//native/` of every published artifact — and `GitVersion.MsBuild` ships that payload **per TFM** (net8.0/net9.0/net10.0). - **Platform lag**: new RIDs, libcs and OS versions require waiting on libgit2 + LibGit2Sharp + NativeBinaries releases - (GitVersion is previously pinned to LibGit2Sharp 0.32.0). + (GitVersion was previously pinned to LibGit2Sharp 0.32.0). - **Feature lag**: libgit2 trails git itself (SHA-256 repos, reftable, partial clone), and its global native state complicates concurrent use inside MSBuild. diff --git a/src/GitVersion.Core.Tests/Core/GitVersionToolDirectoryTests.cs b/src/GitVersion.Core.Tests/Core/GitVersionToolDirectoryTests.cs index 20ed11cbca..0c35a3e561 100644 --- a/src/GitVersion.Core.Tests/Core/GitVersionToolDirectoryTests.cs +++ b/src/GitVersion.Core.Tests/Core/GitVersionToolDirectoryTests.cs @@ -31,12 +31,14 @@ public void FindsGitDirectory() var options = Options.Create(new GitVersionOptions { WorkingDirectory = this.workDirectory, Settings = { NoFetch = true } }); var sp = ConfigureServices(services => services.AddSingleton(options)); + sp.DiscoverRepository(); var gitVersionCalculator = sp.GetRequiredService(); gitVersionCalculator.CalculateVersionVariables(); }); - exception.ShouldNotBeAssignableTo(); + exception.ShouldBeOfType() + .Message.ShouldBe("No commits found on the current branch."); } [Test] @@ -50,11 +52,13 @@ public void FindsGitDirectoryInParent() var options = Options.Create(new GitVersionOptions { WorkingDirectory = childDir, Settings = { NoFetch = true } }); var sp = ConfigureServices(services => services.AddSingleton(options)); + sp.DiscoverRepository(); var gitVersionCalculator = sp.GetRequiredService(); gitVersionCalculator.CalculateVersionVariables(); }); - exception.ShouldNotBeAssignableTo(); + exception.ShouldBeOfType() + .Message.ShouldBe("No commits found on the current branch."); } } From c60b6cba96d91f24c76e439b8e984e356c4eae85 Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Wed, 21 Oct 2026 17:45:09 +0200 Subject: [PATCH 10/15] ci: balance covered macOS Core partitions using measured timings --- build/config/Tasks/SetMatrix.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/config/Tasks/SetMatrix.cs b/build/config/Tasks/SetMatrix.cs index b5c542c2dd..93719fbf96 100644 --- a/build/config/Tasks/SetMatrix.cs +++ b/build/config/Tasks/SetMatrix.cs @@ -27,9 +27,9 @@ public override void Run(BuildContext context) var groups = new[] { new { name = "Core integration", id = "core-integration", filter = "FullyQualifiedName~GitVersion.Tests.IntegrationTests&FullyQualifiedName!~Mainline" }, - new { name = "Core integration mainline", id = "core-integration-mainline", filter = "FullyQualifiedName~GitVersion.Tests.IntegrationTests&FullyQualifiedName~Mainline" }, - new { name = "Core mainline", id = "core-mainline", filter = "FullyQualifiedName!~GitVersion.Tests.IntegrationTests&FullyQualifiedName~Mainline" }, - new { name = "Core remaining", id = "core-other", filter = "FullyQualifiedName!~GitVersion.Tests.IntegrationTests&FullyQualifiedName!~Mainline" } + new { name = "Core integration GitFlow", id = "core-integration-gitflow", filter = "FullyQualifiedName~GitVersion.Tests.IntegrationTests&FullyQualifiedName~Mainline&FullyQualifiedName~GitFlow" }, + new { name = "Core integration other", id = "core-integration-other", filter = "FullyQualifiedName~GitVersion.Tests.IntegrationTests&FullyQualifiedName~Mainline&FullyQualifiedName!~GitFlow" }, + new { name = "Core remaining", id = "core-other", filter = "FullyQualifiedName!~GitVersion.Tests.IntegrationTests" } }; foreach (var group in groups) { From 48d61c68c877308d2c10ddd907822142d2a3b1b2 Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Wed, 21 Oct 2026 18:00:58 +0200 Subject: [PATCH 11/15] build: reject missing NuGet package families during validation --- .../Tasks/Package/PackageContentsValidation.cs | 14 ++++++++++++++ build/build/Tasks/Package/PackageNuget.cs | 7 +++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/build/build/Tasks/Package/PackageContentsValidation.cs b/build/build/Tasks/Package/PackageContentsValidation.cs index 4958fb1017..42925f446f 100644 --- a/build/build/Tasks/Package/PackageContentsValidation.cs +++ b/build/build/Tasks/Package/PackageContentsValidation.cs @@ -4,6 +4,20 @@ namespace Build.Tasks; internal static class PackageContentsValidation { + public static void ValidatePackages(IEnumerable toolPackages, IEnumerable msBuildPackages) + { + var tools = toolPackages.ToArray(); + var msBuild = msBuildPackages.ToArray(); + if (tools.Length == 0 || msBuild.Length == 0) + { + throw new CakeException("Expected both GitVersion.Tool and GitVersion.MsBuild NuGet packages for validation."); + } + foreach (var package in tools.Concat(msBuild)) + { + Validate(package); + } + } + public static void Validate(string packagePath) { using var archive = ZipFile.OpenRead(packagePath); diff --git a/build/build/Tasks/Package/PackageNuget.cs b/build/build/Tasks/Package/PackageNuget.cs index a489f0058f..5140424a0b 100644 --- a/build/build/Tasks/Package/PackageNuget.cs +++ b/build/build/Tasks/Package/PackageNuget.cs @@ -42,9 +42,8 @@ private static void PackageWithCli(BuildContext context) settings.ArgumentCustomization = arg => arg.Append("-p:IsPackaging=true"); context.DotNetPack("./src/GitVersion.MsBuild", settings); - foreach (var package in context.GetFiles($"{Paths.Nuget}/GitVersion.Tool*.nupkg") + context.GetFiles($"{Paths.Nuget}/GitVersion.MsBuild*.nupkg")) - { - PackageContentsValidation.Validate(package.FullPath); - } + PackageContentsValidation.ValidatePackages( + context.GetFiles($"{Paths.Nuget}/GitVersion.Tool*.nupkg").Select(package => package.FullPath), + context.GetFiles($"{Paths.Nuget}/GitVersion.MsBuild*.nupkg").Select(package => package.FullPath)); } } From 7cf36faf0247fb64b16f49e5b40810bba453425d Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Wed, 21 Oct 2026 18:20:04 +0200 Subject: [PATCH 12/15] test: simplify managed query contract assertions for Sonar --- .../Core/ManagedRepositoryContractTests.cs | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/GitVersion.Core.Tests/Core/ManagedRepositoryContractTests.cs b/src/GitVersion.Core.Tests/Core/ManagedRepositoryContractTests.cs index 16d82fc7a5..169a0df3e0 100644 --- a/src/GitVersion.Core.Tests/Core/ManagedRepositoryContractTests.cs +++ b/src/GitVersion.Core.Tests/Core/ManagedRepositoryContractTests.cs @@ -72,28 +72,33 @@ public void QueryByPreservesReachabilityFirstParentAndSortContracts() FirstParentOnly = firstParent, SortBy = sort }; - var actual = repository.Commits.QueryBy(filter).ToArray(); - var args = new List { "rev-list", "main" }; - if (firstParent) - { - args.Add("--first-parent"); - } + AssertQueryByContract(fixture.Repository, repository, filter, excluded); + } + } + } + } - if (excluded != null) - { - args.Add("^" + excluded); - } + private static void AssertQueryByContract(FixtureGit.IRepository git, IGitRepository repository, CommitFilter filter, string? excluded) + { + var actual = repository.Commits.QueryBy(filter).ToArray(); + var args = new List { "rev-list", "main" }; + if (filter.FirstParentOnly) + { + args.Add("--first-parent"); + } - actual.Select(c => c.Sha).Order().ShouldBe(Lines(fixture.Repository.Run([.. args])).Order()); - if (sort.HasFlag(CommitSortStrategies.Topological)) - { - AssertTopological(actual); - } + if (excluded != null) + { + args.Add("^" + excluded); + } - repository.Commits.QueryBy(filter with { SortBy = sort | CommitSortStrategies.Reverse }).Select(c => c.Sha).ShouldBe(actual.Reverse().Select(c => c.Sha)); - } - } + actual.Select(c => c.Sha).Order().ShouldBe(Lines(git.Run([.. args])).Order()); + if (filter.SortBy.HasFlag(CommitSortStrategies.Topological)) + { + AssertTopological(actual); } + + repository.Commits.QueryBy(filter with { SortBy = filter.SortBy | CommitSortStrategies.Reverse }).Select(c => c.Sha).ShouldBe(actual.Reverse().Select(c => c.Sha)); } [Test] From 1b55951a067c9004968e367e9b9c00f5be5eee35 Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Wed, 21 Oct 2026 19:10:40 +0200 Subject: [PATCH 13/15] ci: preserve NUnit filters and balance macOS GitFlow tests --- build/build/Tasks/Test/UnitTest.cs | 3 +++ build/config/Tasks/SetMatrix.cs | 3 ++- build/test.runsettings | 6 ++++++ 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 build/test.runsettings diff --git a/build/build/Tasks/Test/UnitTest.cs b/build/build/Tasks/Test/UnitTest.cs index 7ce717530d..d50c628594 100644 --- a/build/build/Tasks/Test/UnitTest.cs +++ b/build/build/Tasks/Test/UnitTest.cs @@ -126,6 +126,9 @@ private static int TestProjectForTarget(BuildContext context, FilePath project, if (!string.IsNullOrWhiteSpace(filter)) { args.Append("--filter").AppendQuoted(filter); + // NUnit otherwise drops name filters above 2000 cases and executes + // excluded tests without including their outcomes in MTP's reports. + args.Append("--settings").AppendQuoted(context.MakeAbsolute(Paths.Build.CombineWithFilePath("test.runsettings")).FullPath); } if (context.BuildSystem().IsRunningOnGitHubActions) { diff --git a/build/config/Tasks/SetMatrix.cs b/build/config/Tasks/SetMatrix.cs index 93719fbf96..ca08c8a51c 100644 --- a/build/config/Tasks/SetMatrix.cs +++ b/build/config/Tasks/SetMatrix.cs @@ -27,7 +27,8 @@ public override void Run(BuildContext context) var groups = new[] { new { name = "Core integration", id = "core-integration", filter = "FullyQualifiedName~GitVersion.Tests.IntegrationTests&FullyQualifiedName!~Mainline" }, - new { name = "Core integration GitFlow", id = "core-integration-gitflow", filter = "FullyQualifiedName~GitVersion.Tests.IntegrationTests&FullyQualifiedName~Mainline&FullyQualifiedName~GitFlow" }, + new { name = "Core integration GitFlow True cases", id = "core-integration-gitflow-true", filter = "FullyQualifiedName~GitVersion.Tests.IntegrationTests&FullyQualifiedName~Mainline&FullyQualifiedName~GitFlow&FullyQualifiedName~True" }, + new { name = "Core integration GitFlow other cases", id = "core-integration-gitflow-other", filter = "FullyQualifiedName~GitVersion.Tests.IntegrationTests&FullyQualifiedName~Mainline&FullyQualifiedName~GitFlow&FullyQualifiedName!~True" }, new { name = "Core integration other", id = "core-integration-other", filter = "FullyQualifiedName~GitVersion.Tests.IntegrationTests&FullyQualifiedName~Mainline&FullyQualifiedName!~GitFlow" }, new { name = "Core remaining", id = "core-other", filter = "FullyQualifiedName!~GitVersion.Tests.IntegrationTests" } }; diff --git a/build/test.runsettings b/build/test.runsettings new file mode 100644 index 0000000000..d3b45c2127 --- /dev/null +++ b/build/test.runsettings @@ -0,0 +1,6 @@ + + + + 2147483647 + + From 3bb740a9c461089b481abbd8162fd3330a991877 Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Wed, 21 Oct 2026 19:48:46 +0200 Subject: [PATCH 14/15] ci: split remaining slow macOS integration selection --- build/config/Tasks/SetMatrix.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/config/Tasks/SetMatrix.cs b/build/config/Tasks/SetMatrix.cs index ca08c8a51c..62eef4d18d 100644 --- a/build/config/Tasks/SetMatrix.cs +++ b/build/config/Tasks/SetMatrix.cs @@ -29,7 +29,8 @@ public override void Run(BuildContext context) new { name = "Core integration", id = "core-integration", filter = "FullyQualifiedName~GitVersion.Tests.IntegrationTests&FullyQualifiedName!~Mainline" }, new { name = "Core integration GitFlow True cases", id = "core-integration-gitflow-true", filter = "FullyQualifiedName~GitVersion.Tests.IntegrationTests&FullyQualifiedName~Mainline&FullyQualifiedName~GitFlow&FullyQualifiedName~True" }, new { name = "Core integration GitFlow other cases", id = "core-integration-gitflow-other", filter = "FullyQualifiedName~GitVersion.Tests.IntegrationTests&FullyQualifiedName~Mainline&FullyQualifiedName~GitFlow&FullyQualifiedName!~True" }, - new { name = "Core integration other", id = "core-integration-other", filter = "FullyQualifiedName~GitVersion.Tests.IntegrationTests&FullyQualifiedName~Mainline&FullyQualifiedName!~GitFlow" }, + new { name = "Core integration other True cases", id = "core-integration-other-true", filter = "FullyQualifiedName~GitVersion.Tests.IntegrationTests&FullyQualifiedName~Mainline&FullyQualifiedName!~GitFlow&FullyQualifiedName~True" }, + new { name = "Core integration other remaining cases", id = "core-integration-other-remaining", filter = "FullyQualifiedName~GitVersion.Tests.IntegrationTests&FullyQualifiedName~Mainline&FullyQualifiedName!~GitFlow&FullyQualifiedName!~True" }, new { name = "Core remaining", id = "core-other", filter = "FullyQualifiedName!~GitVersion.Tests.IntegrationTests" } }; foreach (var group in groups) From 401e0645482bae67d746f9554fa2b6a053df0532 Mon Sep 17 00:00:00 2001 From: Artur Stolear Date: Wed, 21 Oct 2026 21:10:30 +0200 Subject: [PATCH 15/15] docs: describe Git CLI test oracles and recorded walk baselines --- src/GitVersion.Git.Managed.Tests/GitRevisionWalkerTests.cs | 6 +++--- src/GitVersion.Git.Managed.Tests/GitTreeDiffTests.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/GitVersion.Git.Managed.Tests/GitRevisionWalkerTests.cs b/src/GitVersion.Git.Managed.Tests/GitRevisionWalkerTests.cs index bb045e28df..0676712951 100644 --- a/src/GitVersion.Git.Managed.Tests/GitRevisionWalkerTests.cs +++ b/src/GitVersion.Git.Managed.Tests/GitRevisionWalkerTests.cs @@ -3,9 +3,9 @@ namespace GitVersion.Git.Managed.Tests; /// -/// Validates ordering and merge-base results against libgit2 -/// (via LibGit2Sharp) on the same fixture repositories — the parity GitVersion's version -/// calculation depends on. +/// Validates ordering against recorded LibGit2Sharp baselines +/// and merge-base results against Git CLI on the same fixture repositories, preserving +/// the history semantics GitVersion's version calculation depends on. /// [TestFixture] public class GitRevisionWalkerTests diff --git a/src/GitVersion.Git.Managed.Tests/GitTreeDiffTests.cs b/src/GitVersion.Git.Managed.Tests/GitTreeDiffTests.cs index 37452275ab..cdb1c29282 100644 --- a/src/GitVersion.Git.Managed.Tests/GitTreeDiffTests.cs +++ b/src/GitVersion.Git.Managed.Tests/GitTreeDiffTests.cs @@ -2,8 +2,8 @@ namespace GitVersion.Git.Managed.Tests; /// -/// Validates against libgit2's default TreeChanges path -/// list, which is what GitVersion exposes as ICommit.DiffPaths. +/// Validates changed paths against Git CLI results, +/// including first-parent merge diffs, as exposed by ICommit.DiffPaths. /// [TestFixture] public class GitTreeDiffTests