From 16b83d0cf3502acaea012d3edc686312d4c1a19d Mon Sep 17 00:00:00 2001 From: Konrad Windszus Date: Fri, 9 Feb 2024 17:14:49 +0100 Subject: [PATCH] [SCM-914] Introduce properly typed last modified date Populate it with svnexe, gitexe and JGit providers --- .../apache/maven/scm/CommandParameter.java | 2 +- .../maven/scm/command/info/InfoItem.java | 29 +++++++ .../gitexe/command/info/GitInfoCommand.java | 53 ++++++++----- .../gitexe/command/info/GitInfoConsumer.java | 76 ++++++++++++++----- .../command/info/GitInfoCommandTest.java | 2 +- .../jgit/command/info/JGitInfoCommand.java | 45 +++++++++-- .../svnexe/command/info/SvnInfoConsumer.java | 14 ++++ .../command/info/SvnInfoConsumerTest.java | 35 +++++++++ 8 files changed, 208 insertions(+), 48 deletions(-) create mode 100644 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 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)); + } +}