From 27d69fe275bd09973fadd94ffd0477c4627d8ff5 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 | 34 +++++++ .../apache/maven/scm/util/FilenameUtils.java | 2 +- .../gitexe/command/info/GitInfoCommand.java | 52 ++++++---- .../gitexe/command/info/GitInfoConsumer.java | 78 +++++++++++---- .../info/GitExeInfoCommandTckTest.java | 29 ++++++ .../command/info/GitInfoCommandTest.java | 2 +- .../command/info/GitInfoCommandTckTest.java | 33 +++++++ .../provider/git/jgit/command/JGitUtils.java | 95 ++++++++++--------- .../jgit/command/info/JGitInfoCommand.java | 77 +++++++++++---- .../command/info/JGitInfoCommandTckTest.java | 29 ++++++ .../svnexe/command/info/SvnInfoConsumer.java | 14 +++ .../command/info/SvnInfoConsumerTest.java | 35 +++++++ .../command/info/SvnInfoCommandTckTest.java | 36 +++++++ .../tck/command/info/InfoCommandTckTest.java | 66 +++++++++++++ 15 files changed, 479 insertions(+), 105 deletions(-) create mode 100644 maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gitexe/src/test/java/org/apache/maven/scm/provider/git/gitexe/command/info/GitExeInfoCommandTckTest.java create mode 100644 maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/command/info/GitInfoCommandTckTest.java 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/JGitInfoCommandTckTest.java 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 create mode 100644 maven-scm-providers/maven-scm-providers-svn/maven-scm-provider-svntest/src/main/java/org/apache/maven/scm/provider/svn/command/info/SvnInfoCommandTckTest.java create mode 100644 maven-scm-test/src/main/java/org/apache/maven/scm/tck/command/info/InfoCommandTckTest.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..4c18c714f 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 a 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,36 @@ public void setLastChangedRevision(String lastChangedRevision) { this.lastChangedRevision = lastChangedRevision; } + /** + * @deprecated Use {@link #getLastChangedDateTime()} instead + */ + @Deprecated public String getLastChangedDate() { return lastChangedDate; } + /** + * @deprecated Use {@link #setLastChangedDateTime(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 + * @since 2.1.0 + */ + public OffsetDateTime getLastChangedDateTime() { + return lastChangedDateTime; + } + + /** + * @param accessor temporal accessor from which to populate the last changed date + * @since 2.1.0 + */ + public void setLastChangedDateTime(TemporalAccessor accessor) { + this.lastChangedDateTime = OffsetDateTime.from(accessor); + } } diff --git a/maven-scm-api/src/main/java/org/apache/maven/scm/util/FilenameUtils.java b/maven-scm-api/src/main/java/org/apache/maven/scm/util/FilenameUtils.java index 6994deff4..89bffb5fc 100644 --- a/maven-scm-api/src/main/java/org/apache/maven/scm/util/FilenameUtils.java +++ b/maven-scm-api/src/main/java/org/apache/maven/scm/util/FilenameUtils.java @@ -29,7 +29,7 @@ public final class FilenameUtils { private FilenameUtils() {} public static String normalizeFilename(File file) { - return normalizeFilename(file.getName()); + return normalizeFilename(file.getPath()); } /** 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..e499a92a4 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,36 @@ 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.createArg().setValue("--no-merges"); // skip merge commits + 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<>(); + if (fileSet.getFileList().isEmpty()) { + infoItems.add(executeInfoCommand(baseCli, parameters, fileSet.getBasedir())); + } else { + // Insert a separator to make sure that files aren't interpreted as part of the version spec + baseCli.createArg().setValue("--"); + // iterate over files + for (File scmFile : fileSet.getFileList()) { + Commandline cliClone = (Commandline) baseCli.clone(); + GitCommandLineUtils.addTarget(cliClone, Collections.singletonList(scmFile)); + infoItems.add(executeInfoCommand(cliClone, parameters, scmFile)); + } } - return new InfoScmResult(cli.toString(), consumer.getInfoItems()); + return new InfoScmResult(baseCli.toString(), 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); + protected InfoItem executeInfoCommand(Commandline cli, CommandParameters parameters, File scmFile) + throws ScmException { + GitInfoConsumer consumer = new GitInfoConsumer(scmFile.toPath(), getRevisionLength(parameters)); + CommandLineUtils.StringStreamConsumer stderr = new CommandLineUtils.StringStreamConsumer(); + int exitCode = GitCommandLineUtils.execute(cli, consumer, stderr); + if (exitCode != 0) { + throw new ScmException("The git log command failed: " + cli.toString() + " returned " + stderr.getOutput()); } - cli.createArg().setValue("HEAD"); - - return cli; + return consumer.getInfoItem(); } /** 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..3c1177983 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,50 +18,90 @@ */ 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) { if (logger.isDebugEnabled()) { - logger.debug("consume line " + 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.setLastChangedDateTime( + 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/GitExeInfoCommandTckTest.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/GitExeInfoCommandTckTest.java new file mode 100644 index 000000000..cd1f7a73c --- /dev/null +++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gitexe/src/test/java/org/apache/maven/scm/provider/git/gitexe/command/info/GitExeInfoCommandTckTest.java @@ -0,0 +1,29 @@ +/* + * 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.gitexe.command.info; + +import org.apache.maven.scm.provider.git.GitScmTestUtils; +import org.apache.maven.scm.provider.git.command.info.GitInfoCommandTckTest; + +public class GitExeInfoCommandTckTest extends GitInfoCommandTckTest { + + public String getScmUrl() throws Exception { + return GitScmTestUtils.getScmUrl(getRepositoryRoot(), "git"); + } +} 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-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 new file mode 100644 index 000000000..65d6048b8 --- /dev/null +++ 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 @@ -0,0 +1,33 @@ +/* + * 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.command.info; + +import org.apache.maven.scm.provider.git.GitScmTestUtils; +import org.apache.maven.scm.tck.command.info.InfoCommandTckTest; + +/** + * @author Mark Struberg + * + */ +public abstract class GitInfoCommandTckTest extends InfoCommandTckTest { + /** {@inheritDoc} */ + public void initRepo() throws Exception { + GitScmTestUtils.initRepo("src/test/resources/repository/", getRepositoryRoot(), getWorkingDirectory()); + } +} 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/JGitUtils.java b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-jgit/src/main/java/org/apache/maven/scm/provider/git/jgit/command/JGitUtils.java index d26f86014..1f6484e59 100644 --- a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-jgit/src/main/java/org/apache/maven/scm/provider/git/jgit/command/JGitUtils.java +++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-jgit/src/main/java/org/apache/maven/scm/provider/git/jgit/command/JGitUtils.java @@ -21,7 +21,6 @@ import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.net.URI; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collection; @@ -29,13 +28,13 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.function.BiConsumer; import org.apache.commons.lang3.StringUtils; import org.apache.maven.scm.ScmFile; import org.apache.maven.scm.ScmFileSet; import org.apache.maven.scm.ScmFileStatus; import org.apache.maven.scm.provider.git.repository.GitScmProviderRepository; +import org.apache.maven.scm.util.FilenameUtils; import org.eclipse.jgit.api.AddCommand; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.PushCommand; @@ -256,7 +255,8 @@ public static List getFilesInCommit(Repository repository, RevCommit co for (DiffEntry diff : diffs) { final String path; if (baseDir != null) { - path = relativize(baseDir.toURI(), new File(repository.getWorkTree(), diff.getNewPath())); + path = relativize(baseDir, new File(repository.getWorkTree(), diff.getNewPath())) + .getPath(); } else { path = diff.getNewPath(); } @@ -300,16 +300,10 @@ public static ScmFileStatus getScmFileStatus(ChangeType changeType) { * @throws GitAPIException */ public static List addAllFiles(Git git, ScmFileSet fileSet) throws GitAPIException { - URI workingCopyRootUri = git.getRepository().getWorkTree().toURI(); + File workingCopyRootDirectory = git.getRepository().getWorkTree(); AddCommand add = git.add(); - callWithRepositoryRelativeFilePath( - (relativeFile, absoluteFile) -> { - if (absoluteFile.exists()) { - add.addFilepattern(relativeFile); - } - }, - workingCopyRootUri, - fileSet); + getWorkingCopyRelativePaths(workingCopyRootDirectory, fileSet).stream() + .forEach(f -> add.addFilepattern(toNormalizedFilePath(f))); add.call(); Status status = git.status().call(); @@ -318,7 +312,7 @@ public static List addAllFiles(Git git, ScmFileSet fileSet) throws GitA allInIndex.addAll(status.getAdded()); allInIndex.addAll(status.getChanged()); return getScmFilesForAllFileSetFilesContainedInRepoPath( - workingCopyRootUri, fileSet, allInIndex, ScmFileStatus.ADDED); + workingCopyRootDirectory, fileSet, allInIndex, ScmFileStatus.ADDED); } /** @@ -331,61 +325,68 @@ public static List addAllFiles(Git git, ScmFileSet fileSet) throws GitA * @throws GitAPIException */ public static List removeAllFiles(Git git, ScmFileSet fileSet) throws GitAPIException { - URI workingCopyRootUri = git.getRepository().getWorkTree().toURI(); + File workingCopyRootDirectory = git.getRepository().getWorkTree(); RmCommand remove = git.rm(); - callWithRepositoryRelativeFilePath( - (relativeFile, absoluteFile) -> remove.addFilepattern(relativeFile), workingCopyRootUri, fileSet); + getWorkingCopyRelativePaths(workingCopyRootDirectory, fileSet).stream() + .forEach(f -> remove.addFilepattern(toNormalizedFilePath(f))); remove.call(); Status status = git.status().call(); Set allInIndex = new HashSet<>(status.getRemoved()); return getScmFilesForAllFileSetFilesContainedInRepoPath( - workingCopyRootUri, fileSet, allInIndex, ScmFileStatus.DELETED); + workingCopyRootDirectory, fileSet, allInIndex, ScmFileStatus.DELETED); } /** - * For each file in the {@code fileSet} call the {@code fileCallback} with the file path relative to the repository - * root (forward slashes as separator) and the absolute file path. - * @param repoFileCallback the callback to call for each file in the fileset - * @param git the git repository - * @param fileSet the file set to traverse + * Convert each file in the {@code fileSet} to their relative file path to workingCopyDirectory + * and return them in a list. + * @param workingCopyDirectory the working copy root directory + * @param fileSet the file set to convert */ - private static void callWithRepositoryRelativeFilePath( - BiConsumer fileCallback, URI workingCopyRootUri, ScmFileSet fileSet) { - for (File file : fileSet.getFileList()) { - if (!file.isAbsolute()) { - file = new File(fileSet.getBasedir().getPath(), file.getPath()); + public static List getWorkingCopyRelativePaths(File workingCopyDirectory, ScmFileSet fileSet) { + List repositoryRelativePaths = new ArrayList<>(); + for (File path : fileSet.getFileList()) { + if (!path.isAbsolute()) { + path = new File(fileSet.getBasedir().getPath(), path.getPath()); } - String path = relativize(workingCopyRootUri, file); - fileCallback.accept(path, file); + File repositoryRelativePath = relativize(workingCopyDirectory, path); + repositoryRelativePaths.add(repositoryRelativePath); } + return repositoryRelativePaths; + } + + /** + * Converts the given file to a string only containing forward slashes + * @param file + * @return the normalized file path + */ + public static String toNormalizedFilePath(File file) { + return FilenameUtils.normalizeFilename(file); } private static List getScmFilesForAllFileSetFilesContainedInRepoPath( - URI workingCopyRootUri, ScmFileSet fileSet, Set repoFilePaths, ScmFileStatus fileStatus) { + File workingCopyDirectory, ScmFileSet fileSet, Set repoFilePaths, ScmFileStatus fileStatus) { List files = new ArrayList<>(repoFilePaths.size()); - callWithRepositoryRelativeFilePath( - (relativeFile, absoluteFile) -> { - // check if repo relative path is contained - if (repoFilePaths.contains(relativeFile)) { - // returned ScmFiles should be relative to given fileset's basedir - ScmFile scmfile = - new ScmFile(relativize(fileSet.getBasedir().toURI(), absoluteFile), fileStatus); - files.add(scmfile); - } - }, - workingCopyRootUri, - fileSet); + getWorkingCopyRelativePaths(workingCopyDirectory, fileSet).stream().forEach((relativeFile) -> { + // check if repo relative path is contained + if (repoFilePaths.contains(toNormalizedFilePath(relativeFile))) { + // returned ScmFiles should be relative to given fileset's basedir + ScmFile scmfile = new ScmFile( + relativize(fileSet.getBasedir(), new File(workingCopyDirectory, relativeFile.getPath())) + .getPath(), + fileStatus); + files.add(scmfile); + } + }); return files; } - private static String relativize(URI baseUri, File f) { - String path = f.getPath(); - if (f.isAbsolute()) { - path = baseUri.relativize(new File(path).toURI()).getPath(); + private static File relativize(File baseDir, File file) { + if (file.isAbsolute()) { + return baseDir.toPath().relativize(file.toPath()).toFile(); } - return path; + return file; } /** 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..edf562d81 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,16 @@ 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.revwalk.RevSort; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.filter.AndTreeFilter; +import org.eclipse.jgit.treewalk.filter.PathFilter; +import org.eclipse.jgit.treewalk.filter.TreeFilter; /** * @since 1.9.5 @@ -42,25 +53,59 @@ public class JGitInfoCommand extends AbstractCommand implements GitCommand { @Override protected ScmResult executeCommand( ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters parameters) throws ScmException { - Git git = null; - try { - File basedir = fileSet.getBasedir(); + File basedir = fileSet.getBasedir(); + try (Git git = JGitUtils.openRepo(basedir); ) { + ObjectId objectId = git.getRepository().resolve(Constants.HEAD); + if (objectId == null) { + throw new ScmException("Cannot resolve HEAD in git repository at " + basedir); + } - git = Git.open(basedir); + List infoItems = new LinkedList<>(); + if (fileSet.getFileList().isEmpty()) { + RevCommit headCommit = git.getRepository().parseCommit(objectId); + infoItems.add(getInfoItem(headCommit, fileSet.getBasedir())); + } else { + // iterate over all files + for (File file : JGitUtils.getWorkingCopyRelativePaths( + git.getRepository().getWorkTree(), fileSet)) { + infoItems.add(getInfoItem(git.getRepository(), objectId, file)); + } + } + return new InfoScmResult(infoItems, new ScmResult("JGit.resolve(HEAD)", "", objectId.toString(), true)); + } catch (Exception e) { + throw new ScmException("JGit resolve failure!", e); + } + } - ObjectId objectId = git.getRepository().resolve("HEAD"); + protected InfoItem getInfoItem(Repository repository, ObjectId headObjectId, File file) throws IOException { + RevCommit commit = getMostRecentCommitForPath(repository, headObjectId, JGitUtils.toNormalizedFilePath(file)); + return getInfoItem(commit, file); + } - InfoItem infoItem = new InfoItem(); - infoItem.setRevision(StringUtils.trim(objectId.name())); - infoItem.setURL(basedir.toPath().toUri().toASCIIString()); + 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()); + PersonIdent authorIdent = fileCommit.getAuthorIdent(); + infoItem.setLastChangedDateTime(authorIdent + .getWhen() + .toInstant() + .atZone(authorIdent.getTimeZone().toZoneId())); + infoItem.setLastChangedAuthor(authorIdent.getName() + " <" + authorIdent.getEmailAddress() + ">"); + return infoItem; + } - return new InfoScmResult( - Collections.singletonList(infoItem), - 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, ObjectId headObjectId, String path) + 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); + revWalk.setTreeFilter(AndTreeFilter.create(PathFilter.create(path), TreeFilter.ANY_DIFF)); + latestCommit = revWalk.next(); } + return latestCommit; } } 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/JGitInfoCommandTckTest.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/JGitInfoCommandTckTest.java new file mode 100644 index 000000000..05f410fb9 --- /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/JGitInfoCommandTckTest.java @@ -0,0 +1,29 @@ +/* + * 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 org.apache.maven.scm.provider.git.GitScmTestUtils; +import org.apache.maven.scm.provider.git.command.info.GitInfoCommandTckTest; + +public class JGitInfoCommandTckTest extends GitInfoCommandTckTest { + + public String getScmUrl() throws Exception { + return GitScmTestUtils.getScmUrl(getRepositoryRoot(), "jgit"); + } +} 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..453023571 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.setLastChangedDateTime(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)); + } +} diff --git a/maven-scm-providers/maven-scm-providers-svn/maven-scm-provider-svntest/src/main/java/org/apache/maven/scm/provider/svn/command/info/SvnInfoCommandTckTest.java b/maven-scm-providers/maven-scm-providers-svn/maven-scm-provider-svntest/src/main/java/org/apache/maven/scm/provider/svn/command/info/SvnInfoCommandTckTest.java new file mode 100644 index 000000000..541c6d92c --- /dev/null +++ b/maven-scm-providers/maven-scm-providers-svn/maven-scm-provider-svntest/src/main/java/org/apache/maven/scm/provider/svn/command/info/SvnInfoCommandTckTest.java @@ -0,0 +1,36 @@ +/* + * 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.command.info; + +import java.io.File; + +import org.apache.maven.scm.provider.svn.SvnScmTestUtils; +import org.apache.maven.scm.tck.command.info.InfoCommandTckTest; + +public class SvnInfoCommandTckTest extends InfoCommandTckTest { + /** {@inheritDoc} */ + public String getScmUrl() throws Exception { + return SvnScmTestUtils.getScmUrl(new File(getRepositoryRoot(), "trunk")); + } + + /** {@inheritDoc} */ + public void initRepo() throws Exception { + SvnScmTestUtils.initializeRepository(getRepositoryRoot()); + } +} diff --git a/maven-scm-test/src/main/java/org/apache/maven/scm/tck/command/info/InfoCommandTckTest.java b/maven-scm-test/src/main/java/org/apache/maven/scm/tck/command/info/InfoCommandTckTest.java new file mode 100644 index 000000000..cb89265b4 --- /dev/null +++ b/maven-scm-test/src/main/java/org/apache/maven/scm/tck/command/info/InfoCommandTckTest.java @@ -0,0 +1,66 @@ +/* + * 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.tck.command.info; + +import java.io.File; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +import org.apache.maven.scm.ScmFileSet; +import org.apache.maven.scm.ScmTckTestCase; +import org.apache.maven.scm.command.info.InfoItem; +import org.apache.maven.scm.command.info.InfoScmResult; +import org.apache.maven.scm.provider.ScmProvider; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * This test tests the info command. + * + */ +public abstract class InfoCommandTckTest extends ScmTckTestCase { + + @Test + public void testInfoCommandWithJustBasedir() throws Exception { + ScmProvider scmProvider = getScmManager().getProviderByUrl(getScmUrl()); + InfoScmResult result = scmProvider.info(getScmRepository().getProviderRepository(), getScmFileSet(), null); + assertResultIsSuccess(result); + assertEquals(1, result.getInfoItems().size()); + InfoItem item = result.getInfoItems().get(0); + assertEquals("Mark Struberg ", item.getLastChangedAuthor()); + assertEquals("92f139dfec4d1dfb79c3cd2f94e83bf13129668b", item.getRevision()); + assertEquals( + OffsetDateTime.of(2009, 03, 15, 19, 14, 02, 0, ZoneOffset.ofHours(1)), item.getLastChangedDateTime()); + } + + @Test + public void testInfoCommandFromBasedirDifferentFromWorkingCopyDirectory() throws Exception { + ScmProvider scmProvider = getScmManager().getProviderByUrl(getScmUrl()); + ScmFileSet fileSet = new ScmFileSet(new File(getWorkingCopy(), "src/main"), new File("java/Application.java")); + InfoScmResult result = scmProvider.info(getScmRepository().getProviderRepository(), fileSet, null); + assertResultIsSuccess(result); + assertEquals(1, result.getInfoItems().size()); + InfoItem item = result.getInfoItems().get(0); + assertEquals("Mark Struberg ", item.getLastChangedAuthor()); + assertEquals("92f139dfec4d1dfb79c3cd2f94e83bf13129668b", item.getRevision()); + assertEquals( + OffsetDateTime.of(2009, 03, 15, 19, 14, 02, 0, ZoneOffset.ofHours(1)), item.getLastChangedDateTime()); + } +}