diff --git a/maven-scm-api/src/main/java/org/apache/maven/scm/CommandParameter.java b/maven-scm-api/src/main/java/org/apache/maven/scm/CommandParameter.java
index 121bb7097..2c7d87354 100644
--- a/maven-scm-api/src/main/java/org/apache/maven/scm/CommandParameter.java
+++ b/maven-scm-api/src/main/java/org/apache/maven/scm/CommandParameter.java
@@ -79,6 +79,17 @@ public class CommandParameter implements Serializable {
*/
public static final CommandParameter SCM_SHORT_REVISION_LENGTH = new CommandParameter("shortRevisionLength");
+ /**
+ * Parameter used only for the Git SCM {@code info} command to control whether merge commits are skipped
+ * (adds --no-merges to the underlying git log invocation).
+ * Defaults to {@code true} (skip merge commits) to keep backward compatibility. Set to {@code false} to
+ * include merge commits, e.g. so that the reported revision matches the actual {@code HEAD} in merge-based
+ * release workflows.
+ *
+ * @since 2.2.2
+ */
+ public static final CommandParameter SCM_SKIP_MERGE_COMMITS = new CommandParameter("skipMergeCommits");
+
/**
* Parameter to force add.
*
diff --git a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gitexe/src/main/java/org/apache/maven/scm/provider/git/gitexe/command/info/GitInfoCommand.java b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gitexe/src/main/java/org/apache/maven/scm/provider/git/gitexe/command/info/GitInfoCommand.java
index 08262db6b..dc2046287 100644
--- a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gitexe/src/main/java/org/apache/maven/scm/provider/git/gitexe/command/info/GitInfoCommand.java
+++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gitexe/src/main/java/org/apache/maven/scm/provider/git/gitexe/command/info/GitInfoCommand.java
@@ -47,13 +47,20 @@ public class GitInfoCommand extends AbstractCommand implements GitCommand {
public static final int NO_REVISION_LENGTH = -1;
+ /** Default value applied when the {@link CommandParameter#SCM_SKIP_MERGE_COMMITS} parameter is absent. */
+ public static final boolean DEFAULT_SKIP_MERGE_COMMITS = true;
+
@Override
protected ScmResult executeCommand(
ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters parameters) throws ScmException {
+ boolean skipMergeCommits = isSkipMergeCommits(parameters);
+
Commandline baseCli = GitCommandLineUtils.getBaseGitCommandLine(fileSet.getBasedir(), "log");
baseCli.createArg().setValue("-1"); // only most recent commit matters
- baseCli.createArg().setValue("--no-merges"); // skip merge commits
+ if (skipMergeCommits) {
+ baseCli.createArg().setValue("--no-merges"); // skip merge commits
+ }
baseCli.addArg(GitInfoConsumer.getFormatArgument());
List infoItems = new LinkedList<>();
@@ -64,7 +71,9 @@ protected ScmResult executeCommand(
for (File scmFile : fileSet.getFileList()) {
baseCli = GitCommandLineUtils.getBaseGitCommandLine(fileSet.getBasedir(), "log");
baseCli.createArg().setValue("-1"); // only most recent commit matters
- baseCli.createArg().setValue("--no-merges"); // skip merge commits
+ if (skipMergeCommits) {
+ baseCli.createArg().setValue("--no-merges"); // skip merge commits
+ }
baseCli.addArg(GitInfoConsumer.getFormatArgument());
// Insert a separator to make sure that files aren't interpreted as part of the version spec
baseCli.createArg().setValue("--");
@@ -102,4 +111,21 @@ private static int getRevisionLength(final CommandParameters parameters) throws
return parameters.getInt(CommandParameter.SCM_SHORT_REVISION_LENGTH, NO_REVISION_LENGTH);
}
}
+
+ /**
+ * Whether merge commits should be skipped (i.e. whether {@code --no-merges} should be added).
+ *
+ * @param parameters the command parameters
+ * @return {@link #DEFAULT_SKIP_MERGE_COMMITS} if parameter {@link CommandParameter#SCM_SKIP_MERGE_COMMITS}
+ * (or the whole {@code parameters}) is absent, and otherwise the requested value
+ * @throws ScmException if the parameter has the wrong type
+ * @since 2.2.2
+ */
+ private static boolean isSkipMergeCommits(final CommandParameters parameters) throws ScmException {
+ if (parameters == null) {
+ return DEFAULT_SKIP_MERGE_COMMITS;
+ } else {
+ return parameters.getBoolean(CommandParameter.SCM_SKIP_MERGE_COMMITS, DEFAULT_SKIP_MERGE_COMMITS);
+ }
+ }
}
diff --git a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/pom.xml b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/pom.xml
index ee2938a3d..a5343a445 100644
--- a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/pom.xml
+++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/pom.xml
@@ -44,6 +44,12 @@
org.codehaus.plexus
plexus-utils
+
+
+ org.eclipse.jgit
+ org.eclipse.jgit
+ ${jgitVersion}
+
org.apache.sshd
diff --git a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/command/info/GitInfoCommandTckTest.java b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/command/info/GitInfoCommandTckTest.java
index fa0a91570..cd6bb1b19 100644
--- a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/command/info/GitInfoCommandTckTest.java
+++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/command/info/GitInfoCommandTckTest.java
@@ -18,8 +18,29 @@
*/
package org.apache.maven.scm.provider.git.command.info;
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Date;
+
+import org.apache.maven.scm.CommandParameter;
+import org.apache.maven.scm.CommandParameters;
+import org.apache.maven.scm.command.info.InfoScmResult;
+import org.apache.maven.scm.provider.ScmProvider;
import org.apache.maven.scm.provider.git.GitScmTestUtils;
import org.apache.maven.scm.tck.command.info.InfoCommandTckTest;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.MergeCommand;
+import org.eclipse.jgit.api.MergeResult;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* @author Mark Struberg
@@ -31,4 +52,93 @@ public abstract class GitInfoCommandTckTest extends InfoCommandTckTest {
public void initRepo() throws Exception {
GitScmTestUtils.initRepo("src/test/resources/repository/", getRepositoryRoot(), getWorkingDirectory());
}
+
+ @Test
+ void testInfoCommandSkipsMergeCommitsByDefault() throws Exception {
+ MergeScenario scenario = createMergeCommitInWorkingCopy();
+ ScmProvider scmProvider = getScmManager().getProviderByUrl(getScmUrl());
+ InfoScmResult result = scmProvider.info(getScmRepository().getProviderRepository(), getScmFileSet(), null);
+ assertResultIsSuccess(result);
+ assertEquals(1, result.getInfoItems().size());
+ assertEquals(
+ scenario.lastNonMergeCommit.getName(),
+ result.getInfoItems().get(0).getRevision(),
+ "the most recent non-merge commit must be reported by default");
+ }
+
+ @Test
+ void testInfoCommandIncludesMergeCommitsOnDemand() throws Exception {
+ MergeScenario scenario = createMergeCommitInWorkingCopy();
+ ScmProvider scmProvider = getScmManager().getProviderByUrl(getScmUrl());
+ CommandParameters parameters = new CommandParameters();
+ parameters.setString(CommandParameter.SCM_SKIP_MERGE_COMMITS, Boolean.FALSE.toString());
+ InfoScmResult result =
+ scmProvider.info(getScmRepository().getProviderRepository(), getScmFileSet(), parameters);
+ assertResultIsSuccess(result);
+ assertEquals(1, result.getInfoItems().size());
+ assertEquals(
+ scenario.mergeCommit.getName(),
+ result.getInfoItems().get(0).getRevision(),
+ "the merge commit at HEAD must be reported when skipMergeCommits=false");
+ }
+
+ /**
+ * Turns the working copy's {@code HEAD} into a no-fast-forward merge commit (two parents):
+ * commits once on a side branch and once on the default branch, then merges the side branch.
+ * Distinct commit times make "the most recent non-merge commit" unambiguous.
+ */
+ private MergeScenario createMergeCommitInWorkingCopy() throws Exception {
+ try (Git git = Git.open(getWorkingCopy())) {
+ StoredConfig config = git.getRepository().getConfig();
+ config.setString("user", null, "name", "Test User");
+ config.setString("user", null, "email", "test@example.com");
+ config.setBoolean("commit", null, "gpgsign", false);
+ config.save();
+
+ Date sideBranchTime = new Date(System.currentTimeMillis() - 120_000L);
+ Date defaultBranchTime = new Date(sideBranchTime.getTime() + 60_000L);
+
+ String defaultBranch = git.getRepository().getBranch();
+ git.checkout().setCreateBranch(true).setName("side-branch").call();
+ commitNewFile(git, "side.txt", sideBranchTime);
+
+ git.checkout().setName(defaultBranch).call();
+ RevCommit lastNonMergeCommit = commitNewFile(git, "default.txt", defaultBranchTime);
+
+ MergeResult mergeResult = git.merge()
+ .include(git.getRepository().resolve("side-branch"))
+ .setFastForward(MergeCommand.FastForwardMode.NO_FF)
+ .setMessage("merge side-branch")
+ .call();
+ assertTrue(
+ mergeResult.getMergeStatus().isSuccessful(),
+ "merge must succeed but was: " + mergeResult.getMergeStatus());
+ ObjectId mergeCommit = mergeResult.getNewHead();
+ assertNotNull(mergeCommit, "merge must produce a new HEAD commit");
+ return new MergeScenario(lastNonMergeCommit, mergeCommit);
+ }
+ }
+
+ private RevCommit commitNewFile(Git git, String fileName, Date commitTime) throws Exception {
+ File file = new File(git.getRepository().getWorkTree(), fileName);
+ Files.write(file.toPath(), ("content of " + fileName).getBytes(StandardCharsets.UTF_8));
+ git.add().addFilepattern(fileName).call();
+ PersonIdent ident = new PersonIdent("Test User", "test@example.com", commitTime, GMT_TIME_ZONE);
+ return git.commit()
+ .setMessage("add " + fileName)
+ .setAuthor(ident)
+ .setCommitter(ident)
+ .setSign(false)
+ .call();
+ }
+
+ private static final class MergeScenario {
+ private final RevCommit lastNonMergeCommit;
+ private final ObjectId mergeCommit;
+
+ private MergeScenario(RevCommit lastNonMergeCommit, ObjectId mergeCommit) {
+ this.lastNonMergeCommit = lastNonMergeCommit;
+ this.mergeCommit = mergeCommit;
+ }
+ }
}
diff --git a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-jgit/pom.xml b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-jgit/pom.xml
index d1c8c8b6d..209dc369a 100644
--- a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-jgit/pom.xml
+++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-jgit/pom.xml
@@ -31,11 +31,6 @@
JGIT implementation for SCM Git Provider.
see http://eclipse.org/jgit/
-
-
- 5.13.5.202508271544-r
-
-
diff --git a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-jgit/src/main/java/org/apache/maven/scm/provider/git/jgit/command/info/JGitInfoCommand.java b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-jgit/src/main/java/org/apache/maven/scm/provider/git/jgit/command/info/JGitInfoCommand.java
index 75582bb4d..cb892a660 100644
--- a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-jgit/src/main/java/org/apache/maven/scm/provider/git/jgit/command/info/JGitInfoCommand.java
+++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-jgit/src/main/java/org/apache/maven/scm/provider/git/jgit/command/info/JGitInfoCommand.java
@@ -24,6 +24,7 @@
import java.util.List;
import org.apache.commons.lang3.StringUtils;
+import org.apache.maven.scm.CommandParameter;
import org.apache.maven.scm.CommandParameters;
import org.apache.maven.scm.ScmException;
import org.apache.maven.scm.ScmFileSet;
@@ -42,6 +43,7 @@
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.revwalk.filter.RevFilter;
import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
@@ -54,6 +56,7 @@ public class JGitInfoCommand extends AbstractCommand implements GitCommand {
protected ScmResult executeCommand(
ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters parameters) throws ScmException {
File basedir = fileSet.getBasedir();
+ boolean skipMergeCommits = isSkipMergeCommits(parameters);
Git git = null;
try {
git = JGitUtils.openRepo(basedir);
@@ -64,13 +67,13 @@ protected ScmResult executeCommand(
List infoItems = new LinkedList<>();
if (fileSet.getFileList().isEmpty()) {
- RevCommit headCommit = git.getRepository().parseCommit(objectId);
- infoItems.add(getInfoItem(headCommit, fileSet.getBasedir()));
+ RevCommit commit = getMostRecentCommit(git.getRepository(), objectId, skipMergeCommits);
+ infoItems.add(getInfoItem(commit, fileSet.getBasedir()));
} else {
// iterate over all files
for (File file : JGitUtils.getWorkingCopyRelativePaths(
git.getRepository().getWorkTree(), fileSet)) {
- infoItems.add(getInfoItem(git.getRepository(), objectId, file));
+ infoItems.add(getInfoItem(git.getRepository(), objectId, file, skipMergeCommits));
}
}
return new InfoScmResult(infoItems, new ScmResult("JGit.resolve(HEAD)", "", objectId.toString(), true));
@@ -81,16 +84,42 @@ protected ScmResult executeCommand(
}
}
- protected InfoItem getInfoItem(Repository repository, ObjectId headObjectId, File file) throws IOException {
- RevCommit commit = getMostRecentCommitForPath(repository, headObjectId, JGitUtils.toNormalizedFilePath(file));
+ protected InfoItem getInfoItem(Repository repository, ObjectId headObjectId, File file, boolean skipMergeCommits)
+ throws IOException {
+ RevCommit commit = getMostRecentCommitForPath(
+ repository, headObjectId, JGitUtils.toNormalizedFilePath(file), skipMergeCommits);
return getInfoItem(commit, file);
}
+ /**
+ * Returns the most recent commit reachable from {@code headObjectId}, optionally ignoring merge commits
+ * (mimics {@code git log -1 --no-merges} when {@code skipMergeCommits} is {@code true}).
+ * May return {@code null} when no non-merge commit is reachable (e.g. shallow history of merge commits only).
+ */
+ private RevCommit getMostRecentCommit(Repository repository, ObjectId headObjectId, boolean skipMergeCommits)
+ throws IOException {
+ try (RevWalk revWalk = new RevWalk(repository)) {
+ RevCommit headCommit = revWalk.parseCommit(headObjectId);
+ if (!skipMergeCommits) {
+ return headCommit;
+ }
+ revWalk.markStart(headCommit);
+ revWalk.sort(RevSort.COMMIT_TIME_DESC);
+ revWalk.setRevFilter(RevFilter.NO_MERGES);
+ return revWalk.next();
+ }
+ }
+
protected InfoItem getInfoItem(RevCommit fileCommit, File file) {
InfoItem infoItem = new InfoItem();
infoItem.setPath(file.getPath());
- infoItem.setRevision(StringUtils.trim(fileCommit.name()));
infoItem.setURL(file.toPath().toUri().toASCIIString());
+ if (fileCommit == null) {
+ // no matching commit (e.g. path without history, or only merge commits while skipping them):
+ // like gitexe on empty "git log" output, report the item without revision metadata
+ return infoItem;
+ }
+ infoItem.setRevision(StringUtils.trim(fileCommit.name()));
PersonIdent authorIdent = fileCommit.getAuthorIdent();
infoItem.setLastChangedDateTime(authorIdent
.getWhen()
@@ -100,16 +129,34 @@ protected InfoItem getInfoItem(RevCommit fileCommit, File file) {
return infoItem;
}
- private RevCommit getMostRecentCommitForPath(Repository repository, ObjectId headObjectId, String path)
- throws IOException {
+ private RevCommit getMostRecentCommitForPath(
+ Repository repository, ObjectId headObjectId, String path, boolean skipMergeCommits) throws IOException {
RevCommit latestCommit = null;
try (RevWalk revWalk = new RevWalk(repository)) {
RevCommit headCommit = revWalk.parseCommit(headObjectId);
revWalk.markStart(headCommit);
revWalk.sort(RevSort.COMMIT_TIME_DESC);
+ if (skipMergeCommits) {
+ revWalk.setRevFilter(RevFilter.NO_MERGES);
+ }
revWalk.setTreeFilter(AndTreeFilter.create(PathFilter.create(path), TreeFilter.ANY_DIFF));
latestCommit = revWalk.next();
}
return latestCommit;
}
+
+ /**
+ * Whether merge commits should be skipped for the {@code info} command.
+ *
+ * @param parameters the command parameters (may be {@code null})
+ * @return {@code true} if parameter {@link CommandParameter#SCM_SKIP_MERGE_COMMITS} (or the whole
+ * {@code parameters}) is absent, and otherwise the requested value
+ * @throws ScmException if the parameter has the wrong type
+ */
+ private static boolean isSkipMergeCommits(CommandParameters parameters) throws ScmException {
+ if (parameters == null) {
+ return true;
+ }
+ return parameters.getBoolean(CommandParameter.SCM_SKIP_MERGE_COMMITS, true);
+ }
}
diff --git a/maven-scm-providers/maven-scm-providers-git/pom.xml b/maven-scm-providers/maven-scm-providers-git/pom.xml
index d5e7733e4..81e8653b4 100644
--- a/maven-scm-providers/maven-scm-providers-git/pom.xml
+++ b/maven-scm-providers/maven-scm-providers-git/pom.xml
@@ -41,6 +41,8 @@
2.19.0
+
+ 5.13.5.202508271544-r