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 ceff4c6fd..7fcc4621c 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
@@ -74,7 +74,7 @@ public class CommandParameter implements Serializable {
public static final CommandParameter SCM_MKDIR_CREATE_IN_LOCAL = new CommandParameter("createInLocal");
/**
- * Parameter used only for Git SCM and simulate the git rev-parse --short=lenght
command.
+ * Parameter used only for Git SCM to truncate the emitted hash to the given character length, simulates git rev-parse --short=length
command.
*
* @since 1.7
*/
diff --git a/maven-scm-api/src/main/java/org/apache/maven/scm/command/info/InfoItem.java b/maven-scm-api/src/main/java/org/apache/maven/scm/command/info/InfoItem.java
index 83837dc91..018b9a0aa 100644
--- a/maven-scm-api/src/main/java/org/apache/maven/scm/command/info/InfoItem.java
+++ b/maven-scm-api/src/main/java/org/apache/maven/scm/command/info/InfoItem.java
@@ -18,7 +18,14 @@
*/
package org.apache.maven.scm.command.info;
+import java.time.OffsetDateTime;
+import java.time.temporal.TemporalAccessor;
+
/**
+ * Encapsulates meta information about one file (or directory) being managed with an SCM.
+ *
+ * For historical reasons the field/method names are inspired from (and sometimes only applicable to) the Subversion SCM.
+ *
* @author Kenney Westerhof
* @author Olivier Lamy
*
@@ -45,6 +52,8 @@ public class InfoItem {
private String lastChangedDate;
+ private OffsetDateTime lastChangedDateTime;
+
public String getPath() {
return path;
}
@@ -117,11 +126,31 @@ public void setLastChangedRevision(String lastChangedRevision) {
this.lastChangedRevision = lastChangedRevision;
}
+ /**
+ * @deprecated Use {@link #getLastModifiedDate()} instead
+ */
+ @Deprecated
public String getLastChangedDate() {
return lastChangedDate;
}
+ /**
+ * @deprecated Use {@link #setLastModifiedDate(TemporalAccessor)} instead
+ */
+ @Deprecated
public void setLastChangedDate(String lastChangedDate) {
this.lastChangedDate = lastChangedDate;
}
+
+ /**
+ *
+ * @return the date when the file indicated via {@link #getPath()} has been changed in the SCM for the last time
+ */
+ OffsetDateTime getLastModifiedDate() {
+ return lastChangedDateTime;
+ }
+
+ public void setLastModifiedDate(TemporalAccessor accessor) {
+ this.lastChangedDateTime = OffsetDateTime.from(accessor);
+ }
}
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 2251f2cb4..544a87960 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
@@ -18,12 +18,18 @@
*/
package org.apache.maven.scm.provider.git.gitexe.command.info;
+import java.io.File;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
import org.apache.maven.scm.CommandParameter;
import org.apache.maven.scm.CommandParameters;
import org.apache.maven.scm.ScmException;
import org.apache.maven.scm.ScmFileSet;
import org.apache.maven.scm.ScmResult;
import org.apache.maven.scm.command.AbstractCommand;
+import org.apache.maven.scm.command.info.InfoItem;
import org.apache.maven.scm.command.info.InfoScmResult;
import org.apache.maven.scm.provider.ScmProviderRepository;
import org.apache.maven.scm.provider.git.command.GitCommand;
@@ -32,6 +38,7 @@
import org.codehaus.plexus.util.cli.Commandline;
/**
+ * Uses {@code git log} command to retrieve info about the most recent commits related to specific files.
* @author Olivier Lamy
* @since 1.5
*/
@@ -43,31 +50,37 @@ public class GitInfoCommand extends AbstractCommand implements GitCommand {
protected ScmResult executeCommand(
ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters parameters) throws ScmException {
- GitInfoConsumer consumer = new GitInfoConsumer(fileSet);
- CommandLineUtils.StringStreamConsumer stderr = new CommandLineUtils.StringStreamConsumer();
-
- Commandline cli = createCommandLine(repository, fileSet, parameters);
+ Commandline baseCli = GitCommandLineUtils.getBaseGitCommandLine(fileSet.getBasedir(), "log");
+ baseCli.createArg().setValue("-1"); // only most recent commit matters
+ baseCli.addArg(GitInfoConsumer.getFormatArgument());
- int exitCode = GitCommandLineUtils.execute(cli, consumer, stderr);
- if (exitCode != 0) {
- return new InfoScmResult(cli.toString(), "The git rev-parse command failed.", stderr.getOutput(), false);
+ List infoItems = new LinkedList<>();
+ // iterate over files
+ for (File scmFile : getScmFilesList(fileSet)) {
+ Commandline cliClone = (Commandline) baseCli.clone();
+ cliClone.createArg().setFile(scmFile);
+ GitInfoConsumer consumer = new GitInfoConsumer(scmFile.toPath(), getRevisionLength(parameters));
+ CommandLineUtils.StringStreamConsumer stderr = new CommandLineUtils.StringStreamConsumer();
+ int exitCode = GitCommandLineUtils.execute(cliClone, consumer, stderr);
+ if (exitCode != 0) {
+ return new InfoScmResult(cliClone.toString(), "The git log command failed.", stderr.getOutput(), false);
+ }
+ infoItems.add(consumer.getInfoItem());
}
- return new InfoScmResult(cli.toString(), consumer.getInfoItems());
+ return new InfoScmResult("", infoItems);
}
- public static Commandline createCommandLine(
- ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters parameters) throws ScmException {
- Commandline cli = GitCommandLineUtils.getBaseGitCommandLine(fileSet.getBasedir(), "rev-parse");
- cli.createArg().setValue("--verify");
- final int revLength = getRevisionLength(parameters);
- if (revLength > NO_REVISION_LENGTH) // set the --short key only if revision length parameter is passed and
- // different from -1
- {
- cli.createArg().setValue("--short=" + revLength);
+ /**
+ *
+ * @param scmFileSet
+ * @return the normalized SCM file list (if no explicit files given, returns just the base directory)
+ */
+ public static List getScmFilesList(ScmFileSet scmFileSet) {
+ if (scmFileSet.getFileList().isEmpty()) {
+ return Collections.singletonList(scmFileSet.getBasedir());
+ } else {
+ return scmFileSet.getFileList();
}
- cli.createArg().setValue("HEAD");
-
- return cli;
}
/**
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/GitInfoConsumer.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/GitInfoConsumer.java
index 7d8066eff..8509ba2fe 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/GitInfoConsumer.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/GitInfoConsumer.java
@@ -18,32 +18,53 @@
*/
package org.apache.maven.scm.provider.git.gitexe.command.info;
-import java.util.ArrayList;
-import java.util.List;
+import java.nio.file.Path;
+import java.time.format.DateTimeFormatter;
import org.apache.commons.lang3.StringUtils;
-import org.apache.maven.scm.ScmFileSet;
import org.apache.maven.scm.command.info.InfoItem;
import org.apache.maven.scm.util.AbstractConsumer;
+import org.codehaus.plexus.util.cli.Arg;
+import org.codehaus.plexus.util.cli.Commandline;
/**
+ * Parses output of {@code git log} with a particular format and populates a {@link InfoItem}.
+ *
* @author Olivier Lamy
* @since 1.5
+ * @see Pretty Formats
*/
public class GitInfoConsumer extends AbstractConsumer {
- // $ git show
- // commit cd3c0dfacb65955e6fbb35c56cc5b1bf8ce4f767
+ private final InfoItem infoItem;
+ private final int revisionLength;
+
+ public GitInfoConsumer(Path path, int revisionLength) {
+ infoItem = new InfoItem();
+ infoItem.setPath(path.toString());
+ infoItem.setURL(path.toUri().toASCIIString());
+ this.revisionLength = revisionLength;
+ }
+
+ enum LineParts {
+ HASH(0),
+ AUTHOR_NAME(3),
+ AUTHOR_EMAIL(2),
+ AUTHOR_LAST_MODIFIED(1);
- private final List infoItems = new ArrayList<>(1);
+ private final int index;
- private final ScmFileSet scmFileSet;
+ LineParts(int index) {
+ this.index = index;
+ }
- public GitInfoConsumer(ScmFileSet scmFileSet) {
- this.scmFileSet = scmFileSet;
+ public int getIndex() {
+ return index;
+ }
}
/**
+ * @param line the line which is supposed to have the format as specified by {@link #getFormatArgument()}.
* @see org.codehaus.plexus.util.cli.StreamConsumer#consumeLine(java.lang.String)
*/
public void consumeLine(String line) {
@@ -51,17 +72,36 @@ public void consumeLine(String line) {
logger.debug("consume line " + line);
}
- if (infoItems.isEmpty()) {
- if (!(line == null || line.isEmpty())) {
- InfoItem infoItem = new InfoItem();
- infoItem.setRevision(StringUtils.trim(line));
- infoItem.setURL(scmFileSet.getBasedir().toPath().toUri().toASCIIString());
- infoItems.add(infoItem);
- }
+ // name must be last token as it may contain separators
+ String[] parts = line.split("\\s", 4);
+ if (parts.length != 4) {
+ throw new IllegalArgumentException(
+ "Unexpected line: expecting 4 tokens separated by whitespace but got " + line);
}
+ infoItem.setLastChangedAuthor(
+ parts[LineParts.AUTHOR_NAME.getIndex()] + " <" + parts[LineParts.AUTHOR_EMAIL.getIndex()] + ">");
+ String revision = parts[LineParts.HASH.getIndex()];
+ if (revisionLength > -1) {
+ // do not truncate below 4 characters
+ revision = StringUtils.truncate(revision, Integer.max(4, revisionLength));
+ }
+ infoItem.setRevision(revision);
+ infoItem.setLastModifiedDate(
+ DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(parts[LineParts.AUTHOR_LAST_MODIFIED.getIndex()]));
+ }
+
+ public InfoItem getInfoItem() {
+ return infoItem;
}
- public List getInfoItems() {
- return infoItems;
+ /**
+ * The format argument to use with {@code git log}
+ * @return the format argument to use {@code git log} command
+ * @see Pretty Formats
+ */
+ public static Arg getFormatArgument() {
+ Commandline.Argument arg = new Commandline.Argument();
+ arg.setValue("--format=format:%H %aI %aE %aN");
+ return arg;
}
}
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 e94b3a3ba..57536280f 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
@@ -109,7 +109,7 @@ public void testInfoCommandWithZeroShortRevision() throws Exception {
InfoScmResult result = provider.info(repository, new ScmFileSet(getRepositoryRoot()), commandParameters);
assertNotNull(result);
assertTrue(
- "revision should be not empty, minimum 4 (see git help rev-parse --short)",
+ "revision should be not empty, minimum 4 (similar to git help rev-parse --short)",
result.getInfoItems().get(0).getRevision().length() >= 4);
}
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 320b3e589..93698454d 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
@@ -19,7 +19,9 @@
package org.apache.maven.scm.provider.git.jgit.command.info;
import java.io.File;
-import java.util.Collections;
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.scm.CommandParameters;
@@ -33,7 +35,12 @@
import org.apache.maven.scm.provider.git.command.GitCommand;
import org.apache.maven.scm.provider.git.jgit.command.JGitUtils;
import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.treewalk.TreeWalk;
/**
* @since 1.9.5
@@ -44,23 +51,45 @@ protected ScmResult executeCommand(
ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters parameters) throws ScmException {
Git git = null;
try {
+ // TODO: basedir is not necessarily the root of the working copy
File basedir = fileSet.getBasedir();
git = Git.open(basedir);
- ObjectId objectId = git.getRepository().resolve("HEAD");
+ ObjectId objectId = git.getRepository().resolve(Constants.HEAD);
+ RevCommit headCommit = git.getRepository().parseCommit(objectId);
- InfoItem infoItem = new InfoItem();
- infoItem.setRevision(StringUtils.trim(objectId.name()));
- infoItem.setURL(basedir.toPath().toUri().toASCIIString());
+ List infoItems = new LinkedList<>();
+ // iterate over all files
+ for (File file : fileSet.getFileList()) {
+ RevCommit fileCommit = getMostRecentCommitForPath(git.getRepository(), headCommit, file.getPath());
+ InfoItem infoItem = new InfoItem();
+ infoItem.setPath(file.getPath());
+ infoItem.setRevision(StringUtils.trim(fileCommit.name()));
+ // TODO: what is the URL in this context?
+ infoItem.setURL(basedir.toPath().toUri().toASCIIString());
+ PersonIdent authorIdent = fileCommit.getAuthorIdent();
+ infoItem.setLastModifiedDate(authorIdent
+ .getWhen()
+ .toInstant()
+ .atZone(authorIdent.getTimeZone().toZoneId()));
+ infoItem.setLastChangedAuthor(authorIdent.getName() + " <" + authorIdent.getEmailAddress() + ">");
+ infoItems.add(infoItem);
+ }
- return new InfoScmResult(
- Collections.singletonList(infoItem),
- new ScmResult("JGit.resolve(HEAD)", "", objectId.toString(), true));
+ return new InfoScmResult(infoItems, new ScmResult("JGit.resolve(HEAD)", "", objectId.toString(), true));
} catch (Exception e) {
throw new ScmException("JGit resolve failure!", e);
} finally {
JGitUtils.closeRepo(git);
}
}
+
+ private RevCommit getMostRecentCommitForPath(Repository repository, RevCommit commit, String path)
+ throws IOException {
+ try (TreeWalk treeWalk = TreeWalk.forPath(repository, path, commit.getTree())) {
+ ObjectId fileObjectId = treeWalk.getObjectId(0);
+ return repository.parseCommit(fileObjectId);
+ }
+ }
}
diff --git a/maven-scm-providers/maven-scm-providers-svn/maven-scm-provider-svnexe/src/main/java/org/apache/maven/scm/provider/svn/svnexe/command/info/SvnInfoConsumer.java b/maven-scm-providers/maven-scm-providers-svn/maven-scm-provider-svnexe/src/main/java/org/apache/maven/scm/provider/svn/svnexe/command/info/SvnInfoConsumer.java
index 0a4a71680..6a28bbc49 100644
--- a/maven-scm-providers/maven-scm-providers-svn/maven-scm-provider-svnexe/src/main/java/org/apache/maven/scm/provider/svn/svnexe/command/info/SvnInfoConsumer.java
+++ b/maven-scm-providers/maven-scm-providers-svn/maven-scm-provider-svnexe/src/main/java/org/apache/maven/scm/provider/svn/svnexe/command/info/SvnInfoConsumer.java
@@ -18,6 +18,8 @@
*/
package org.apache.maven.scm.provider.svn.svnexe.command.info;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.List;
@@ -33,6 +35,8 @@ public class SvnInfoConsumer extends AbstractConsumer {
private InfoItem currentItem = new InfoItem();
+ private static final DateTimeFormatter DT_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z");
+
/** {@inheritDoc} */
public void consumeLine(String s) {
if (s.equals("")) {
@@ -60,6 +64,7 @@ public void consumeLine(String s) {
} else if (s.startsWith("Last Changed Rev: ")) {
currentItem.setLastChangedRevision(getValue(s));
} else if (s.startsWith("Last Changed Date: ")) {
+ currentItem.setLastModifiedDate(parseDate(getValue(s)));
currentItem.setLastChangedDate(getValue(s));
}
}
@@ -78,4 +83,13 @@ private static String getValue(String s) {
public List getInfoItems() {
return infoItems;
}
+
+ static TemporalAccessor parseDate(String dateText) {
+ // strip the tailing text in parenthesis
+ int startSuffix = dateText.indexOf('(');
+ if (startSuffix != -1) {
+ dateText = dateText.substring(0, startSuffix);
+ }
+ return DT_FORMATTER.parse(dateText.trim());
+ }
}
diff --git a/maven-scm-providers/maven-scm-providers-svn/maven-scm-provider-svnexe/src/test/java/org/apache/maven/scm/provider/svn/svnexe/command/info/SvnInfoConsumerTest.java b/maven-scm-providers/maven-scm-providers-svn/maven-scm-provider-svnexe/src/test/java/org/apache/maven/scm/provider/svn/svnexe/command/info/SvnInfoConsumerTest.java
new file mode 100644
index 000000000..d74c8adad
--- /dev/null
+++ b/maven-scm-providers/maven-scm-providers-svn/maven-scm-provider-svnexe/src/test/java/org/apache/maven/scm/provider/svn/svnexe/command/info/SvnInfoConsumerTest.java
@@ -0,0 +1,35 @@
+/*
+ * 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.svn.svnexe.command.info;
+
+import java.time.temporal.ChronoField;
+import java.time.temporal.TemporalAccessor;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class SvnInfoConsumerTest {
+
+ @Test
+ public void testParseDate() {
+ TemporalAccessor date = SvnInfoConsumer.parseDate("2024-01-19 16:33:05 +0100 (Fr, 19 Jan 2024");
+ assertEquals(2024, date.get(ChronoField.YEAR));
+ }
+}