Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>--no-merges</code> to the underlying <code>git log</code> 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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<InfoItem> infoItems = new LinkedList<>();
Expand All @@ -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("--");
Expand Down Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-utils</artifactId>
</dependency>
<!-- for building test scenarios (e.g. merge commits) in a provider-independent way -->
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>${jgitVersion}</version>
</dependency>
<!-- for testing SSH authentication -->
<dependency>
<groupId>org.apache.sshd</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href="mailto:struberg@yahoo.de">Mark Struberg</a>
Expand All @@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,6 @@
<description>JGIT implementation for SCM Git Provider.
see http://eclipse.org/jgit/</description>

<properties>
<!-- version 6+ requires Java 11 -->
<jgitVersion>5.13.5.202508271544-r</jgitVersion>
</properties>

<dependencyManagement>
<dependencies>
<!-- upgrade SSHD used in JGit to newer version for being compatible with ITs -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -64,13 +67,13 @@ protected ScmResult executeCommand(

List<InfoItem> 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));
Expand All @@ -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()
Expand All @@ -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);
}
}
2 changes: 2 additions & 0 deletions maven-scm-providers/maven-scm-providers-git/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@

<properties>
<minaSshdVersion>2.19.0</minaSshdVersion>
<!-- version 6+ requires Java 11 -->
<jgitVersion>5.13.5.202508271544-r</jgitVersion>
</properties>

<profiles>
Expand Down