Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -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;

Expand Down Expand Up @@ -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);
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,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);
Comment thread
GabriRuflex marked this conversation as resolved.
}

/**
* 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();
}
Comment thread
GabriRuflex marked this conversation as resolved.
}

protected InfoItem getInfoItem(RevCommit fileCommit, File file) {
InfoItem infoItem = new InfoItem();
infoItem.setPath(file.getPath());
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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");
Comment thread
GabriRuflex marked this conversation as resolved.
}

@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");
Comment thread
GabriRuflex marked this conversation as resolved.
}

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();
Comment thread
GabriRuflex marked this conversation as resolved.
Outdated
}
}

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();
}
}