From bd47a66de8292880f9c44684caa918747f2b3a9c Mon Sep 17 00:00:00 2001 From: Pascal van Buijtene Date: Tue, 7 Oct 2025 16:58:42 +0200 Subject: [PATCH] Reduce memory usage by caching git objects --- src/GitVersion.LibGit2Sharp/Git/Branch.cs | 8 +++-- .../Git/BranchCollection.cs | 8 +++-- src/GitVersion.LibGit2Sharp/Git/Commit.cs | 6 ++-- .../Git/CommitCollection.cs | 8 +++-- .../Git/GitRepository.cs | 35 ++++++++++++++++--- src/GitVersion.LibGit2Sharp/Git/Tag.cs | 6 ++-- .../Git/TagCollection.cs | 8 +++-- 7 files changed, 58 insertions(+), 21 deletions(-) diff --git a/src/GitVersion.LibGit2Sharp/Git/Branch.cs b/src/GitVersion.LibGit2Sharp/Git/Branch.cs index 088f4674c7..bfa286cad8 100644 --- a/src/GitVersion.LibGit2Sharp/Git/Branch.cs +++ b/src/GitVersion.LibGit2Sharp/Git/Branch.cs @@ -10,16 +10,18 @@ internal sealed class Branch : IBranch private readonly LibGit2Sharp.Branch innerBranch; - internal Branch(LibGit2Sharp.Branch branch, LibGit2Sharp.Diff diff) + internal Branch(LibGit2Sharp.Branch branch, LibGit2Sharp.Diff diff, GitRepository repo) { + diff.NotNull(); + repo.NotNull(); this.innerBranch = branch.NotNull(); Name = new(branch.CanonicalName); var commit = this.innerBranch.Tip; - Tip = commit is null ? null : new Commit(commit, diff); + Tip = commit is null ? null : repo.GetOrCreate(commit, diff); var commits = this.innerBranch.Commits; - Commits = new CommitCollection(commits, diff); + Commits = new CommitCollection(commits, diff, repo); } public ReferenceName Name { get; } diff --git a/src/GitVersion.LibGit2Sharp/Git/BranchCollection.cs b/src/GitVersion.LibGit2Sharp/Git/BranchCollection.cs index f2abcfd5b4..b8b1df839d 100644 --- a/src/GitVersion.LibGit2Sharp/Git/BranchCollection.cs +++ b/src/GitVersion.LibGit2Sharp/Git/BranchCollection.cs @@ -8,12 +8,14 @@ internal sealed class BranchCollection : IBranchCollection private readonly LibGit2Sharp.BranchCollection innerCollection; private readonly Lazy> branches; private readonly Diff diff; + private readonly GitRepository repo; - internal BranchCollection(LibGit2Sharp.BranchCollection collection, Diff diff) + internal BranchCollection(LibGit2Sharp.BranchCollection collection, Diff diff, GitRepository repo) { this.innerCollection = collection.NotNull(); - this.branches = new Lazy>(() => [.. this.innerCollection.Select(branch => new Branch(branch, diff))]); + this.branches = new Lazy>(() => [.. this.innerCollection.Select(branch => repo.GetOrCreate(branch, diff))]); this.diff = diff.NotNull(); + this.repo = repo.NotNull(); } public IEnumerator GetEnumerator() @@ -27,7 +29,7 @@ public IBranch? this[string name] { name = name.NotNull(); var branch = this.innerCollection[name]; - return branch is null ? null : new Branch(branch, this.diff); + return branch is null ? null : this.repo.GetOrCreate(branch, this.diff); } } diff --git a/src/GitVersion.LibGit2Sharp/Git/Commit.cs b/src/GitVersion.LibGit2Sharp/Git/Commit.cs index eea385bd54..60bb853dd7 100644 --- a/src/GitVersion.LibGit2Sharp/Git/Commit.cs +++ b/src/GitVersion.LibGit2Sharp/Git/Commit.cs @@ -14,10 +14,12 @@ internal sealed class Commit : GitObject, ICommit private readonly LibGit2Sharp.Commit innerCommit; private readonly LibGit2Sharp.Diff repoDiff; - internal Commit(LibGit2Sharp.Commit innerCommit, LibGit2Sharp.Diff repoDiff) : base(innerCommit) + internal Commit(LibGit2Sharp.Commit innerCommit, LibGit2Sharp.Diff repoDiff, GitRepository repo) : base(innerCommit) { + repoDiff.NotNull(); + repo.NotNull(); this.innerCommit = innerCommit.NotNull(); - this.parentsLazy = new(() => innerCommit.Parents.Select(parent => new Commit(parent, repoDiff)).ToList()); + this.parentsLazy = new(() => innerCommit.Parents.Select(parent => repo.GetOrCreate(parent, repoDiff)).ToList()); When = innerCommit.Committer.When; this.repoDiff = repoDiff; } diff --git a/src/GitVersion.LibGit2Sharp/Git/CommitCollection.cs b/src/GitVersion.LibGit2Sharp/Git/CommitCollection.cs index d1b5549037..05d9940f94 100644 --- a/src/GitVersion.LibGit2Sharp/Git/CommitCollection.cs +++ b/src/GitVersion.LibGit2Sharp/Git/CommitCollection.cs @@ -8,12 +8,14 @@ internal sealed class CommitCollection : ICommitCollection private readonly ICommitLog innerCollection; private readonly Lazy> commits; private readonly Diff diff; + private readonly GitRepository repo; - internal CommitCollection(ICommitLog collection, Diff diff) + internal CommitCollection(ICommitLog collection, Diff diff, GitRepository repo) { this.innerCollection = collection.NotNull(); - this.commits = new Lazy>(() => [.. this.innerCollection.Select(commit => new Commit(commit, diff))]); + this.commits = new Lazy>(() => [.. this.innerCollection.Select(commit => repo.GetOrCreate(commit, diff))]); this.diff = diff.NotNull(); + this.repo = repo.NotNull(); } public IEnumerator GetEnumerator() @@ -36,7 +38,7 @@ public IEnumerable QueryBy(CommitFilter commitFilter) SortBy = (LibGit2Sharp.CommitSortStrategies)commitFilter.SortBy }; var commitLog = ((IQueryableCommitLog)this.innerCollection).QueryBy(filter); - return new CommitCollection(commitLog, this.diff); + return new CommitCollection(commitLog, this.diff, this.repo); static object? GetReacheableFrom(object? item) => item switch diff --git a/src/GitVersion.LibGit2Sharp/Git/GitRepository.cs b/src/GitVersion.LibGit2Sharp/Git/GitRepository.cs index 3d2fd756ed..115bceb44c 100644 --- a/src/GitVersion.LibGit2Sharp/Git/GitRepository.cs +++ b/src/GitVersion.LibGit2Sharp/Git/GitRepository.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using GitVersion.Extensions; using GitVersion.Helpers; using LibGit2Sharp; @@ -8,6 +9,10 @@ internal sealed partial class GitRepository { private Lazy? repositoryLazy; + private readonly ConcurrentDictionary cachedBranches = new(); + private readonly ConcurrentDictionary cachedCommits = new(); + private readonly ConcurrentDictionary cachedTags = new(); + private IRepository RepositoryInstance { get @@ -20,12 +25,12 @@ private IRepository RepositoryInstance public string WorkingDirectory => RepositoryInstance.Info.WorkingDirectory; public bool IsHeadDetached => RepositoryInstance.Info.IsHeadDetached; public bool IsShallow => RepositoryInstance.Info.IsShallow; - public IBranch Head => new Branch(RepositoryInstance.Head, RepositoryInstance.Diff); + public IBranch Head => GetOrCreate(RepositoryInstance.Head, RepositoryInstance.Diff); - public ITagCollection Tags => new TagCollection(RepositoryInstance.Tags, RepositoryInstance.Diff); + public ITagCollection Tags => new TagCollection(RepositoryInstance.Tags, RepositoryInstance.Diff, this); public IReferenceCollection Refs => new ReferenceCollection(RepositoryInstance.Refs); - public IBranchCollection Branches => new BranchCollection(RepositoryInstance.Branches, RepositoryInstance.Diff); - public ICommitCollection Commits => new CommitCollection(RepositoryInstance.Commits, RepositoryInstance.Diff); + public IBranchCollection Branches => new BranchCollection(RepositoryInstance.Branches, RepositoryInstance.Diff, this); + public ICommitCollection Commits => new CommitCollection(RepositoryInstance.Commits, RepositoryInstance.Diff, this); public IRemoteCollection Remotes => new RemoteCollection(RepositoryInstance.Network.Remotes); public void DiscoverRepository(string? gitDirectory) @@ -48,7 +53,7 @@ public void DiscoverRepository(string? gitDirectory) var first = (Commit)commit; var second = (Commit)otherCommit; var mergeBase = RepositoryInstance.ObjectDatabase.FindMergeBase(first, second); - return mergeBase == null ? null : new Commit(mergeBase, RepositoryInstance.Diff); + return mergeBase == null ? null : GetOrCreate(mergeBase, RepositoryInstance.Diff); }); } @@ -58,6 +63,26 @@ public int UncommittedChangesCount() return retryAction.Execute(GetUncommittedChangesCountInternal); } + public Branch GetOrCreate(LibGit2Sharp.Branch innerBranch, Diff repoDiff) + { + if (innerBranch.Tip is null) + { + return new Branch(innerBranch, repoDiff, this); + } + + var cacheKey = $"{innerBranch.CanonicalName}|{innerBranch.Tip.Sha}|{innerBranch.RemoteName}"; + return cachedBranches.GetOrAdd(cacheKey, new Branch(innerBranch, repoDiff, this)); + } + + public Commit GetOrCreate(LibGit2Sharp.Commit innerCommit, Diff repoDiff) => + cachedCommits.GetOrAdd(innerCommit.Sha, new Commit(innerCommit, repoDiff, this)); + + public Tag GetOrCreate(LibGit2Sharp.Tag innerTag, Diff repoDiff) + { + var cacheKey = $"{innerTag.CanonicalName}|{innerTag.Target.Sha}"; + return cachedTags.GetOrAdd(cacheKey, new Tag(innerTag, repoDiff, this)); + } + public void Dispose() { if (this.repositoryLazy is { IsValueCreated: true }) RepositoryInstance.Dispose(); diff --git a/src/GitVersion.LibGit2Sharp/Git/Tag.cs b/src/GitVersion.LibGit2Sharp/Git/Tag.cs index 8b891ef231..89fb6d777a 100644 --- a/src/GitVersion.LibGit2Sharp/Git/Tag.cs +++ b/src/GitVersion.LibGit2Sharp/Git/Tag.cs @@ -11,12 +11,14 @@ internal sealed class Tag : ITag private readonly LibGit2Sharp.Tag innerTag; private readonly Diff diff; private readonly Lazy commitLazy; + private readonly GitRepository repo; - internal Tag(LibGit2Sharp.Tag tag, Diff diff) + internal Tag(LibGit2Sharp.Tag tag, Diff diff, GitRepository repo) { this.innerTag = tag.NotNull(); this.commitLazy = new(PeeledTargetCommit); this.diff = diff.NotNull(); + this.repo = repo.NotNull(); Name = new(this.innerTag.CanonicalName); } @@ -35,7 +37,7 @@ internal Tag(LibGit2Sharp.Tag tag, Diff diff) target = annotation.Target; } - return target is LibGit2Sharp.Commit commit ? new Commit(commit, this.diff) : null; + return target is LibGit2Sharp.Commit commit ? this.repo.GetOrCreate(commit, this.diff) : null; } public override bool Equals(object? obj) => Equals(obj as ITag); diff --git a/src/GitVersion.LibGit2Sharp/Git/TagCollection.cs b/src/GitVersion.LibGit2Sharp/Git/TagCollection.cs index bac1b4f790..ec339e28b3 100644 --- a/src/GitVersion.LibGit2Sharp/Git/TagCollection.cs +++ b/src/GitVersion.LibGit2Sharp/Git/TagCollection.cs @@ -6,10 +6,12 @@ internal sealed class TagCollection : ITagCollection { private readonly Lazy> tags; - internal TagCollection(LibGit2Sharp.TagCollection collection, LibGit2Sharp.Diff diff) + internal TagCollection(LibGit2Sharp.TagCollection collection, LibGit2Sharp.Diff diff, GitRepository repo) { - collection = collection.NotNull(); - this.tags = new Lazy>(() => [.. collection.Select(tag => new Tag(tag, diff))]); + collection.NotNull(); + diff.NotNull(); + repo.NotNull(); + this.tags = new Lazy>(() => [.. collection.Select(tag => repo.GetOrCreate(tag, diff))]); } public IEnumerator GetEnumerator()