From 490162998bbdce7962ea84fb7103424917fef0da Mon Sep 17 00:00:00 2001 From: GabriRuflex <6223348+GabriRuflex@users.noreply.github.com> Date: Wed, 1 Jul 2026 20:27:29 +0200 Subject: [PATCH 1/3] Add skipMergeCommits parameter to Git info command The Git "info" command always passed --no-merges to git log, so the reported revision could differ from the actual HEAD in merge-based release workflows (regression surfaced in buildnumber-maven-plugin#229). Introduce CommandParameter.SCM_SKIP_MERGE_COMMITS (default true, fully backward compatible) to make this behavior configurable. When set to false, merge commits are included so the reported revision matches HEAD. Both providers honor the flag: gitexe adds --no-merges conditionally, and the JGit provider now applies RevFilter.NO_MERGES (previously it never filtered merge commits, diverging from gitexe). Fixes #1327 --- .../apache/maven/scm/CommandParameter.java | 11 ++ .../gitexe/command/info/GitInfoCommand.java | 30 +++- .../command/info/GitInfoCommandTest.java | 35 +++++ .../jgit/command/info/JGitInfoCommand.java | 54 +++++++- .../command/info/JGitInfoCommandTest.java | 128 ++++++++++++++++++ 5 files changed, 249 insertions(+), 9 deletions(-) create mode 100644 maven-scm-providers/maven-scm-providers-git/maven-scm-provider-jgit/src/test/java/org/apache/maven/scm/provider/git/jgit/command/info/JGitInfoCommandTest.java 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-gitexe/src/test/java/org/apache/maven/scm/provider/git/gitexe/command/info/GitInfoCommandTest.java b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gitexe/src/test/java/org/apache/maven/scm/provider/git/gitexe/command/info/GitInfoCommandTest.java index 9bbc2f305..85aa778f5 100644 --- a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gitexe/src/test/java/org/apache/maven/scm/provider/git/gitexe/command/info/GitInfoCommandTest.java +++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gitexe/src/test/java/org/apache/maven/scm/provider/git/gitexe/command/info/GitInfoCommandTest.java @@ -32,6 +32,7 @@ import static org.apache.maven.scm.provider.git.GitScmTestUtils.GIT_COMMAND_LINE; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -92,6 +93,40 @@ void testInfoCommandWithNegativeShortRevision() throws Exception { "revision should not be short"); } + @Test + void testInfoCommandSkipsMergeCommitsByDefault() throws Exception { + checkSystemCmdPresence(GIT_COMMAND_LINE); + + GitScmTestUtils.initRepo("src/test/resources/git/info", getRepositoryRoot(), getWorkingCopy()); + + ScmProvider provider = getScmManager().getProviderByUrl(getScmUrl()); + ScmProviderRepository repository = provider.makeProviderScmRepository(getRepositoryRoot()); + assertNotNull(repository); + InfoScmResult result = provider.info(repository, new ScmFileSet(getRepositoryRoot()), new CommandParameters()); + assertNotNull(result); + assertTrue( + result.getCommandLine().contains("--no-merges"), + "merge commits must be skipped by default (--no-merges present)"); + } + + @Test + void testInfoCommandIncludeMergeCommits() throws Exception { + checkSystemCmdPresence(GIT_COMMAND_LINE); + + GitScmTestUtils.initRepo("src/test/resources/git/info", getRepositoryRoot(), getWorkingCopy()); + + ScmProvider provider = getScmManager().getProviderByUrl(getScmUrl()); + ScmProviderRepository repository = provider.makeProviderScmRepository(getRepositoryRoot()); + assertNotNull(repository); + CommandParameters commandParameters = new CommandParameters(); + commandParameters.setString(CommandParameter.SCM_SKIP_MERGE_COMMITS, Boolean.FALSE.toString()); + InfoScmResult result = provider.info(repository, new ScmFileSet(getRepositoryRoot()), commandParameters); + assertNotNull(result); + assertFalse( + result.getCommandLine().contains("--no-merges"), + "merge commits must be included when skipMergeCommits=false (no --no-merges)"); + } + @Test void testInfoCommandWithZeroShortRevision() throws Exception { checkSystemCmdPresence(GIT_COMMAND_LINE); 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..3f2ae9223 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,11 +84,30 @@ 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}). + */ + 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.setRevFilter(RevFilter.NO_MERGES); + return revWalk.next(); + } + } + protected InfoItem getInfoItem(RevCommit fileCommit, File file) { InfoItem infoItem = new InfoItem(); infoItem.setPath(file.getPath()); @@ -100,16 +122,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/maven-scm-provider-jgit/src/test/java/org/apache/maven/scm/provider/git/jgit/command/info/JGitInfoCommandTest.java b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-jgit/src/test/java/org/apache/maven/scm/provider/git/jgit/command/info/JGitInfoCommandTest.java new file mode 100644 index 000000000..b65d99164 --- /dev/null +++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-jgit/src/test/java/org/apache/maven/scm/provider/git/jgit/command/info/JGitInfoCommandTest.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.scm.provider.git.jgit.command.info; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +import org.apache.maven.scm.CommandParameter; +import org.apache.maven.scm.CommandParameters; +import org.apache.maven.scm.ScmFileSet; +import org.apache.maven.scm.command.info.InfoScmResult; +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.StoredConfig; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Unit tests for the {@code skipMergeCommits} handling of {@link JGitInfoCommand}. + * A repository whose {@code HEAD} is a merge commit is built with the JGit API, then the + * {@code info} command is invoked with and without the {@link CommandParameter#SCM_SKIP_MERGE_COMMITS} flag. + */ +class JGitInfoCommandTest { + + @TempDir + File workDir; + + @Test + void includesMergeCommitWhenSkipMergeCommitsIsFalse() throws Exception { + ObjectId mergeCommit = buildRepositoryWithMergeHead(); + + CommandParameters parameters = new CommandParameters(); + parameters.setString(CommandParameter.SCM_SKIP_MERGE_COMMITS, Boolean.FALSE.toString()); + + InfoScmResult result = info(parameters); + + assertNotNull(result); + assertEquals( + mergeCommit.getName(), + result.getInfoItems().get(0).getRevision(), + "HEAD merge commit must be reported when skipMergeCommits=false"); + } + + @Test + void skipsMergeCommitByDefault() throws Exception { + ObjectId mergeCommit = buildRepositoryWithMergeHead(); + + InfoScmResult result = info(new CommandParameters()); + + assertNotNull(result); + assertNotEquals( + mergeCommit.getName(), + result.getInfoItems().get(0).getRevision(), + "merge commit must be skipped by default, a non-merge commit must be reported"); + } + + private InfoScmResult info(CommandParameters parameters) throws Exception { + // executeCommand is package-private accessible and ignores the repository argument + return (InfoScmResult) new JGitInfoCommand().executeCommand(null, new ScmFileSet(workDir), parameters); + } + + /** + * Builds a repository whose {@code HEAD} is a no-fast-forward merge commit (two parents). + * + * @return the id of the merge commit which is now {@code HEAD} + */ + private ObjectId buildRepositoryWithMergeHead() throws Exception { + try (Git git = Git.init().setDirectory(workDir).call()) { + 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(); + + commit(git, "a.txt"); + String mainBranch = git.getRepository().getBranch(); + + git.checkout().setCreateBranch(true).setName("feature").call(); + commit(git, "b.txt"); + + git.checkout().setName(mainBranch).call(); + commit(git, "c.txt"); + + MergeResult mergeResult = git.merge() + .include(git.getRepository().resolve("feature")) + .setFastForward(MergeCommand.FastForwardMode.NO_FF) + .setMessage("merge feature") + .call(); + return mergeResult.getNewHead(); + } + } + + private RevCommit commit(Git git, String fileName) 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(); + return git.commit() + .setMessage("add " + fileName) + .setAuthor("Test User", "test@example.com") + .setCommitter("Test User", "test@example.com") + .setSign(false) + .call(); + } +} From 379540b455bf087ee91a617e20a98949c6985d79 Mon Sep 17 00:00:00 2001 From: GabriRuflex <6223348+GabriRuflex@users.noreply.github.com> Date: Wed, 8 Jul 2026 11:15:47 +0200 Subject: [PATCH 2/3] Address Copilot review: deterministic walk order and null-safety in JGitInfoCommand --- .../git/jgit/command/info/JGitInfoCommand.java | 9 ++++++++- .../git/jgit/command/info/JGitInfoCommandTest.java | 10 +++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) 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 3f2ae9223..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 @@ -94,6 +94,7 @@ protected InfoItem getInfoItem(Repository repository, ObjectId headObjectId, Fil /** * 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 { @@ -103,6 +104,7 @@ private RevCommit getMostRecentCommit(Repository repository, ObjectId headObject return headCommit; } revWalk.markStart(headCommit); + revWalk.sort(RevSort.COMMIT_TIME_DESC); revWalk.setRevFilter(RevFilter.NO_MERGES); return revWalk.next(); } @@ -111,8 +113,13 @@ private RevCommit getMostRecentCommit(Repository repository, ObjectId headObject 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() diff --git a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-jgit/src/test/java/org/apache/maven/scm/provider/git/jgit/command/info/JGitInfoCommandTest.java b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-jgit/src/test/java/org/apache/maven/scm/provider/git/jgit/command/info/JGitInfoCommandTest.java index b65d99164..c64a0c8b6 100644 --- a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-jgit/src/test/java/org/apache/maven/scm/provider/git/jgit/command/info/JGitInfoCommandTest.java +++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-jgit/src/test/java/org/apache/maven/scm/provider/git/jgit/command/info/JGitInfoCommandTest.java @@ -38,6 +38,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Unit tests for the {@code skipMergeCommits} handling of {@link JGitInfoCommand}. @@ -59,6 +60,7 @@ void includesMergeCommitWhenSkipMergeCommitsIsFalse() throws Exception { InfoScmResult result = info(parameters); assertNotNull(result); + assertEquals(1, result.getInfoItems().size(), "exactly one info item is expected"); assertEquals( mergeCommit.getName(), result.getInfoItems().get(0).getRevision(), @@ -72,6 +74,7 @@ void skipsMergeCommitByDefault() throws Exception { InfoScmResult result = info(new CommandParameters()); assertNotNull(result); + assertEquals(1, result.getInfoItems().size(), "exactly one info item is expected"); assertNotEquals( mergeCommit.getName(), result.getInfoItems().get(0).getRevision(), @@ -110,7 +113,12 @@ private ObjectId buildRepositoryWithMergeHead() throws Exception { .setFastForward(MergeCommand.FastForwardMode.NO_FF) .setMessage("merge feature") .call(); - return mergeResult.getNewHead(); + assertTrue( + mergeResult.getMergeStatus().isSuccessful(), + "merge must succeed but was: " + mergeResult.getMergeStatus()); + ObjectId mergeHead = mergeResult.getNewHead(); + assertNotNull(mergeHead, "merge must produce a new HEAD commit"); + return mergeHead; } } From ab81a46f043e1b201705455784abf0055027d161 Mon Sep 17 00:00:00 2001 From: GabriRuflex <6223348+GabriRuflex@users.noreply.github.com> Date: Sun, 19 Jul 2026 14:30:23 +0200 Subject: [PATCH 3/3] Move skipMergeCommits tests to shared GitInfoCommandTckTest --- .../command/info/GitInfoCommandTest.java | 35 ----- .../maven-scm-provider-gittest/pom.xml | 6 + .../command/info/GitInfoCommandTckTest.java | 110 ++++++++++++++ .../maven-scm-provider-jgit/pom.xml | 5 - .../command/info/JGitInfoCommandTest.java | 136 ------------------ .../maven-scm-providers-git/pom.xml | 2 + 6 files changed, 118 insertions(+), 176 deletions(-) delete mode 100644 maven-scm-providers/maven-scm-providers-git/maven-scm-provider-jgit/src/test/java/org/apache/maven/scm/provider/git/jgit/command/info/JGitInfoCommandTest.java diff --git a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gitexe/src/test/java/org/apache/maven/scm/provider/git/gitexe/command/info/GitInfoCommandTest.java b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gitexe/src/test/java/org/apache/maven/scm/provider/git/gitexe/command/info/GitInfoCommandTest.java index 85aa778f5..9bbc2f305 100644 --- a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gitexe/src/test/java/org/apache/maven/scm/provider/git/gitexe/command/info/GitInfoCommandTest.java +++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gitexe/src/test/java/org/apache/maven/scm/provider/git/gitexe/command/info/GitInfoCommandTest.java @@ -32,7 +32,6 @@ import static org.apache.maven.scm.provider.git.GitScmTestUtils.GIT_COMMAND_LINE; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -93,40 +92,6 @@ void testInfoCommandWithNegativeShortRevision() throws Exception { "revision should not be short"); } - @Test - void testInfoCommandSkipsMergeCommitsByDefault() throws Exception { - checkSystemCmdPresence(GIT_COMMAND_LINE); - - GitScmTestUtils.initRepo("src/test/resources/git/info", getRepositoryRoot(), getWorkingCopy()); - - ScmProvider provider = getScmManager().getProviderByUrl(getScmUrl()); - ScmProviderRepository repository = provider.makeProviderScmRepository(getRepositoryRoot()); - assertNotNull(repository); - InfoScmResult result = provider.info(repository, new ScmFileSet(getRepositoryRoot()), new CommandParameters()); - assertNotNull(result); - assertTrue( - result.getCommandLine().contains("--no-merges"), - "merge commits must be skipped by default (--no-merges present)"); - } - - @Test - void testInfoCommandIncludeMergeCommits() throws Exception { - checkSystemCmdPresence(GIT_COMMAND_LINE); - - GitScmTestUtils.initRepo("src/test/resources/git/info", getRepositoryRoot(), getWorkingCopy()); - - ScmProvider provider = getScmManager().getProviderByUrl(getScmUrl()); - ScmProviderRepository repository = provider.makeProviderScmRepository(getRepositoryRoot()); - assertNotNull(repository); - CommandParameters commandParameters = new CommandParameters(); - commandParameters.setString(CommandParameter.SCM_SKIP_MERGE_COMMITS, Boolean.FALSE.toString()); - InfoScmResult result = provider.info(repository, new ScmFileSet(getRepositoryRoot()), commandParameters); - assertNotNull(result); - assertFalse( - result.getCommandLine().contains("--no-merges"), - "merge commits must be included when skipMergeCommits=false (no --no-merges)"); - } - @Test void testInfoCommandWithZeroShortRevision() throws Exception { checkSystemCmdPresence(GIT_COMMAND_LINE); 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/test/java/org/apache/maven/scm/provider/git/jgit/command/info/JGitInfoCommandTest.java b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-jgit/src/test/java/org/apache/maven/scm/provider/git/jgit/command/info/JGitInfoCommandTest.java deleted file mode 100644 index c64a0c8b6..000000000 --- a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-jgit/src/test/java/org/apache/maven/scm/provider/git/jgit/command/info/JGitInfoCommandTest.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.maven.scm.provider.git.jgit.command.info; - -import java.io.File; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; - -import org.apache.maven.scm.CommandParameter; -import org.apache.maven.scm.CommandParameters; -import org.apache.maven.scm.ScmFileSet; -import org.apache.maven.scm.command.info.InfoScmResult; -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.StoredConfig; -import org.eclipse.jgit.revwalk.RevCommit; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Unit tests for the {@code skipMergeCommits} handling of {@link JGitInfoCommand}. - * A repository whose {@code HEAD} is a merge commit is built with the JGit API, then the - * {@code info} command is invoked with and without the {@link CommandParameter#SCM_SKIP_MERGE_COMMITS} flag. - */ -class JGitInfoCommandTest { - - @TempDir - File workDir; - - @Test - void includesMergeCommitWhenSkipMergeCommitsIsFalse() throws Exception { - ObjectId mergeCommit = buildRepositoryWithMergeHead(); - - CommandParameters parameters = new CommandParameters(); - parameters.setString(CommandParameter.SCM_SKIP_MERGE_COMMITS, Boolean.FALSE.toString()); - - InfoScmResult result = info(parameters); - - assertNotNull(result); - assertEquals(1, result.getInfoItems().size(), "exactly one info item is expected"); - assertEquals( - mergeCommit.getName(), - result.getInfoItems().get(0).getRevision(), - "HEAD merge commit must be reported when skipMergeCommits=false"); - } - - @Test - void skipsMergeCommitByDefault() throws Exception { - ObjectId mergeCommit = buildRepositoryWithMergeHead(); - - InfoScmResult result = info(new CommandParameters()); - - assertNotNull(result); - assertEquals(1, result.getInfoItems().size(), "exactly one info item is expected"); - assertNotEquals( - mergeCommit.getName(), - result.getInfoItems().get(0).getRevision(), - "merge commit must be skipped by default, a non-merge commit must be reported"); - } - - private InfoScmResult info(CommandParameters parameters) throws Exception { - // executeCommand is package-private accessible and ignores the repository argument - return (InfoScmResult) new JGitInfoCommand().executeCommand(null, new ScmFileSet(workDir), parameters); - } - - /** - * Builds a repository whose {@code HEAD} is a no-fast-forward merge commit (two parents). - * - * @return the id of the merge commit which is now {@code HEAD} - */ - private ObjectId buildRepositoryWithMergeHead() throws Exception { - try (Git git = Git.init().setDirectory(workDir).call()) { - 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(); - - commit(git, "a.txt"); - String mainBranch = git.getRepository().getBranch(); - - git.checkout().setCreateBranch(true).setName("feature").call(); - commit(git, "b.txt"); - - git.checkout().setName(mainBranch).call(); - commit(git, "c.txt"); - - MergeResult mergeResult = git.merge() - .include(git.getRepository().resolve("feature")) - .setFastForward(MergeCommand.FastForwardMode.NO_FF) - .setMessage("merge feature") - .call(); - assertTrue( - mergeResult.getMergeStatus().isSuccessful(), - "merge must succeed but was: " + mergeResult.getMergeStatus()); - ObjectId mergeHead = mergeResult.getNewHead(); - assertNotNull(mergeHead, "merge must produce a new HEAD commit"); - return mergeHead; - } - } - - private RevCommit commit(Git git, String fileName) 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(); - return git.commit() - .setMessage("add " + fileName) - .setAuthor("Test User", "test@example.com") - .setCommitter("Test User", "test@example.com") - .setSign(false) - .call(); - } -} 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