Skip to content

Commit 9f69d2e

Browse files
committed
fix: remove dependency on ScmManager
1 parent 095d93f commit 9f69d2e

5 files changed

Lines changed: 143 additions & 88 deletions

File tree

pom.xml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -180,12 +180,6 @@
180180
<version>${maven-scm.version}</version>
181181
<scope>compile</scope>
182182
</dependency>
183-
<dependency>
184-
<groupId>org.apache.maven.scm</groupId>
185-
<artifactId>maven-scm-provider-gitexe</artifactId>
186-
<version>${maven-scm.version}</version>
187-
<scope>runtime</scope>
188-
</dependency>
189183
<dependency>
190184
<groupId>org.apache.maven.scm</groupId>
191185
<artifactId>maven-scm-provider-svnexe</artifactId>

src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package org.apache.commons.release.plugin.internal;
1818

19+
import java.io.BufferedReader;
1920
import java.io.IOException;
2021
import java.nio.charset.StandardCharsets;
2122
import java.nio.file.Files;
@@ -37,6 +38,14 @@ public final class GitUtils {
3738
* <p>See <a href="https://git-scm.com/docs/gitrepository-layout">gitrepository-layout</a>.</p>
3839
*/
3940
private static final String GITDIR_PREFIX = "gitdir: ";
41+
/**
42+
* Maximum number of symbolic-ref hops before we give up (to avoid cycles).
43+
*/
44+
private static final int MAX_REF_DEPTH = 5;
45+
/**
46+
* Prefix used in {@code HEAD} and ref files to indicate a symbolic reference.
47+
*/
48+
private static final String REF_PREFIX = "ref: ";
4049
/**
4150
* The SCM URI prefix for Git repositories.
4251
*/
@@ -79,14 +88,37 @@ private static Path findGitDir(final Path path) throws IOException {
7988
*/
8089
public static String getCurrentBranch(final Path repositoryPath) throws IOException {
8190
final Path gitDir = findGitDir(repositoryPath);
82-
final String head = new String(Files.readAllBytes(gitDir.resolve("HEAD")), StandardCharsets.UTF_8).trim();
91+
final String head = readHead(gitDir);
8392
if (head.startsWith("ref: refs/heads/")) {
8493
return head.substring("ref: refs/heads/".length());
8594
}
86-
// detached HEAD — return the commit SHA
95+
// Detached HEAD: the file contains the commit SHA.
8796
return head;
8897
}
8998

99+
/**
100+
* Gets the commit SHA pointed to by {@code HEAD}.
101+
*
102+
* <p>Handles loose refs under {@code <gitDir>/refs/...}, packed refs in {@code <gitDir>/packed-refs},
103+
* symbolic indirection (a ref file that itself contains {@code ref: ...}), and detached HEAD.</p>
104+
*
105+
* @param repositoryPath A path inside the Git repository.
106+
* @return The hex-encoded commit SHA.
107+
* @throws IOException If the {@code .git} directory cannot be found, the ref cannot be resolved,
108+
* or the symbolic chain is deeper than {@value #MAX_REF_DEPTH}.
109+
*/
110+
public static String getHeadCommit(final Path repositoryPath) throws IOException {
111+
final Path gitDir = findGitDir(repositoryPath);
112+
String value = readHead(gitDir);
113+
for (int i = 0; i < MAX_REF_DEPTH; i++) {
114+
if (!value.startsWith(REF_PREFIX)) {
115+
return value;
116+
}
117+
value = resolveRef(gitDir, value.substring(REF_PREFIX.length()));
118+
}
119+
throw new IOException("Symbolic ref chain exceeds " + MAX_REF_DEPTH + " hops in: " + gitDir);
120+
}
121+
90122
/**
91123
* Returns the Git tree hash for the given directory.
92124
*
@@ -102,6 +134,75 @@ public static String gitTree(final Path path) throws IOException {
102134
return Hex.encodeHexString(GitIdentifiers.treeId(digest, path));
103135
}
104136

137+
/**
138+
* Reads and trims the {@code HEAD} file of the given Git directory.
139+
*
140+
* @param gitDir The {@code .git} directory.
141+
* @return The trimmed contents of {@code <gitDir>/HEAD}.
142+
* @throws IOException If the file cannot be read.
143+
*/
144+
private static String readHead(final Path gitDir) throws IOException {
145+
return new String(Files.readAllBytes(gitDir.resolve("HEAD")), StandardCharsets.UTF_8).trim();
146+
}
147+
148+
/**
149+
* Returns the directory that holds shared repository state (loose refs, {@code packed-refs}).
150+
* In a linked worktree this is read from {@code <gitDir>/commondir}; otherwise it is
151+
* {@code gitDir} itself.
152+
*
153+
* @param gitDir The {@code .git} directory.
154+
* @return The shared-state directory.
155+
* @throws IOException If {@code commondir} exists but cannot be read.
156+
*/
157+
private static Path resolveCommonDir(final Path gitDir) throws IOException {
158+
final Path commonDir = gitDir.resolve("commondir");
159+
if (Files.isRegularFile(commonDir)) {
160+
final String value = new String(Files.readAllBytes(commonDir), StandardCharsets.UTF_8).trim();
161+
return gitDir.resolve(value).normalize();
162+
}
163+
return gitDir;
164+
}
165+
166+
/**
167+
* Resolves a single ref (e.g. {@code refs/heads/foo}) to its stored value.
168+
*
169+
* <p>The return value is either a commit SHA or another {@code ref: ...} line, which the caller continues to resolve.</p>
170+
*
171+
* <p>In a linked worktree, loose and packed refs are stored in the "common dir" (usually the
172+
* main repository's {@code .git}), which is pointed to by {@code <gitDir>/commondir}.</p>
173+
*
174+
* @param gitDir The {@code .git} directory.
175+
* @param refPath The ref path relative to the common dir (e.g. {@code refs/heads/main}).
176+
* @return Either a commit SHA or another {@code ref: ...} line to be resolved by the caller.
177+
* @throws IOException If the ref is not found as a loose file or in {@code packed-refs}.
178+
*/
179+
private static String resolveRef(final Path gitDir, final String refPath) throws IOException {
180+
final Path refsDir = resolveCommonDir(gitDir);
181+
final Path refFile = refsDir.resolve(refPath);
182+
if (Files.isRegularFile(refFile)) {
183+
return new String(Files.readAllBytes(refFile), StandardCharsets.UTF_8).trim();
184+
}
185+
final Path packed = refsDir.resolve("packed-refs");
186+
if (Files.isRegularFile(packed)) {
187+
try (BufferedReader reader = Files.newBufferedReader(packed, StandardCharsets.UTF_8)) {
188+
// packed-refs format: one ref per line as "<sha> <refname>", with '#' header lines,
189+
// blank lines, and "^<sha>" peeled-tag continuation lines that we skip.
190+
// See https://git-scm.com/docs/gitrepository-layout
191+
String line;
192+
while ((line = reader.readLine()) != null) {
193+
if (line.isEmpty() || line.charAt(0) == '#' || line.charAt(0) == '^') {
194+
continue;
195+
}
196+
final int space = line.indexOf(' ');
197+
if (space > 0 && refPath.equals(line.substring(space + 1))) {
198+
return line.substring(0, space);
199+
}
200+
}
201+
}
202+
}
203+
throw new IOException("Cannot resolve ref: " + refPath);
204+
}
205+
105206
/**
106207
* Converts an SCM URI to a download URI suffixed with the current branch name.
107208
*

src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java

Lines changed: 6 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,6 @@
6161
import org.apache.maven.project.MavenProject;
6262
import org.apache.maven.project.MavenProjectHelper;
6363
import org.apache.maven.rtinfo.RuntimeInformation;
64-
import org.apache.maven.scm.CommandParameters;
65-
import org.apache.maven.scm.ScmException;
66-
import org.apache.maven.scm.ScmFileSet;
67-
import org.apache.maven.scm.command.info.InfoItem;
68-
import org.apache.maven.scm.command.info.InfoScmResult;
69-
import org.apache.maven.scm.manager.ScmManager;
70-
import org.apache.maven.scm.repository.ScmRepository;
7164

7265
/**
7366
* Generates a SLSA v1.2 in-toto attestation covering all artifacts attached to the project.
@@ -174,10 +167,6 @@ public class BuildAttestationMojo extends AbstractMojo {
174167
*/
175168
@Parameter(property = "commons.release.scmDirectory", defaultValue = "${basedir}")
176169
private File scmDirectory;
177-
/**
178-
* SCM manager to detect the Git revision.
179-
*/
180-
private final ScmManager scmManager;
181170
/**
182171
* The current Maven session, used to resolve plugin dependencies.
183172
*/
@@ -215,16 +204,14 @@ public class BuildAttestationMojo extends AbstractMojo {
215204
* Creates a new instance with the given dependencies.
216205
*
217206
* @param project A Maven project.
218-
* @param scmManager A SCM manager.
219207
* @param runtimeInformation Maven runtime information.
220208
* @param session A Maven session.
221209
* @param mavenProjectHelper A helper to attach artifacts to the project.
222210
*/
223211
@Inject
224-
public BuildAttestationMojo(final MavenProject project, final ScmManager scmManager, final RuntimeInformation runtimeInformation,
212+
public BuildAttestationMojo(final MavenProject project, final RuntimeInformation runtimeInformation,
225213
final MavenSession session, final MavenProjectHelper mavenProjectHelper) {
226214
this.project = project;
227-
this.scmManager = scmManager;
228215
this.runtimeInformation = runtimeInformation;
229216
this.session = session;
230217
this.mavenProjectHelper = mavenProjectHelper;
@@ -323,13 +310,13 @@ private List<ResourceDescriptor> getProjectDependencies() throws MojoExecutionEx
323310
* Gets a resource descriptor for the current SCM source, including the URI and Git commit digest.
324311
*
325312
* @return A resource descriptor for the SCM source.
326-
* @throws IOException If the current branch cannot be determined.
327-
* @throws MojoExecutionException If the SCM revision cannot be retrieved.
313+
* @throws IOException If the current branch or the HEAD commit cannot be determined.
328314
*/
329-
private ResourceDescriptor getScmDescriptor() throws IOException, MojoExecutionException {
315+
private ResourceDescriptor getScmDescriptor() throws IOException {
316+
final Path scmPath = scmDirectory.toPath();
330317
return new ResourceDescriptor()
331-
.setUri(GitUtils.scmToDownloadUri(scmConnectionUrl, scmDirectory.toPath()))
332-
.setDigest(Collections.singletonMap("gitCommit", getScmRevision()));
318+
.setUri(GitUtils.scmToDownloadUri(scmConnectionUrl, scmPath))
319+
.setDigest(Collections.singletonMap("gitCommit", GitUtils.getHeadCommit(scmPath)));
333320
}
334321

335322
/**
@@ -341,64 +328,6 @@ public File getScmDirectory() {
341328
return scmDirectory;
342329
}
343330

344-
/**
345-
* Gets an SCM repository from the configured connection URL.
346-
*
347-
* @return The SCM repository.
348-
* @throws MojoExecutionException If the SCM repository cannot be created.
349-
*/
350-
private ScmRepository getScmRepository() throws MojoExecutionException {
351-
try {
352-
return scmManager.makeScmRepository(scmConnectionUrl);
353-
} catch (final ScmException e) {
354-
throw new MojoExecutionException("Failed to create SCM repository", e);
355-
}
356-
}
357-
358-
/**
359-
* Gets the current SCM revision (commit hash) for the configured SCM directory.
360-
*
361-
* @return The current SCM revision string.
362-
* @throws MojoExecutionException If the revision cannot be retrieved from SCM.
363-
*/
364-
private String getScmRevision() throws MojoExecutionException {
365-
final ScmRepository scmRepository = getScmRepository();
366-
final CommandParameters commandParameters = new CommandParameters();
367-
try {
368-
final InfoScmResult result = scmManager.getProviderByRepository(scmRepository).info(scmRepository.getProviderRepository(),
369-
new ScmFileSet(scmDirectory), commandParameters);
370-
371-
return getScmRevision(result);
372-
} catch (final ScmException e) {
373-
throw new MojoExecutionException("Failed to retrieve SCM revision", e);
374-
}
375-
}
376-
377-
/**
378-
* Extracts the revision string from an SCM info result.
379-
*
380-
* @param result The SCM info result.
381-
* @return The revision string.
382-
* @throws MojoExecutionException If the result is unsuccessful or contains no revision.
383-
*/
384-
private String getScmRevision(final InfoScmResult result) throws MojoExecutionException {
385-
if (!result.isSuccess()) {
386-
throw new MojoExecutionException("Failed to retrieve SCM revision: " + result.getProviderMessage());
387-
}
388-
389-
if (result.getInfoItems() == null || result.getInfoItems().isEmpty()) {
390-
throw new MojoExecutionException("No SCM revision information found for " + scmDirectory);
391-
}
392-
393-
final InfoItem item = result.getInfoItems().get(0);
394-
395-
final String revision = item.getRevision();
396-
if (revision == null) {
397-
throw new MojoExecutionException("Empty SCM revision returned for " + scmDirectory);
398-
}
399-
return revision;
400-
}
401-
402331
/**
403332
* Gets the GPG signer, creating and preparing it from plugin parameters if not already set.
404333
*

src/test/java/org/apache/commons/release/plugin/internal/GitUtilsTest.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ static Stream<Arguments> testGetCurrentBranch() {
5151
Arguments.of(worktree, GitFixture.WORKTREE_BRANCH), Arguments.of(worktree.resolve(GitFixture.SUBDIR), GitFixture.WORKTREE_BRANCH));
5252
}
5353

54+
static Stream<Arguments> testGetHeadCommit() {
55+
return Stream.of(
56+
Arguments.of(repo, GitFixture.INITIAL_COMMIT_SHA),
57+
Arguments.of(repo.resolve(GitFixture.SUBDIR), GitFixture.INITIAL_COMMIT_SHA),
58+
Arguments.of(worktree, GitFixture.INITIAL_COMMIT_SHA),
59+
Arguments.of(worktree.resolve(GitFixture.SUBDIR), GitFixture.INITIAL_COMMIT_SHA));
60+
}
61+
5462
static Stream<Arguments> testScmToDownloadUri() {
5563
return Stream.of(
5664
Arguments.of("scm:git:https://gitbox.apache.org/repos/asf/commons-release-plugin.git",
@@ -80,6 +88,31 @@ void testGetCurrentBranchDetachedHead() throws IOException {
8088
assertEquals(GitFixture.INITIAL_COMMIT_SHA, GitUtils.getCurrentBranch(detachedRepo));
8189
}
8290

91+
@ParameterizedTest
92+
@MethodSource
93+
void testGetHeadCommit(final Path repositoryPath, final String expectedSha) throws IOException {
94+
assertEquals(expectedSha, GitUtils.getHeadCommit(repositoryPath));
95+
}
96+
97+
@Test
98+
void testGetHeadCommitDetachedHead() throws IOException {
99+
final Path detachedRepo = tempDir.resolve("detached-head-commit-repo");
100+
final Path detachedWorktree = tempDir.resolve("detached-head-commit-worktree");
101+
GitFixture.createRepoAndWorktree(detachedRepo, detachedWorktree);
102+
GitFixture.git(detachedRepo, "checkout", "-q", "--detach", "HEAD");
103+
assertEquals(GitFixture.INITIAL_COMMIT_SHA, GitUtils.getHeadCommit(detachedRepo));
104+
}
105+
106+
@Test
107+
void testGetHeadCommitPackedRefs() throws IOException {
108+
final Path packedRepo = tempDir.resolve("packed-repo");
109+
final Path packedWorktree = tempDir.resolve("packed-worktree");
110+
GitFixture.createRepoAndWorktree(packedRepo, packedWorktree);
111+
// Move all loose refs (branches, tags) into .git/packed-refs and delete the loose files.
112+
GitFixture.git(packedRepo, "pack-refs", "--all", "--prune");
113+
assertEquals(GitFixture.INITIAL_COMMIT_SHA, GitUtils.getHeadCommit(packedRepo));
114+
}
115+
83116
@ParameterizedTest
84117
@MethodSource
85118
void testScmToDownloadUri(final String scmUri, final Path repositoryPath, final String expectedDownloadUri) throws IOException {

src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@
5959
import org.apache.maven.project.MavenProject;
6060
import org.apache.maven.project.MavenProjectHelper;
6161
import org.apache.maven.rtinfo.RuntimeInformation;
62-
import org.apache.maven.scm.manager.ScmManager;
6362
import org.codehaus.plexus.PlexusContainer;
6463
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
6564
import org.eclipse.aether.RepositorySystemSession;
@@ -118,9 +117,8 @@ private static void configureBuildAttestationMojo(final BuildAttestationMojo moj
118117

119118
private static BuildAttestationMojo createBuildAttestationMojo(final MavenProject project, final MavenProjectHelper projectHelper)
120119
throws ComponentLookupException {
121-
final ScmManager scmManager = container.lookup(ScmManager.class);
122120
final RuntimeInformation runtimeInfo = container.lookup(RuntimeInformation.class);
123-
return new BuildAttestationMojo(project, scmManager, runtimeInfo,
121+
return new BuildAttestationMojo(project, runtimeInfo,
124122
createMavenSession(createMavenExecutionRequest(), new DefaultMavenExecutionResult()), projectHelper);
125123
}
126124

0 commit comments

Comments
 (0)