From b353c984081e63f5d79cd2931dd6f7c0480eb7e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 2 May 2026 14:05:46 +0000 Subject: [PATCH 1/7] test: demonstrate NRE in GetTags() with annotated tags GetTags() casts tag.Target directly to Commit. For annotated tags, tag.Target is a TagAnnotation, so the cast yields null and the null-forgiving ! operator causes a NullReferenceException at runtime. UnityCsReference uses annotated tags, making this crash systematic. https://claude.ai/code/session_01WNaTJnpDwNjqyfL1uhYqJ4 --- .../RepositoryExtensionsTests.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 UnityXrefMaps.Tests/RepositoryExtensionsTests.cs diff --git a/UnityXrefMaps.Tests/RepositoryExtensionsTests.cs b/UnityXrefMaps.Tests/RepositoryExtensionsTests.cs new file mode 100644 index 0000000..3daf290 --- /dev/null +++ b/UnityXrefMaps.Tests/RepositoryExtensionsTests.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.IO; +using LibGit2Sharp; + +namespace UnityXrefMaps.Tests; + +public sealed class RepositoryExtensionsTests : IDisposable +{ + private readonly string _tempPath; + private readonly Repository _repository; + + public RepositoryExtensionsTests() + { + _tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(_tempPath); + Repository.Init(_tempPath); + _repository = new Repository(_tempPath); + } + + /// + /// Annotated tags (created with a message) have a TagAnnotation as their Target, not a Commit. + /// GetTags() was casting Target directly to Commit via (tag.Target as Commit)!, which returned + /// null for annotated tags and then threw NullReferenceException on .Author.When. + /// Unity's UnityCsReference repository uses annotated tags. + /// + [Fact] + public void GetTags_WithAnnotatedTag_ReturnsTagName() + { + // Arrange: one commit + one annotated tag (tag.Target is TagAnnotation, not Commit) + File.WriteAllText(Path.Combine(_tempPath, "file.txt"), "content"); + Commands.Stage(_repository, "*"); + var signature = new Signature("test", "test@test.com", DateTimeOffset.UtcNow); + Commit commit = _repository.Commit("Initial commit", signature, signature); + _repository.Tags.Add("6000.0.1f1", commit, signature, "Unity 6000.0.1f1 release"); + + // Act + IEnumerable tags = _repository.GetTags(); + + // Assert + Assert.Contains("6000.0.1f1", tags); + } + + public void Dispose() + { + _repository.Dispose(); + try { Directory.Delete(_tempPath, recursive: true); } catch { } + } +} From 6872cc581076a6c4be30973edc7c909392268185 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 2 May 2026 14:06:05 +0000 Subject: [PATCH 2/7] fix: peel through TagAnnotation in GetTags() to avoid NRE For annotated tags, tag.Target is a TagAnnotation (not a Commit), so casting it directly to Commit returned null and the null-forgiving ! caused a NullReferenceException. Extracted GetTaggedCommit() which traverses the annotation chain until it reaches the underlying Commit, handling both lightweight and annotated tags (including nested ones). Fixes the crash that prevented --repositoryTags auto-discovery from working with UnityCsReference, which exclusively uses annotated tags. https://claude.ai/code/session_01WNaTJnpDwNjqyfL1uhYqJ4 --- UnityXrefMaps/RepositoryExtensions.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/UnityXrefMaps/RepositoryExtensions.cs b/UnityXrefMaps/RepositoryExtensions.cs index e3fcda9..c5055c5 100644 --- a/UnityXrefMaps/RepositoryExtensions.cs +++ b/UnityXrefMaps/RepositoryExtensions.cs @@ -20,10 +20,20 @@ internal static partial class RepositoryExtensions public static IEnumerable GetTags(this Repository repository) { return repository.Tags - .OrderByDescending(tag => (tag.Target as Commit)!.Author.When) + .OrderByDescending(tag => GetTaggedCommit(tag)?.Author.When ?? DateTimeOffset.MinValue) .Select(tag => tag.FriendlyName); } + // Lightweight tags point directly to a Commit; annotated tags point to a TagAnnotation + // whose own Target is the Commit (possibly through multiple layers of annotation). + private static Commit? GetTaggedCommit(Tag tag) + { + GitObject target = tag.Target; + while (target is TagAnnotation annotation) + target = annotation.Target; + return target as Commit; + } + /// /// Hard resets the specified to the specified commit. /// @@ -42,8 +52,8 @@ public static void HardReset(this Repository repository, string commit, ILogger repository.RemoveUntrackedFiles(); } catch (Exception) { } - } - + } + public static IEnumerable<(string name, string release)> GetLatestVersions(this Repository unityRepository) { return unityRepository From 063818ec0caf7620c8a6d938e9f55ecdc0a55b33 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 2 May 2026 14:11:57 +0000 Subject: [PATCH 3/7] fix(test): add LibGit2Sharp as direct dependency to test project RepositoryExtensionsTests creates Repository instances directly, requiring LibGit2Sharp types and its native binaries to be explicitly present in the test output. Relying on the transitive reference through UnityXrefMaps.csproj is not sufficient for native library resolution. https://claude.ai/code/session_01WNaTJnpDwNjqyfL1uhYqJ4 --- UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj b/UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj index 62ef39c..ccc2e1b 100644 --- a/UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj +++ b/UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj @@ -8,6 +8,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive From 6c31153649c0a6fcaeaffe31c2e3b89174781ad1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 2 May 2026 14:17:33 +0000 Subject: [PATCH 4/7] fix(test): use explicit filename in Commands.Stage Commands.Stage with the "*" glob pathspec may not reliably match untracked files in LibGit2Sharp; using the explicit filename avoids a potential empty index that would cause Commit() to throw. https://claude.ai/code/session_01WNaTJnpDwNjqyfL1uhYqJ4 --- UnityXrefMaps.Tests/RepositoryExtensionsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnityXrefMaps.Tests/RepositoryExtensionsTests.cs b/UnityXrefMaps.Tests/RepositoryExtensionsTests.cs index 3daf290..bd3fe92 100644 --- a/UnityXrefMaps.Tests/RepositoryExtensionsTests.cs +++ b/UnityXrefMaps.Tests/RepositoryExtensionsTests.cs @@ -29,7 +29,7 @@ public void GetTags_WithAnnotatedTag_ReturnsTagName() { // Arrange: one commit + one annotated tag (tag.Target is TagAnnotation, not Commit) File.WriteAllText(Path.Combine(_tempPath, "file.txt"), "content"); - Commands.Stage(_repository, "*"); + Commands.Stage(_repository, "file.txt"); var signature = new Signature("test", "test@test.com", DateTimeOffset.UtcNow); Commit commit = _repository.Commit("Initial commit", signature, signature); _repository.Tags.Add("6000.0.1f1", commit, signature, "Unity 6000.0.1f1 release"); From 3e5584a7f523c6bdfbf8cb89842095972bea7cb3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 2 May 2026 14:36:13 +0000 Subject: [PATCH 5/7] fix(test): use Index API for staging and string SHA for tag creation Replace Commands.Stage with Index.Add/Write (lower-level, unambiguous) and use commit.Sha (string objectish) instead of passing the Commit object directly to Tags.Add, removing any overload resolution ambiguity. Also use var to avoid shadowing the Commit type with a same-named variable. https://claude.ai/code/session_01WNaTJnpDwNjqyfL1uhYqJ4 --- UnityXrefMaps.Tests/RepositoryExtensionsTests.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/UnityXrefMaps.Tests/RepositoryExtensionsTests.cs b/UnityXrefMaps.Tests/RepositoryExtensionsTests.cs index bd3fe92..d170639 100644 --- a/UnityXrefMaps.Tests/RepositoryExtensionsTests.cs +++ b/UnityXrefMaps.Tests/RepositoryExtensionsTests.cs @@ -28,11 +28,12 @@ public RepositoryExtensionsTests() public void GetTags_WithAnnotatedTag_ReturnsTagName() { // Arrange: one commit + one annotated tag (tag.Target is TagAnnotation, not Commit) - File.WriteAllText(Path.Combine(_tempPath, "file.txt"), "content"); - Commands.Stage(_repository, "file.txt"); var signature = new Signature("test", "test@test.com", DateTimeOffset.UtcNow); - Commit commit = _repository.Commit("Initial commit", signature, signature); - _repository.Tags.Add("6000.0.1f1", commit, signature, "Unity 6000.0.1f1 release"); + File.WriteAllText(Path.Combine(_tempPath, "file.txt"), "content"); + _repository.Index.Add("file.txt"); + _repository.Index.Write(); + var commit = _repository.Commit("Initial commit", signature, signature); + _repository.Tags.Add("6000.0.1f1", commit.Sha, signature, "Unity 6000.0.1f1 release"); // Act IEnumerable tags = _repository.GetTags(); From 9bb8f5929a9c41bd41e40da7ec41ff724867fd01 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 2 May 2026 19:10:49 +0000 Subject: [PATCH 6/7] test: cover all GetTaggedCommit code paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds three tests alongside the existing annotated-tag regression test: - GetTags_WithLightweightTag_ReturnsTagName: tag.Target is Commit directly, while-loop in GetTaggedCommit never iterates. - GetTags_WithNestedAnnotatedTag_ReturnsTagName: outer TagAnnotation → inner TagAnnotation → Commit, loop iterates twice (multi-level peeling). - GetTags_WithTagNotPointingToCommit_IsStillReturned: tag points to a blob, GetTaggedCommit returns null, tag is still present sorted last. https://claude.ai/code/session_01WNaTJnpDwNjqyfL1uhYqJ4 --- .../RepositoryExtensionsTests.cs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/UnityXrefMaps.Tests/RepositoryExtensionsTests.cs b/UnityXrefMaps.Tests/RepositoryExtensionsTests.cs index d170639..9eaccd4 100644 --- a/UnityXrefMaps.Tests/RepositoryExtensionsTests.cs +++ b/UnityXrefMaps.Tests/RepositoryExtensionsTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using LibGit2Sharp; namespace UnityXrefMaps.Tests; @@ -42,6 +43,59 @@ public void GetTags_WithAnnotatedTag_ReturnsTagName() Assert.Contains("6000.0.1f1", tags); } + [Fact] + public void GetTags_WithLightweightTag_ReturnsTagName() + { + // Arrange: lightweight tag (tag.Target is the Commit directly, while-loop never iterates) + var signature = new Signature("test", "test@test.com", DateTimeOffset.UtcNow); + File.WriteAllText(Path.Combine(_tempPath, "file.txt"), "content"); + _repository.Index.Add("file.txt"); + _repository.Index.Write(); + var commit = _repository.Commit("Initial commit", signature, signature); + _repository.Tags.Add("2023.1.0f1", commit); + + // Act + IEnumerable tags = _repository.GetTags(); + + // Assert + Assert.Contains("2023.1.0f1", tags); + } + + [Fact] + public void GetTags_WithNestedAnnotatedTag_ReturnsTagName() + { + // Arrange: outer annotated tag → inner TagAnnotation → Commit (while-loop iterates twice) + var signature = new Signature("test", "test@test.com", DateTimeOffset.UtcNow); + File.WriteAllText(Path.Combine(_tempPath, "file.txt"), "content"); + _repository.Index.Add("file.txt"); + _repository.Index.Write(); + var commit = _repository.Commit("Initial commit", signature, signature); + var innerTag = _repository.Tags.Add("inner", commit.Sha, signature, "Inner annotated tag"); + _repository.Tags.Add("outer", innerTag.Target.Sha, signature, "Outer tag pointing to inner annotation"); + + // Act + IEnumerable tags = _repository.GetTags(); + + // Assert + Assert.Contains("outer", tags); + } + + [Fact] + public void GetTags_WithTagNotPointingToCommit_IsStillReturned() + { + // Arrange: tag pointing to a blob (not a Commit); GetTaggedCommit returns null, + // so the tag sorts last with DateTimeOffset.MinValue but still appears in output. + using var stream = new MemoryStream(Encoding.UTF8.GetBytes("blob content")); + var blob = _repository.ObjectDatabase.CreateBlob(stream); + _repository.Tags.Add("blob-tag", blob); + + // Act + IEnumerable tags = _repository.GetTags(); + + // Assert + Assert.Contains("blob-tag", tags); + } + public void Dispose() { _repository.Dispose(); From 84a565cca4350e38f9809b1660e9f7ee18fbb43f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 2 May 2026 19:17:37 +0000 Subject: [PATCH 7/7] refactor: extract CreateCommit() helper to remove test boilerplate Three of the four tests shared the same 5-line commit-creation block (signature, write file, stage, write index, commit). Extracted into a private CreateCommit() helper returning the commit and signature tuple. https://claude.ai/code/session_01WNaTJnpDwNjqyfL1uhYqJ4 --- .../RepositoryExtensionsTests.cs | 59 ++++++------------- 1 file changed, 19 insertions(+), 40 deletions(-) diff --git a/UnityXrefMaps.Tests/RepositoryExtensionsTests.cs b/UnityXrefMaps.Tests/RepositoryExtensionsTests.cs index 9eaccd4..98b7645 100644 --- a/UnityXrefMaps.Tests/RepositoryExtensionsTests.cs +++ b/UnityXrefMaps.Tests/RepositoryExtensionsTests.cs @@ -28,72 +28,42 @@ public RepositoryExtensionsTests() [Fact] public void GetTags_WithAnnotatedTag_ReturnsTagName() { - // Arrange: one commit + one annotated tag (tag.Target is TagAnnotation, not Commit) - var signature = new Signature("test", "test@test.com", DateTimeOffset.UtcNow); - File.WriteAllText(Path.Combine(_tempPath, "file.txt"), "content"); - _repository.Index.Add("file.txt"); - _repository.Index.Write(); - var commit = _repository.Commit("Initial commit", signature, signature); + var (commit, signature) = CreateCommit(); _repository.Tags.Add("6000.0.1f1", commit.Sha, signature, "Unity 6000.0.1f1 release"); - // Act - IEnumerable tags = _repository.GetTags(); - - // Assert - Assert.Contains("6000.0.1f1", tags); + Assert.Contains("6000.0.1f1", _repository.GetTags()); } [Fact] public void GetTags_WithLightweightTag_ReturnsTagName() { - // Arrange: lightweight tag (tag.Target is the Commit directly, while-loop never iterates) - var signature = new Signature("test", "test@test.com", DateTimeOffset.UtcNow); - File.WriteAllText(Path.Combine(_tempPath, "file.txt"), "content"); - _repository.Index.Add("file.txt"); - _repository.Index.Write(); - var commit = _repository.Commit("Initial commit", signature, signature); + // tag.Target is the Commit directly; while-loop in GetTaggedCommit never iterates + var (commit, _) = CreateCommit(); _repository.Tags.Add("2023.1.0f1", commit); - // Act - IEnumerable tags = _repository.GetTags(); - - // Assert - Assert.Contains("2023.1.0f1", tags); + Assert.Contains("2023.1.0f1", _repository.GetTags()); } [Fact] public void GetTags_WithNestedAnnotatedTag_ReturnsTagName() { - // Arrange: outer annotated tag → inner TagAnnotation → Commit (while-loop iterates twice) - var signature = new Signature("test", "test@test.com", DateTimeOffset.UtcNow); - File.WriteAllText(Path.Combine(_tempPath, "file.txt"), "content"); - _repository.Index.Add("file.txt"); - _repository.Index.Write(); - var commit = _repository.Commit("Initial commit", signature, signature); + // outer annotated tag → inner TagAnnotation → Commit; while-loop iterates twice + var (commit, signature) = CreateCommit(); var innerTag = _repository.Tags.Add("inner", commit.Sha, signature, "Inner annotated tag"); _repository.Tags.Add("outer", innerTag.Target.Sha, signature, "Outer tag pointing to inner annotation"); - // Act - IEnumerable tags = _repository.GetTags(); - - // Assert - Assert.Contains("outer", tags); + Assert.Contains("outer", _repository.GetTags()); } [Fact] public void GetTags_WithTagNotPointingToCommit_IsStillReturned() { - // Arrange: tag pointing to a blob (not a Commit); GetTaggedCommit returns null, - // so the tag sorts last with DateTimeOffset.MinValue but still appears in output. + // GetTaggedCommit returns null for a blob target; tag sorts last but still appears in output using var stream = new MemoryStream(Encoding.UTF8.GetBytes("blob content")); var blob = _repository.ObjectDatabase.CreateBlob(stream); _repository.Tags.Add("blob-tag", blob); - // Act - IEnumerable tags = _repository.GetTags(); - - // Assert - Assert.Contains("blob-tag", tags); + Assert.Contains("blob-tag", _repository.GetTags()); } public void Dispose() @@ -101,4 +71,13 @@ public void Dispose() _repository.Dispose(); try { Directory.Delete(_tempPath, recursive: true); } catch { } } + + private (Commit commit, Signature signature) CreateCommit() + { + var signature = new Signature("test", "test@test.com", DateTimeOffset.UtcNow); + File.WriteAllText(Path.Combine(_tempPath, "file.txt"), "content"); + _repository.Index.Add("file.txt"); + _repository.Index.Write(); + return (_repository.Commit("Initial commit", signature, signature), signature); + } }