You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
❌ "alternatively make the isClean-property to be lazily evaluated, so it's only computed if it is needed"
❌ "retrieving the tags takes some time because it iterates the whole log ... maybe it is enough to partial-compute the data, only for the relevant tags"
I hit the same wall and implemented the two remaining suggestions, plus an optional native-git read path. #1074 covers the lazy isClean part. This issue is about the rest, since it adds public DSL surface and I would rather agree on the approach before opening a ~1000 line PR.
Measuring inside the real build was useless — the configuration phase swings between 50s and 104s regardless of backend, because the Android plugins dominate. So I benchmarked in an isolated single-module project pointing at the same .git, warm daemon, alternating runs. That is reproducible to within 30 ms.
plus -Prelease.scmBackend=nativeGit. Default stays jgit, so no behaviour change unless opted in.
ScmRepositoryFactory picks the implementation; a new NativeGitRepository implements the read half of ScmRepository and delegates every write to the existing JGit GitRepository.
Why hybrid rather than a full native rewrite
A full rewrite would also have to reimplement push/fetch/commit and the remote authentication that TransportConfigCallback / ScmIdentity does today (ssh keys, in-memory PGP, token auth). Replacing that with ambient credential and ssh config would be a real, security-sensitive behaviour change. Since all of the measured cost is in reads, delegating writes keeps release tagging and pushing bit-for-bit unchanged.
Implementation notes
Configuration cache. Reads go through ProviderFactory.exec, not ProcessBuilder — raw process execution during configuration is a hard configuration-cache failure ("external process started ...").
Bounded tag walk. This is Retrieving the version for large repositories is slow #736's "partial-compute" suggestion. providers.exec cannot stream, so the walk reads a bounded chunk (rev-list --max-count=2000) and only falls back to the full history if that chunk yields no match and more history exists. Exact, not approximate. On this repository the nearest tag is 103 commits from HEAD, and the bounded call costs 0.042s versus 0.660s.
Semantics preserved. Nearest reachable tagged commit, annotated tags peeled via %(*objectname), for-each-ref ref-name ordering matching tagList(), HEAD when detached, overriddenBranchName / GITHUB_HEAD_REF / overriddenIsClean / releaseBranchNames all honoured, :(exclude) pathspecs for excludeSubFolders.
Hermetic.GIT_CONFIG_GLOBAL / GIT_CONFIG_SYSTEM are neutralised when ignoreGlobalGitConfig is set, mirroring SystemReaderWithoutSystemConfig. GIT_OPTIONAL_LOCKS=0 avoids taking index.lock (measured: no cost).
Requiresgit on PATH when enabled.
Alternatives considered
Caching GitRepository results — discussed at length in Negative impact on configuration phase performance #182. It would help both backends and cut more than this does: resolving one version currently issues 70 git operations, including 14 working-tree scans and 8 tag walks. But it changes staleness semantics after release creates a tag, and Negative impact on configuration phase performance #182 notes the plugin is applied per-module with different tag prefixes, which complicates the cache key. Happy to explore this instead if you prefer.
This only pays off on large repositories. Resolving a version spawns roughly a dozen git processes, so on small repositories the native backend is a wash or marginally slower. That is why it is opt-in.
The win only materialises on configuration-cache misses. On a cache hit axion does not run at all.
The remaining hotspot after both changes is currentPosition() recomputing isClean (10 of the 14 scans). Fixing that means making ScmPosition.isClean lazy, which touches a user-visible @Input — deliberately out of scope here.
Status
Implemented and green: NativeGitRepositoryTest mirrors the read-path scenarios of GitRepositoryTest and additionally asserts output parity with the JGit backend on the same fixture repository, plus integration tests for the DSL flag, the Gradle property and an unknown backend value.
Would you accept a PR along these lines? I am happy to change the DSL naming (backend vs something else), make it a Gradle-property-only escape hatch rather than DSL, or drop it in favour of the caching approach from #182.
Context
Follow-up to #736 and #182, both of which reported that version resolution dominates the configuration phase on large repositories.
#736 proposed three fixes. Only the first landed:
isClean→overriddenIsClean(ScmPosition: added isClean #543)I hit the same wall and implemented the two remaining suggestions, plus an optional native-git read path. #1074 covers the lazy
isCleanpart. This issue is about the rest, since it adds public DSL surface and I would rather agree on the approach before opening a ~1000 line PR.Measurements
Repository: 1 GB pack, 169,177 commits, 745 tags, ~200 Gradle modules.
Measuring inside the real build was useless — the configuration phase swings between 50s and 104s regardless of backend, because the Android plugins dominate. So I benchmarked in an isolated single-module project pointing at the same
.git, warm daemon, alternating runs. That is reproducible to within 30 ms.maintodayCost of the underlying commands on that repository:
Note that tags are already packed here, so the
git gc/packed-refsworkaround from #736 does not apply — the walk itself is the cost.Proposal: hybrid backend, opt-in
scmVersion { repository { backend.set("nativeGit") // default: "jgit" } }plus
-Prelease.scmBackend=nativeGit. Default staysjgit, so no behaviour change unless opted in.ScmRepositoryFactorypicks the implementation; a newNativeGitRepositoryimplements the read half ofScmRepositoryand delegates every write to the existing JGitGitRepository.Why hybrid rather than a full native rewrite
A full rewrite would also have to reimplement push/fetch/commit and the remote authentication that
TransportConfigCallback/ScmIdentitydoes today (ssh keys, in-memory PGP, token auth). Replacing that with ambient credential and ssh config would be a real, security-sensitive behaviour change. Since all of the measured cost is in reads, delegating writes keeps release tagging and pushing bit-for-bit unchanged.Implementation notes
ProviderFactory.exec, notProcessBuilder— raw process execution during configuration is a hard configuration-cache failure ("external process started ...").providers.execcannot stream, so the walk reads a bounded chunk (rev-list --max-count=2000) and only falls back to the full history if that chunk yields no match and more history exists. Exact, not approximate. On this repository the nearest tag is 103 commits from HEAD, and the bounded call costs 0.042s versus 0.660s.%(*objectname),for-each-refref-name ordering matchingtagList(),HEADwhen detached,overriddenBranchName/GITHUB_HEAD_REF/overriddenIsClean/releaseBranchNamesall honoured,:(exclude)pathspecs forexcludeSubFolders.GIT_CONFIG_GLOBAL/GIT_CONFIG_SYSTEMare neutralised whenignoreGlobalGitConfigis set, mirroringSystemReaderWithoutSystemConfig.GIT_OPTIONAL_LOCKS=0avoids takingindex.lock(measured: no cost).gitonPATHwhen enabled.Alternatives considered
GitRepositoryresults — discussed at length in Negative impact on configuration phase performance #182. It would help both backends and cut more than this does: resolving one version currently issues 70 git operations, including 14 working-tree scans and 8 tag walks. But it changes staleness semantics afterreleasecreates a tag, and Negative impact on configuration phase performance #182 notes the plugin is applied per-module with different tag prefixes, which complicates the cache key. Happy to explore this instead if you prefer.overriddenIsCleanalready avoids the status cost, but it is a correctness trade-off users must opt into per-project; Skip the working tree scan when uncommitted changes are ignored #1074 makes it unnecessary in the default case.Honest caveats
gitprocesses, so on small repositories the native backend is a wash or marginally slower. That is why it is opt-in.currentPosition()recomputingisClean(10 of the 14 scans). Fixing that means makingScmPosition.isCleanlazy, which touches a user-visible@Input— deliberately out of scope here.Status
Implemented and green:
NativeGitRepositoryTestmirrors the read-path scenarios ofGitRepositoryTestand additionally asserts output parity with the JGit backend on the same fixture repository, plus integration tests for the DSL flag, the Gradle property and an unknown backend value.Would you accept a PR along these lines? I am happy to change the DSL naming (
backendvs something else), make it a Gradle-property-only escape hatch rather than DSL, or drop it in favour of the caching approach from #182.