diff --git a/.buildscript/settings.xml b/.buildscript/settings.xml index 5086713..5ab0fd4 100644 --- a/.buildscript/settings.xml +++ b/.buildscript/settings.xml @@ -1,7 +1,7 @@ - ossrh + central ${env.CI_DEPLOY_USERNAME} ${env.CI_DEPLOY_PASSWORD} diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..af94b60 --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,16 @@ +[bumpversion] +current_version = 6.1.5 +commit = True +tag = True +tag_name = v{new_version} +parse = (?P\d+)\.(?P\d+)\.(?P\d+) +serialize = + {major}.{minor}.{patch} + +[bumpversion:file:pom.xml] +search = {current_version} +replace = {new_version} + +[bumpversion:file:./pom.xml] +search = v{current_version} +replace = v{new_version} diff --git a/.github/ISSUE_TEMPLATE/new_release.yml b/.github/ISSUE_TEMPLATE/new_release.yml index a54422b..0581ed2 100644 --- a/.github/ISSUE_TEMPLATE/new_release.yml +++ b/.github/ISSUE_TEMPLATE/new_release.yml @@ -19,21 +19,24 @@ body: Fixed some minor things, added some minor features...(full list https://github.com/git-commit-id/git-commit-id-plugin-core//issues?q=milestone%3A6.0.0-rc.2). # Release-Guide - see http://central.sonatype.org/pages/ossrh-guide.html + see + - https://central.sonatype.org/publish/publish-guide + - https://central.sonatype.org/publish/publish-portal-maven/ + - (OSSRH was deprecated on June 30, 2025 -- http://central.sonatype.org/pages/ossrh-guide.html) + + The release process has changed from the old OSSRH staging to the new Central Portal: + - Option 1: Use the new central-publishing-maven-plugin (recommended for automated releases) + - Option 2: Use the OSSRH Staging API Service (compatibility layer) - - [ ] verify that `~/.m2/settings.xml` exists and contains username/password which is required as per https://central.sonatype.org/publish/publish-maven/#distribution-management-and-authentication - - [ ] `mvn release:prepare` - [INFO] Checking dependencies and plugins for snapshots ... - What is the release version for "Git Commit Id Plugin Core"? (io.github.git-commit-id:git-commit-id-plugin-core) **6.0.0**: : [ENTER] - What is SCM release tag or label for "Git Commit Id Plugin Core"? (io.github.git-commit-id:git-commit-id-plugin-core) git-commit-id-plugin-core-6.0.0: : **v6.0.0** [ENTER] - What is the new development version for "Git Commit Id Plugin Core"? (io.github.git-commit-id:git-commit-id-plugin-core) **6.0.1-SNAPSHOT**: : [ENTER] + - [ ] verify that `~/.m2/settings.xml` exists (https://central.sonatype.org/publish/publish-portal-maven/#credentials) + - contains user token credentials from https://central.sonatype.com/ (generate token at https://central.sonatype.org/publish/generate-portal-token/) + - [ ] Set release version: `bump2version patch --dry-run --verbose` + - [ ] Push changes and tag: `git push origin master --follow-tags` - [ ] wait for github actions to pass - - [ ] `mvn release:perform` - - (or `mvn clean source:jar javadoc:jar deploy -Pgpg` from the git tag) - - (or `mvn release:perform -Dresume=false`) - - Note: If the uploading of the artifacts fails, ensure that a [`settings.xml`](https://github.com/git-commit-id/git-commit-id-maven-plugin/blob/master/.buildscript/settings.xml) exists under the local `.m2`-Folder - - [ ] then go to https://s01.oss.sonatype.org/ log in there and go to the staging repositories, there will be the plugin, you have to first close and then release it if validation passed. + - [ ] `mvn clean deploy -Pgpg` - [ ] verify plugin is available on (might take some time) https://repo1.maven.org/maven2/io/github/git-commit-id/git-commit-id-plugin-core/ + - [ ] Set next development version: `bump2version patch --dry-run --verbose` + - [ ] Push snapshot version: `git push origin master` - [ ] under [Milestones](https://github.com/git-commit-id/git-commit-id-plugin-core/milestones) close old milestone - [ ] under [Milestones](https://github.com/git-commit-id/git-commit-id-plugin-core/milestones) create new milestone for new version - [ ] under [Releases](https://github.com/git-commit-id/git-commit-id-plugin-core/releases) publish Release-Notes diff --git a/.github/workflows/default-tests.yml b/.github/workflows/default-tests.yml index 0ccd981..826a420 100644 --- a/.github/workflows/default-tests.yml +++ b/.github/workflows/default-tests.yml @@ -7,17 +7,17 @@ jobs: name: Run checkstyle runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: recursive - name: Set up JDK 11 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: 11 java-package: jdk - name: Cache local Maven repository - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} @@ -31,19 +31,19 @@ jobs: needs: checkstyle strategy: matrix: - java_version: [ '11', '12', '13', '14', '15', '16', '17', '18', '19', '20' ] + java_version: [ '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26' ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: recursive - name: Set up JDK ${{ matrix.java_version }} - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: ${{ matrix.java_version }} java-package: jdk - name: Cache local Maven repository - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} @@ -58,17 +58,17 @@ jobs: if: ${{ github.event_name != 'pull_request' && !startsWith(github.ref, 'refs/tags/') && github.ref == 'refs/heads/master' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: recursive - name: Set up JDK 11 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: 11 java-package: jdk - name: Cache local Maven repository - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} diff --git a/pom.xml b/pom.xml index 45e10ca..de348e3 100644 --- a/pom.xml +++ b/pom.xml @@ -1,19 +1,24 @@ 4.0.0 - - org.sonatype.oss - oss-parent - 9 - - io.github.git-commit-id git-commit-id-plugin-core jar - 6.0.0 + 6.1.5 Git Commit Id Plugin Core + A library for extracting git information at build time https://github.com/git-commit-id/git-commit-id-plugin-core + + + git-commit-id + Git Commit ID Community + github@git-commit-id.io + git-commit-id + https://github.com/git-commit-id + + + GNU Lesser General Public License 3.0 @@ -25,7 +30,7 @@ git@github.com:git-commit-id/git-commit-id-plugin-core.git scm:git@github.com:git-commit-id/git-commit-id-plugin-core scm:git:git@github.com:git-commit-id/git-commit-id-plugin-core.git - v6.0.0 + v6.1.5 @@ -44,10 +49,10 @@ https://github.com/eclipse-jgit/jgit/issues/28 https://github.com/eclipse-jgit/jgit/issues/36 --> - 6.10.0.202406032230-r - 5.12.2 - 5.17.0 - 3.27.3 + 6.10.1.202505221210-r + 5.13.1 + 5.18.0 + 3.27.7 @@ -67,12 +72,12 @@ org.apache.maven.plugins maven-antrun-plugin - 3.1.0 + 3.2.0 org.apache.maven.plugins maven-assembly-plugin - 3.7.1 + 3.8.0 org.apache.maven.plugins @@ -82,7 +87,7 @@ org.apache.maven.plugins maven-release-plugin - 3.1.1 + 3.3.1 -Pgpg @@ -90,32 +95,38 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.5.0 + 3.6.2 org.apache.maven.plugins maven-compiler-plugin - 3.14.0 + 3.15.0 org.apache.maven.plugins maven-gpg-plugin - 3.2.7 + 3.2.8 + + + --pinentry-mode + loopback + + org.apache.maven.plugins maven-clean-plugin - 3.4.1 + 3.5.0 org.apache.maven.plugins maven-resources-plugin - 3.3.1 + 3.5.0 org.apache.maven.plugins maven-jar-plugin - 3.4.2 + 3.5.0 org.apache.maven.plugins @@ -125,7 +136,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.5.3 + 3.5.5 org.apache.maven.plugins @@ -133,9 +144,9 @@ 3.1.4 - org.apache.maven.plugins - maven-deploy-plugin - 3.1.4 + org.sonatype.central + central-publishing-maven-plugin + 0.10.0 org.apache.maven.plugins @@ -145,7 +156,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.11.2 + 3.12.0 org.apache.maven.plugins @@ -157,6 +168,16 @@ + + org.sonatype.central + central-publishing-maven-plugin + true + + central + true + published + + org.apache.maven.plugins @@ -169,7 +190,7 @@ org.codehaus.mojo versions-maven-plugin - 2.18.0 + 2.21.0 .*-M.*,.*-alpha.* @@ -222,7 +243,7 @@ joda-time joda-time - 2.14.0 + 2.14.1 com.google.code.findbugs @@ -248,7 +269,7 @@ org.yaml snakeyaml - 2.4 + 2.6 @@ -285,7 +306,7 @@ commons-io commons-io - 2.19.0 + 2.21.0 jar test @@ -298,17 +319,7 @@ - - - ossrh - https://s01.oss.sonatype.org/content/repositories/snapshots/ - - - ossrh - https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ - - - + gpg @@ -353,7 +364,7 @@ org.jacoco jacoco-maven-plugin - 0.8.13 + 0.8.14 prepare-agent @@ -437,7 +448,7 @@ org.codehaus.mojo exec-maven-plugin - 3.5.0 + 3.5.1 false diff --git a/src/main/java/pl/project13/core/GitCommitIdPlugin.java b/src/main/java/pl/project13/core/GitCommitIdPlugin.java index faccfb7..83fd130 100644 --- a/src/main/java/pl/project13/core/GitCommitIdPlugin.java +++ b/src/main/java/pl/project13/core/GitCommitIdPlugin.java @@ -27,6 +27,7 @@ import javax.annotation.Nullable; import java.io.File; import java.nio.charset.Charset; +import java.nio.file.Path; import java.text.SimpleDateFormat; import java.util.*; import java.util.function.Supplier; @@ -282,6 +283,16 @@ default Map getSystemEnv() { boolean shouldPropertiesEscapeUnicode(); boolean shouldFailOnNoGitDirectory(); + + /** + * When set to {@code true}, the plugin will only consider commits affecting + * paths within the current project's base directory. + * + *

This is useful for multi-module projects stored in a single git repository where you want + + properties like {@code git.commit.id.*} and {@code git.total.commit.count} to be based on the + + latest commit that touched this module, not the whole repository. + */ + boolean isPerModuleVersions(); } protected static final Pattern allowedCharactersForEvaluateOnCommit = Pattern.compile("[a-zA-Z0-9\\_\\-\\^\\/\\.]+"); @@ -368,17 +379,9 @@ private static void loadGitDataWithNativeGit( @Nonnull File dotGitDirectory, @Nonnull Properties properties) throws GitCommitIdExecutionException { GitDataProvider nativeGitProvider = NativeGitProvider - .on(dotGitDirectory, cb.getNativeGitTimeoutInMs(), cb.getLogInterface()) - .setPrefixDot(cb.getPrefixDot()) - .setAbbrevLength(cb.getAbbrevLength()) - .setDateFormat(cb.getDateFormat()) - .setDateFormatTimeZone(cb.getDateFormatTimeZone()) - .setGitDescribe(cb.getGitDescribe()) - .setCommitIdGenerationMode(cb.getCommitIdGenerationMode()) - .setUseBranchNameFromBuildEnvironment(cb.getUseBranchNameFromBuildEnvironment()) - .setExcludeProperties(cb.getExcludeProperties()) - .setIncludeOnlyProperties(cb.getIncludeOnlyProperties()) - .setOffline(cb.isOffline()); + .on(dotGitDirectory, cb.getNativeGitTimeoutInMs(), cb.getLogInterface()); + + configureCommonProvider(nativeGitProvider, cb, dotGitDirectory); nativeGitProvider.loadGitData(cb.getEvaluateOnCommit(), cb.getSystemEnv(), properties); } @@ -388,7 +391,18 @@ private static void loadGitDataWithJGit( @Nonnull File dotGitDirectory, @Nonnull Properties properties) throws GitCommitIdExecutionException { GitDataProvider jGitProvider = JGitProvider - .on(dotGitDirectory, cb.getLogInterface()) + .on(dotGitDirectory, cb.getLogInterface()); + + configureCommonProvider(jGitProvider, cb, dotGitDirectory); + + jGitProvider.loadGitData(cb.getEvaluateOnCommit(), cb.getSystemEnv(), properties); + } + + private static void configureCommonProvider( + @Nonnull GitDataProvider provider, + @Nonnull Callback cb, + @Nonnull File dotGitDirectory) { + provider .setPrefixDot(cb.getPrefixDot()) .setAbbrevLength(cb.getAbbrevLength()) .setDateFormat(cb.getDateFormat()) @@ -398,8 +412,57 @@ private static void loadGitDataWithJGit( .setUseBranchNameFromBuildEnvironment(cb.getUseBranchNameFromBuildEnvironment()) .setExcludeProperties(cb.getExcludeProperties()) .setIncludeOnlyProperties(cb.getIncludeOnlyProperties()) - .setOffline(cb.isOffline()); + .setOffline(cb.isOffline()) + .setPathFilter( + cb.isPerModuleVersions() + ? resolveRelativeModulePath(cb, dotGitDirectory) + : null); + } - jGitProvider.loadGitData(cb.getEvaluateOnCommit(), cb.getSystemEnv(), properties); + /** + * Resolves the relative module path from repository root to project base directory. + * This is used for per-module filtering to limit git operations to the current project. + * Returns null if the project directory is not within the repository or if resolution fails. + */ + @Nullable + private static String resolveRelativeModulePath(@Nonnull Callback cb, @Nonnull File dotGitDirectory) { + try { + // Determine repository work tree + // If dotGitDirectory is named ".git", the work tree is its parent + // Otherwise, dotGitDirectory might be the work tree itself (for bare repos or special cases) + File repoWorkTree; + if (dotGitDirectory.getName().equals(".git")) { + repoWorkTree = dotGitDirectory.getParentFile(); + } else { + repoWorkTree = dotGitDirectory; + } + + if (repoWorkTree == null || !repoWorkTree.exists()) { + cb.getLogInterface().warn("Could not determine repository work tree from dotGitDirectory: " + dotGitDirectory); + return null; + } + + Path repoRoot = repoWorkTree.toPath().toAbsolutePath().normalize(); + Path moduleDir = cb.getProjectBaseDir().toPath().toAbsolutePath().normalize(); + + if (!moduleDir.startsWith(repoRoot)) { + cb.getLogInterface() + .warn("Project directory is not within repository work tree. " + + "Project: " + moduleDir + ", Repo: " + repoRoot + ". " + + "Falling back to normal behavior."); + return null; + } + + String relative = repoRoot.relativize(moduleDir).toString(); + if (relative.isEmpty()) { + relative = "."; + } + return relative.replace(File.separatorChar, '/'); + } catch (Exception e) { + cb.getLogInterface() + .warn("Failed to resolve relative module path: " + e.getMessage() + + ". Falling back to normal behavior."); + return null; + } } } diff --git a/src/main/java/pl/project13/core/GitDataProvider.java b/src/main/java/pl/project13/core/GitDataProvider.java index 65b1dd7..237b8c9 100644 --- a/src/main/java/pl/project13/core/GitDataProvider.java +++ b/src/main/java/pl/project13/core/GitDataProvider.java @@ -116,6 +116,12 @@ public abstract class GitDataProvider implements GitProvider { */ protected boolean offline; + /** + * Optional path filter to limit certain computations (e.g. total commit count) to commits + * affecting a specific path. + */ + protected String pathFilter; + /** * Constructor to encapsulates all references required to dertermine all git-data. * @param log logging provider which will be used to log events @@ -242,6 +248,17 @@ public GitDataProvider setOffline(boolean offline) { return this; } + /** + * Sets an optional path filter. + * + * @param pathFilter path relative to repository root using forward slashes (git-style) + * @return The {@code GitProvider} with the corresponding path filter set. + */ + public GitDataProvider setPathFilter(String pathFilter) { + this.pathFilter = pathFilter; + return this; + } + /** * Main function that will attempt to load the desired properties from the git repository. * diff --git a/src/main/java/pl/project13/core/JGitProvider.java b/src/main/java/pl/project13/core/JGitProvider.java index c018603..22bbfe8 100644 --- a/src/main/java/pl/project13/core/JGitProvider.java +++ b/src/main/java/pl/project13/core/JGitProvider.java @@ -41,6 +41,7 @@ import org.eclipse.jgit.storage.file.WindowCacheConfig; import javax.annotation.Nonnull; +import javax.annotation.Nullable; public class JGitProvider extends GitDataProvider { @@ -84,6 +85,15 @@ public String getBuildAuthorEmail() throws GitCommitIdExecutionException { @Override public void prepareGitToExtractMoreDetailedRepoInformation() throws GitCommitIdExecutionException { try { + // For per-module versions, we need to find the latest commit that touched the module path + if (pathFilter != null && !pathFilter.isEmpty()) { + String latestCommitForModule = findLatestCommitForPath(pathFilter); + if (latestCommitForModule != null && !latestCommitForModule.isEmpty()) { + // Override evaluateOnCommit with the latest commit that touched this module + this.evaluateOnCommit = latestCommitForModule; + } + } + // more details parsed out bellow Ref evaluateOnCommitReference = git.findRef(evaluateOnCommit); ObjectId evaluateOnCommitResolvedObjectId = git.resolve(evaluateOnCommit); @@ -115,6 +125,36 @@ public void prepareGitToExtractMoreDetailedRepoInformation() throws GitCommitIdE } } + /** + * Finds the latest commit that touched the specified path. + * Returns the commit hash or null if no commits found. + */ + @Nullable + private String findLatestCommitForPath(@Nonnull String path) throws GitCommitIdExecutionException { + try { + ObjectId start = git.resolve(evaluateOnCommit); + if (start == null) { + return null; + } + + try (Git git = Git.wrap(this.git)) { + Iterable commits = git.log() + .add(start) + .addPath(path) + .setMaxCount(1) + .call(); + + Iterator it = commits.iterator(); + if (it.hasNext()) { + return it.next().getName(); + } + } + return null; + } catch (Exception e) { + throw new GitCommitIdExecutionException("Failed to find latest commit for path: " + path, e); + } + } + @Override public String getBranchName() throws GitCommitIdExecutionException { try { @@ -173,7 +213,17 @@ private boolean evalCommitIsNotHead() { @Override public String getGitDescribe() throws GitCommitIdExecutionException { - return getGitDescribe(git); + try { + Repository repo = getGitRepository(); + DescribeResult describeResult = DescribeCommand + .on(evaluateOnCommit, repo, log, pathFilter) + .apply(super.gitDescribe) + .call(); + + return describeResult.toString(); + } catch (GitAPIException ex) { + throw new GitCommitIdExecutionException("Unable to obtain git.commit.id.describe information", ex); + } } @Override @@ -189,7 +239,7 @@ public String getAbbrevCommitId() throws GitCommitIdExecutionException { @Override public boolean isDirty() throws GitCommitIdExecutionException { try { - return JGitCommon.isRepositoryInDirtyState(git); + return JGitCommon.isRepositoryInDirtyState(git, pathFilter); } catch (GitAPIException e) { throw new GitCommitIdExecutionException("Failed to get git status: " + e.getMessage(), e); } @@ -271,6 +321,20 @@ public String getTag() throws GitCommitIdExecutionException { @Override public String getClosestTagName() throws GitCommitIdExecutionException { + if (pathFilter != null) { + try { + // When path filter is present, find the latest commit for that path first + String latestCommitForPath = findLatestCommitForPath(pathFilter); + if (latestCommitForPath != null && !latestCommitForPath.isEmpty()) { + // Use jGitCommon.getClosestTagName on the latest commit that touched this path + Repository repo = getGitRepository(); + return jGitCommon.getClosestTagName(latestCommitForPath, repo, gitDescribe); + } + } catch (Exception e) { + log.warn("Failed to find tags for path: " + pathFilter + ", falling back to normal behavior"); + } + } + // Fallback to normal behavior Repository repo = getGitRepository(); try { return jGitCommon.getClosestTagName(evaluateOnCommit, repo, gitDescribe); @@ -282,6 +346,19 @@ public String getClosestTagName() throws GitCommitIdExecutionException { @Override public String getClosestTagCommitCount() throws GitCommitIdExecutionException { + if (pathFilter != null) { + try { + // When path filter is present, we need to find the latest commit for that path first + String latestCommitForPath = findLatestCommitForPath(pathFilter); + if (latestCommitForPath != null && !latestCommitForPath.isEmpty()) { + Repository repo = getGitRepository(); + return jGitCommon.getClosestTagCommitCount(latestCommitForPath, repo, gitDescribe); + } + } catch (Exception e) { + log.warn("Failed to find latest commit for path: " + pathFilter + ", falling back to normal behavior"); + } + } + // Fallback to normal behavior Repository repo = getGitRepository(); try { return jGitCommon.getClosestTagCommitCount(evaluateOnCommit, repo, gitDescribe); @@ -294,6 +371,13 @@ public String getClosestTagCommitCount() throws GitCommitIdExecutionException { @Override public String getTotalCommitCount() throws GitCommitIdExecutionException { try { + if (pathFilter != null && !pathFilter.isEmpty()) { + long count = 0; + for (RevCommit ignored : Git.wrap(git).log().add(evalCommit).addPath(pathFilter).call()) { + count++; + } + return String.valueOf(count); + } return String.valueOf(RevWalkUtils.count(revWalk, evalCommit, null)); } catch (Throwable t) { // could not find any tags to describe @@ -318,21 +402,6 @@ public void finalCleanUp() { } } - // Visible for testing - String getGitDescribe(@Nonnull Repository repository) throws GitCommitIdExecutionException { - try { - DescribeResult describeResult = DescribeCommand - .on(evaluateOnCommit, repository, log) - .apply(super.gitDescribe) - .call(); - - return describeResult.toString(); - } catch (GitAPIException ex) { - ex.printStackTrace(); - throw new GitCommitIdExecutionException("Unable to obtain git.commit.id.describe information", ex); - } - } - private String getAbbrevCommitId(ObjectReader objectReader, RevCommit headCommit, int abbrevLength) throws GitCommitIdExecutionException { try { AbbreviatedObjectId abbreviatedObjectId = objectReader.abbreviate(headCommit, abbrevLength); diff --git a/src/main/java/pl/project13/core/NativeGitProvider.java b/src/main/java/pl/project13/core/NativeGitProvider.java index 6f6616d..8cda37e 100644 --- a/src/main/java/pl/project13/core/NativeGitProvider.java +++ b/src/main/java/pl/project13/core/NativeGitProvider.java @@ -23,6 +23,7 @@ import pl.project13.core.log.LogInterface; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.io.*; import java.text.SimpleDateFormat; @@ -130,7 +131,9 @@ private String getBranchForHead(File canonical) throws GitCommitIdExecutionExcep } private String getBranchForCommitish(File canonical) throws GitCommitIdExecutionException { - String branch = runQuietGitCommand(canonical, nativeGitTimeoutInMs, "branch --points-at " + evaluateOnCommit); + String branch = runQuietGitCommand( + canonical, nativeGitTimeoutInMs, + "branch --points-at " + evaluateOnCommit); if (branch != null && !branch.isEmpty()) { // multiple branches could point to the same commit - return them all... branch = Stream.of(branch.split("\n")) @@ -147,8 +150,22 @@ private String getBranchForCommitish(File canonical) throws GitCommitIdExecution @Override public String getGitDescribe() throws GitCommitIdExecutionException { + if (pathFilter != null) { + // When path filter is present, we need to find the latest commit for that path first + String latestCommitForPath = findLatestCommitForPath(pathFilter); + if (latestCommitForPath != null && !latestCommitForPath.isEmpty()) { + // Use the latest commit that touched this path + final String argumentsForGitDescribe = getArgumentsForGitDescribe(gitDescribe); + return runQuietGitCommand( + canonical, nativeGitTimeoutInMs, + "describe" + argumentsForGitDescribe + " " + latestCommitForPath); + } + } + // Fallback to normal behavior final String argumentsForGitDescribe = getArgumentsForGitDescribe(gitDescribe); - return runQuietGitCommand(canonical, nativeGitTimeoutInMs, "describe" + argumentsForGitDescribe); + return runQuietGitCommand( + canonical, nativeGitTimeoutInMs, + "describe" + argumentsForGitDescribe); } private String getArgumentsForGitDescribe(GitDescribeConfig describeConfig) { @@ -158,6 +175,8 @@ private String getArgumentsForGitDescribe(GitDescribeConfig describeConfig) { StringBuilder argumentsForGitDescribe = new StringBuilder(); boolean hasCommitish = evalCommitIsNotHead(); + boolean hasPathFilter = pathFilter != null; + if (hasCommitish) { argumentsForGitDescribe.append(" " + evaluateOnCommit); } @@ -168,9 +187,10 @@ private String getArgumentsForGitDescribe(GitDescribeConfig describeConfig) { final String dirtyMark = describeConfig.getDirty(); if ((dirtyMark != null) && !dirtyMark.isEmpty()) { - // we can either have evaluateOnCommit or --dirty flag set - if (hasCommitish) { - log.warn("You might use strange arguments since it's unfortunately not supported to have evaluateOnCommit and the --dirty flag for the describe command set at the same time"); + // we can either have evaluateOnCommit or --dirty flag set, but not both + // also, we can't have path filter and --dirty flag set at the same time + if (hasCommitish || hasPathFilter) { + log.warn("You might use strange arguments since it's unfortunately not supported to have evaluateOnCommit/path filter and the --dirty flag for the describe command set at the same time"); } else { argumentsForGitDescribe.append(" --dirty=").append(dirtyMark); } @@ -195,16 +215,50 @@ private String getArgumentsForGitDescribe(GitDescribeConfig describeConfig) { @Override public String getCommitId() throws GitCommitIdExecutionException { + // For per-module versions, find the latest commit that touched the module path + if (pathFilter != null && !pathFilter.isEmpty()) { + String latestCommitForModule = findLatestCommitForPath(pathFilter); + if (latestCommitForModule != null && !latestCommitForModule.isEmpty()) { + // Use the latest commit that touched this module + return runQuietGitCommand( + canonical, nativeGitTimeoutInMs, + "rev-parse " + latestCommitForModule); + } + } + + // Fallback to normal logic boolean evaluateOnCommitIsSet = evalCommitIsNotHead(); if (evaluateOnCommitIsSet) { // if evaluateOnCommit represents a tag we need to perform the rev-parse on the actual commit reference // in case evaluateOnCommit is not a reference rev-list will just return the argument given // and thus it's always safe(r) to unwrap it // however when evaluateOnCommit is not set we don't want to waste calls to the native binary - String actualCommitId = runQuietGitCommand(canonical, nativeGitTimeoutInMs, "rev-list -n 1 " + evaluateOnCommit); - return runQuietGitCommand(canonical, nativeGitTimeoutInMs, "rev-parse " + actualCommitId); + String actualCommitId = runQuietGitCommand( + canonical, nativeGitTimeoutInMs, + "rev-list -n 1 " + evaluateOnCommit); + return runQuietGitCommand( + canonical, nativeGitTimeoutInMs, + "rev-parse " + actualCommitId); } else { - return runQuietGitCommand(canonical, nativeGitTimeoutInMs, "rev-parse HEAD"); + return runQuietGitCommand( + canonical, nativeGitTimeoutInMs, + "rev-parse HEAD"); + } + } + + /** + * Finds the latest commit that touched the specified path using native git. + * Returns the commit hash or null if no commits found. + */ + @Nullable + private String findLatestCommitForPath(@Nonnull String path) throws GitCommitIdExecutionException { + try { + return runQuietGitCommand( + canonical, nativeGitTimeoutInMs, + "log -n 1 --format=%H --no-show-signature " + evaluateOnCommit + " -- " + path); + } catch (GitCommitIdExecutionException e) { + log.warn("Failed to find latest commit for path: " + path + ", falling back to normal behavior"); + return null; } } @@ -224,59 +278,89 @@ public String getAbbrevCommitId() throws GitCommitIdExecutionException { @Override public boolean isDirty() throws GitCommitIdExecutionException { - return !tryCheckEmptyRunGitCommand(canonical, nativeGitTimeoutInMs, "status -s"); + String pathSpec = pathFilter != null ? " -- " + pathFilter : ""; + return !tryCheckEmptyRunGitCommand( + canonical, nativeGitTimeoutInMs, + "status -s" + pathSpec); } @Override public String getCommitAuthorName() throws GitCommitIdExecutionException { - return runQuietGitCommand(canonical, nativeGitTimeoutInMs, "log -1 --pretty=format:%an --no-show-signature " + evaluateOnCommit); + String pathSpec = pathFilter != null ? " -- " + pathFilter : ""; + return runQuietGitCommand( + canonical, nativeGitTimeoutInMs, + "log -1 --pretty=format:%an --no-show-signature " + evaluateOnCommit + pathSpec); } @Override public String getCommitAuthorEmail() throws GitCommitIdExecutionException { - return runQuietGitCommand(canonical, nativeGitTimeoutInMs, "log -1 --pretty=format:%ae --no-show-signature " + evaluateOnCommit); + String pathSpec = pathFilter != null ? " -- " + pathFilter : ""; + return runQuietGitCommand( + canonical, nativeGitTimeoutInMs, + "log -1 --pretty=format:%ae --no-show-signature " + evaluateOnCommit + pathSpec); } @Override public String getCommitMessageFull() throws GitCommitIdExecutionException { - return runQuietGitCommand(canonical, nativeGitTimeoutInMs, "log -1 --pretty=format:%B --no-show-signature " + evaluateOnCommit); + String pathSpec = pathFilter != null ? " -- " + pathFilter : ""; + return runQuietGitCommand( + canonical, nativeGitTimeoutInMs, + "log -1 --pretty=format:%B --no-show-signature " + evaluateOnCommit + pathSpec); } @Override public String getCommitMessageShort() throws GitCommitIdExecutionException { - return runQuietGitCommand(canonical, nativeGitTimeoutInMs, "log -1 --pretty=format:%s --no-show-signature " + evaluateOnCommit); + String pathSpec = pathFilter != null ? " -- " + pathFilter : ""; + return runQuietGitCommand( + canonical, nativeGitTimeoutInMs, + "log -1 --pretty=format:%s --no-show-signature " + evaluateOnCommit + pathSpec); } @Override public String getCommitTime() throws GitCommitIdExecutionException { - String value = runQuietGitCommand(canonical, nativeGitTimeoutInMs, "log -1 --pretty=format:%ct --no-show-signature " + evaluateOnCommit); + String pathSpec = pathFilter != null ? " -- " + pathFilter : ""; + String value = runQuietGitCommand( + canonical, nativeGitTimeoutInMs, + "log -1 --pretty=format:%ct --no-show-signature " + evaluateOnCommit + pathSpec); SimpleDateFormat smf = getSimpleDateFormatWithTimeZone(); return smf.format(Long.parseLong(value) * 1000L); } @Override public String getCommitAuthorTime() throws GitCommitIdExecutionException { - String value = runQuietGitCommand(canonical, nativeGitTimeoutInMs, "log -1 --pretty=format:%at --no-show-signature " + evaluateOnCommit); + String pathSpec = pathFilter != null ? " -- " + pathFilter : ""; + String value = runQuietGitCommand( + canonical, nativeGitTimeoutInMs, + "log -1 --pretty=format:%at --no-show-signature " + evaluateOnCommit + pathSpec); SimpleDateFormat smf = getSimpleDateFormatWithTimeZone(); return smf.format(Long.parseLong(value) * 1000L); } @Override public String getCommitCommitterTime() throws GitCommitIdExecutionException { - String value = runQuietGitCommand(canonical, nativeGitTimeoutInMs, "log -1 --pretty=format:%ct --no-show-signature " + evaluateOnCommit); + String pathSpec = pathFilter != null ? " -- " + pathFilter : ""; + String value = runQuietGitCommand( + canonical, nativeGitTimeoutInMs, + "log -1 --pretty=format:%ct --no-show-signature " + evaluateOnCommit + pathSpec); SimpleDateFormat smf = getSimpleDateFormatWithTimeZone(); return smf.format(Long.parseLong(value) * 1000L); } @Override public String getTags() throws GitCommitIdExecutionException { - final String result = runQuietGitCommand(canonical, nativeGitTimeoutInMs, "tag --contains " + evaluateOnCommit); + String pathSpec = pathFilter != null ? " -- " + pathFilter : ""; + final String result = runQuietGitCommand( + canonical, nativeGitTimeoutInMs, + "tag --contains " + evaluateOnCommit + pathSpec); return result.replace('\n', ','); } @Override public String getTag() throws GitCommitIdExecutionException { - final String result = runQuietGitCommand(canonical, nativeGitTimeoutInMs, "tag --points-at " + evaluateOnCommit); + String pathSpec = pathFilter != null ? " -- " + pathFilter : ""; + final String result = runQuietGitCommand( + canonical, nativeGitTimeoutInMs, + "tag --points-at " + evaluateOnCommit + pathSpec); return result.replace('\n', ','); } @@ -287,6 +371,31 @@ public String getRemoteOriginUrl() throws GitCommitIdExecutionException { @Override public String getClosestTagName() throws GitCommitIdExecutionException { + if (pathFilter != null) { + try { + // When path filter is present, find the latest commit for that path first + String latestCommitForPath = findLatestCommitForPath(pathFilter); + if (latestCommitForPath != null && !latestCommitForPath.isEmpty()) { + // Use git describe on the latest commit that touched this path + StringBuilder argumentsForGitDescribe = new StringBuilder(); + argumentsForGitDescribe.append("describe " + latestCommitForPath + " --abbrev=0"); + if (gitDescribe != null) { + if (gitDescribe.getTags()) { + argumentsForGitDescribe.append(" --tags"); + } + + final String matchOption = gitDescribe.getMatch(); + if (matchOption != null && !matchOption.isEmpty()) { + argumentsForGitDescribe.append(" --match=").append(matchOption); + } + } + return runGitCommand(canonical, nativeGitTimeoutInMs, argumentsForGitDescribe.toString()); + } + } catch (Exception e) { + log.warn("Failed to find tags for path: " + pathFilter + ", falling back to normal behavior"); + } + } + // Fallback to normal behavior try { StringBuilder argumentsForGitDescribe = new StringBuilder(); argumentsForGitDescribe.append("describe " + evaluateOnCommit + " --abbrev=0"); @@ -311,14 +420,20 @@ public String getClosestTagName() throws GitCommitIdExecutionException { public String getClosestTagCommitCount() throws GitCommitIdExecutionException { String closestTagName = getClosestTagName(); if (closestTagName != null && !closestTagName.trim().isEmpty()) { - return runQuietGitCommand(canonical, nativeGitTimeoutInMs, "rev-list " + closestTagName + ".." + evaluateOnCommit + " --count"); + String pathSpec = pathFilter != null ? " -- " + pathFilter : ""; + return runQuietGitCommand( + canonical, nativeGitTimeoutInMs, + "rev-list " + closestTagName + ".." + evaluateOnCommit + " --count" + pathSpec); } return ""; } @Override public String getTotalCommitCount() throws GitCommitIdExecutionException { - return runQuietGitCommand(canonical, nativeGitTimeoutInMs, "rev-list " + evaluateOnCommit + " --count"); + String pathSpec = pathFilter != null ? " -- " + pathFilter : ""; + return runQuietGitCommand( + canonical, nativeGitTimeoutInMs, + "rev-list " + evaluateOnCommit + " --count" + pathSpec); } @Override @@ -532,8 +647,12 @@ public AheadBehind getAheadBehind() throws GitCommitIdExecutionException { fetch(remoteBranch.get()); } String localBranchName = getBranchName(); - String ahead = runQuietGitCommand(canonical, nativeGitTimeoutInMs, "rev-list --right-only --count " + remoteBranch.get() + "..." + localBranchName); - String behind = runQuietGitCommand(canonical, nativeGitTimeoutInMs, "rev-list --left-only --count " + remoteBranch.get() + "..." + localBranchName); + String ahead = runQuietGitCommand( + canonical, nativeGitTimeoutInMs, + "rev-list --right-only --count " + remoteBranch.get() + "..." + localBranchName); + String behind = runQuietGitCommand( + canonical, nativeGitTimeoutInMs, + "rev-list --left-only --count " + remoteBranch.get() + "..." + localBranchName); return AheadBehind.of(ahead, behind); } catch (Exception e) { throw new GitCommitIdExecutionException("Failed to read ahead behind count: " + e.getMessage(), e); @@ -542,12 +661,16 @@ public AheadBehind getAheadBehind() throws GitCommitIdExecutionException { private Optional remoteBranch() { try { - String remoteRef = runQuietGitCommand(canonical, nativeGitTimeoutInMs, "symbolic-ref -q " + evaluateOnCommit); + String remoteRef = runQuietGitCommand( + canonical, nativeGitTimeoutInMs, + "symbolic-ref -q " + evaluateOnCommit); if (remoteRef == null || remoteRef.isEmpty()) { log.debug("Could not find ref for: " + evaluateOnCommit); return Optional.empty(); } - String remoteBranch = runQuietGitCommand(canonical, nativeGitTimeoutInMs, "for-each-ref --format=%(upstream:short) " + remoteRef); + String remoteBranch = runQuietGitCommand( + canonical, nativeGitTimeoutInMs, + "for-each-ref --format=%(upstream:short) " + remoteRef); return Optional.ofNullable(remoteBranch.isEmpty() ? null : remoteBranch); } catch (Exception e) { return Optional.empty(); @@ -556,7 +679,9 @@ private Optional remoteBranch() { private void fetch(String remoteBranch) { try { - runQuietGitCommand(canonical, nativeGitTimeoutInMs, "fetch " + remoteBranch.replaceFirst("/", " ")); + runQuietGitCommand( + canonical, nativeGitTimeoutInMs, + "fetch " + remoteBranch.replaceFirst("/", " ")); } catch (Exception e) { log.error("Failed to execute fetch", e); } diff --git a/src/main/java/pl/project13/core/jgit/DescribeCommand.java b/src/main/java/pl/project13/core/jgit/DescribeCommand.java index ee72716..798c214 100644 --- a/src/main/java/pl/project13/core/jgit/DescribeCommand.java +++ b/src/main/java/pl/project13/core/jgit/DescribeCommand.java @@ -17,6 +17,7 @@ package pl.project13.core.jgit; +import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.GitCommand; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.ObjectId; @@ -30,6 +31,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.io.IOException; import java.util.*; /** @@ -40,6 +42,7 @@ public class DescribeCommand extends GitCommand { private LogInterface log; private JGitCommon jGitCommon; private String evaluateOnCommit; + private String pathFilter; // TODO not yet implemented options: // private boolean containsFlag = false; @@ -83,7 +86,12 @@ public class DescribeCommand extends GitCommand { */ @Nonnull public static DescribeCommand on(String evaluateOnCommit, Repository repo, LogInterface log) { - return new DescribeCommand(evaluateOnCommit, repo, log); + return new DescribeCommand(evaluateOnCommit, repo, log, null); + } + + @Nonnull + public static DescribeCommand on(String evaluateOnCommit, Repository repo, LogInterface log, String pathFilter) { + return new DescribeCommand(evaluateOnCommit, repo, log, pathFilter); } /** @@ -94,11 +102,12 @@ public static DescribeCommand on(String evaluateOnCommit, Repository repo, LogIn * @param log logger bridge to direct logs to * @return itself with the options set as specified by the arguments to allow fluent configuration */ - private DescribeCommand(@Nonnull String evaluateOnCommit, @Nonnull Repository repo, @Nonnull LogInterface log) { + private DescribeCommand(@Nonnull String evaluateOnCommit, @Nonnull Repository repo, @Nonnull LogInterface log, String pathFilter) { super(repo); this.evaluateOnCommit = evaluateOnCommit; this.jGitCommon = new JGitCommon(log); this.log = log; + this.pathFilter = pathFilter; } /** @@ -272,12 +281,26 @@ public DescribeResult call() throws GitAPIException { // needed for abbrev id's calculation ObjectReader objectReader = repo.newObjectReader(); + // Handle path filter - find latest commit for the path if needed + String actualEvaluateOnCommit = evaluateOnCommit; + if (pathFilter != null && !pathFilter.isEmpty()) { + try { + String latestCommitForPath = findLatestCommitForPath(pathFilter); + if (latestCommitForPath != null && !latestCommitForPath.isEmpty()) { + actualEvaluateOnCommit = latestCommitForPath; + log.info("Using latest commit for path '" + pathFilter + "': " + latestCommitForPath); + } + } catch (Exception e) { + log.warn("Failed to find latest commit for path: " + pathFilter + ", falling back to normal behavior"); + } + } + // get tags String matchPattern = createMatchPattern(); Map> tagObjectIdToName = jGitCommon.findTagObjectIds(repo, tagsFlag, matchPattern); // get current commit - RevCommit evalCommit = findEvalCommitObjectId(evaluateOnCommit, repo); + RevCommit evalCommit = findEvalCommitObjectId(actualEvaluateOnCommit, repo); ObjectId evalCommitId = evalCommit.getId(); // check if dirty @@ -360,7 +383,7 @@ private static boolean foundZeroTags(@Nonnull Map> tags) // Visible for testing boolean findDirtyState(Repository repo) throws GitAPIException { - return JGitCommon.isRepositoryInDirtyState(repo); + return JGitCommon.isRepositoryInDirtyState(repo, pathFilter); } // Visible for testing @@ -372,6 +395,32 @@ RevCommit findEvalCommitObjectId(@Nonnull String evaluateOnCommit, @Nonnull Repo return jGitCommon.findEvalCommitObjectId(evaluateOnCommit, repo); } + /** + * Finds the latest commit that touched the specified path. + * Returns the commit hash or null if no commits found. + */ + @Nullable + private String findLatestCommitForPath(@Nonnull String path) throws GitAPIException, IOException { + ObjectId start = repo.resolve(evaluateOnCommit); + if (start == null) { + return null; + } + + try (Git git = Git.wrap(repo)) { + Iterable commits = git.log() + .add(start) + .addPath(path) + .setMaxCount(1) + .call(); + + Iterator it = commits.iterator(); + if (it.hasNext()) { + return it.next().getName(); + } + } + return null; + } + private String createMatchPattern() { if (!matchOption.isPresent()) { return ".*"; diff --git a/src/main/java/pl/project13/core/jgit/JGitCommon.java b/src/main/java/pl/project13/core/jgit/JGitCommon.java index b429005..95ae54c 100644 --- a/src/main/java/pl/project13/core/jgit/JGitCommon.java +++ b/src/main/java/pl/project13/core/jgit/JGitCommon.java @@ -357,19 +357,26 @@ private void seeAllParents(@Nonnull RevWalk revWalk, RevCommit child, @Nonnull S } } - public static boolean isRepositoryInDirtyState(Repository repo) throws GitAPIException { - Git git = Git.wrap(repo); - Status status = git.status().call(); - - // Git describe doesn't mind about untracked files when checking if - // repo is dirty. JGit does this, so we cannot use the isClean method - // to get the same behaviour. Instead check dirty state without - // status.getUntracked().isEmpty() - return !(status.getAdded().isEmpty() - && status.getChanged().isEmpty() - && status.getRemoved().isEmpty() - && status.getMissing().isEmpty() - && status.getModified().isEmpty() - && status.getConflicting().isEmpty()); + public static boolean isRepositoryInDirtyState(Repository repo, String pathFilter) throws GitAPIException { + try (Git git = Git.wrap(repo)) { + Status status; + if (pathFilter != null && !pathFilter.isEmpty()) { + // When path filter is present, check dirty state only for that path + status = git.status().addPath(pathFilter).call(); + } else { + // Fallback to normal behavior - check entire repository + status = git.status().call(); + } + // Git describe doesn't mind about untracked files when checking if + // repo is dirty. JGit does this, so we cannot use the isClean method + // to get the same behaviour. Instead check dirty state without + // status.getUntracked().isEmpty() + return !(status.getAdded().isEmpty() + && status.getChanged().isEmpty() + && status.getRemoved().isEmpty() + && status.getMissing().isEmpty() + && status.getModified().isEmpty() + && status.getConflicting().isEmpty()); + } } } diff --git a/src/test/java/pl/project13/core/AvailableGitTestRepo.java b/src/test/java/pl/project13/core/AvailableGitTestRepo.java index 83e7697..3436253 100644 --- a/src/test/java/pl/project13/core/AvailableGitTestRepo.java +++ b/src/test/java/pl/project13/core/AvailableGitTestRepo.java @@ -116,6 +116,28 @@ public enum AvailableGitTestRepo { * */ WITH_THREE_COMMITS_AND_TWO_TAGS_CURRENTLY_ON_COMMIT_WITHOUT_TAG("src/test/resources/_git_three_commits_and_two_tags_currently_on_commit_without_tag"), + /** + *

+   * $  git log --name-only --pretty=format:"%H '%an' '%aD' '%s' %d" --date=short
+   * 2ed2ea209fb99c360cd8434eb2d82b929da6b908 'TheSnoozer' 'Fri, 27 Mar 2026 17:40:36 +0100' 'a change in the root pom'  (HEAD -> master)
+   * pom.xml
+   *
+   * 70a13b95591dac76ce92dd9087d557fca539f98a 'submodule-two Author' 'Fri, 27 Mar 2026 17:39:59 +0100' 'a change in submodule-two'  (tag: tag-submodule-two)
+   * submodule-two/pom.xml
+   *
+   * 91e49245092c089624d3e770d902cfc8bc53a852 'submodule-one Author' 'Fri, 27 Mar 2026 17:39:23 +0100' 'a change in submodule-one'  (tag: tag-submodule-one)
+   * submodule-one/pom.xml
+   *
+   * 9c5d2e13d042b0acb71c48232a9c408e42da87f7 'TheSnoozer' 'Fri, 27 Mar 2026 17:38:16 +0100' 'new repo for testing (based on git-commit-id-maven-debugging)'
+   * [snip]
+   * 
+ * and dirty: + *
+   * $ git status -s
+   * M submodule-two/pom.xml
+   * 
+ */ + WITH_SUBMODULES_AND_MULTIPLE_COMMITS("src/test/resources/_git_with_submodules_and_multiple_commits"), // TODO: Why do the tests get stuck when we use .git?? MAVEN_GIT_COMMIT_ID_PLUGIN("src/test/resources/_git_one_commit_with_umlaut") ; diff --git a/src/test/java/pl/project13/core/GitCommitIdPluginIntegrationTest.java b/src/test/java/pl/project13/core/GitCommitIdPluginIntegrationTest.java index 83dead2..9de1b71 100644 --- a/src/test/java/pl/project13/core/GitCommitIdPluginIntegrationTest.java +++ b/src/test/java/pl/project13/core/GitCommitIdPluginIntegrationTest.java @@ -28,6 +28,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import pl.project13.core.git.GitDescribeConfig; import pl.project13.core.util.GenericFileManager; @@ -43,17 +44,16 @@ import java.util.*; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; -import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; public class GitCommitIdPluginIntegrationTest { - public static Collection useNativeGit() { - return asList(true, false); - } - - public static Collection useDirty() { - return asList(true, false); + public static Stream useNativeGit() { + return Stream.of( + Arguments.of(true), + Arguments.of(false) + ); } private Path sandbox; @@ -1743,6 +1743,86 @@ public void verifyAllowedCharactersForEvaluateOnCommit() { Assertions.assertFalse(p.matcher("&&cat /etc/passwd").matches()); } + static Stream useNativeGitWithSubmoduleName() { + return useNativeGit().flatMap(arg -> + Stream.of( + "submodule-one", + "submodule-two" + ).map(str -> + Arguments.of(arg.get()[0], str) + ) + ); + } + + @ParameterizedTest + @MethodSource("useNativeGitWithSubmoduleName") + public void shouldGiveCommitIdForEachFolderWhenPerModuleVersionsEnabled(boolean useNativeGit, String submoduleName) throws Exception { + // given + File dotGitDirectory = createTmpDotGitDirectory(AvailableGitTestRepo.WITH_SUBMODULES_AND_MULTIPLE_COMMITS); + + GitCommitIdPlugin.Callback cb = + new GitCommitIdTestCallback() + .setDotGitDirectory(dotGitDirectory) + .setUseNativeGit(useNativeGit) + .setPerModuleVersions(true) + .setDateFormatTimeZone("CET") + .setProjectBaseDir(dotGitDirectory.getParentFile().toPath().resolve(submoduleName).toFile()) + .build(); + Properties properties = new Properties(); + + // when + GitCommitIdPlugin.runPlugin(cb, properties); + + // then + assertGitPropertiesPresentInProject(properties); + + // setup expectations + Map expectedValues = new HashMap<>(); + expectedValues.put("git.commit.id", null); + expectedValues.put("git.closest.tag.name", "tag-" + submoduleName); + expectedValues.put("git.closest.tag.commit.count", null); + expectedValues.put("git.dirty", null); + expectedValues.put("git.commit.message.full", "a change in " + submoduleName); + expectedValues.put("git.commit.user.name", submoduleName + " Author"); + expectedValues.put("git.commit.user.email", submoduleName + "@example.com"); + expectedValues.put("git.commit.time", null); + expectedValues.put("git.commit.author.time", null); + expectedValues.put("git.commit.committer.time", null); + + if (submoduleName.equals("submodule-one")) { + expectedValues.put("git.commit.id", "91e49245092c089624d3e770d902cfc8bc53a852"); + expectedValues.put("git.closest.tag.commit.count", 0); + expectedValues.put("git.dirty", true); // Really? + // date -d @$(git log -1 --pretty=format:%ct 91e4924) "+%Y-%m-%dT%H:%M:%S%z" + expectedValues.put("git.commit.time", "2026-03-28T11:47:51+0100"); + // date -d @$(git log -1 --pretty=format:%at 91e4924) "+%Y-%m-%dT%H:%M:%S%z" + expectedValues.put("git.commit.author.time", "2026-03-27T17:39:23+0100"); + // date -d @$(git log -1 --pretty=format:%ct 91e4924) "+%Y-%m-%dT%H:%M:%S%z" + expectedValues.put("git.commit.committer.time", "2026-03-28T11:47:51+0100"); + } else if (submoduleName.equals("submodule-two")) { + expectedValues.put("git.commit.id", "70a13b95591dac76ce92dd9087d557fca539f98a"); + expectedValues.put("git.closest.tag.commit.count", 0); + expectedValues.put("git.dirty", true); + // date -d @$(git log -1 --pretty=format:%ct 70a13b9) "+%Y-%m-%dT%H:%M:%S%z" + expectedValues.put("git.commit.time", "2026-03-28T11:48:17+0100"); + // date -d @$(git log -1 --pretty=format:%at 70a13b9) "+%Y-%m-%dT%H:%M:%S%z" + expectedValues.put("git.commit.author.time", "2026-03-27T17:39:59+0100"); + // date -d @$(git log -1 --pretty=format:%ct 70a13b9) "+%Y-%m-%dT%H:%M:%S%z" + expectedValues.put("git.commit.committer.time", "2026-03-28T11:48:17+0100"); + } + + // Assertions + for (Map.Entry entry : expectedValues.entrySet()) { + String key = entry.getKey(); + Object expectedValue = entry.getValue(); + + assertThat(expectedValue).isNotNull(); + assertThat(properties.getProperty(key)) + .as("useNativeGit=%s,submoduleName=%s,key=%s", useNativeGit, submoduleName, key) + .isEqualTo(String.valueOf(expectedValue)); + } + } + private GitDescribeConfig createGitDescribeConfig(boolean forceLongFormat, int abbrev) { GitDescribeConfig gitDescribeConfig = new GitDescribeConfig(); gitDescribeConfig.setTags(true); diff --git a/src/test/java/pl/project13/core/GitCommitIdTestCallback.java b/src/test/java/pl/project13/core/GitCommitIdTestCallback.java index a637d12..b76ccbc 100644 --- a/src/test/java/pl/project13/core/GitCommitIdTestCallback.java +++ b/src/test/java/pl/project13/core/GitCommitIdTestCallback.java @@ -59,6 +59,8 @@ public class GitCommitIdTestCallback { private Charset propertiesSourceCharset = StandardCharsets.UTF_8; private boolean shouldPropertiesEscapeUnicode = false; private boolean shouldFailOnNoGitDirectory = false; + private boolean perModuleVersions = false; + private File moduleBaseDir; public GitCommitIdTestCallback() { try { @@ -200,6 +202,11 @@ public GitCommitIdTestCallback setShouldFailOnNoGitDirectory(boolean shouldFailO return this; } + public GitCommitIdTestCallback setPerModuleVersions(boolean perModuleVersions) { + this.perModuleVersions = perModuleVersions; + return this; + } + public GitCommitIdPlugin.Callback build() { return new GitCommitIdPlugin.Callback() { @Override @@ -353,6 +360,11 @@ public boolean shouldPropertiesEscapeUnicode() { public boolean shouldFailOnNoGitDirectory() { return shouldFailOnNoGitDirectory; } + + @Override + public boolean isPerModuleVersions() { + return perModuleVersions; + } }; } diff --git a/src/test/resources b/src/test/resources index 0e54950..13136ff 160000 --- a/src/test/resources +++ b/src/test/resources @@ -1 +1 @@ -Subproject commit 0e549504984403f5e9a29b9c104d027a705d9963 +Subproject commit 13136ff6241eefacb36ae19d4e18e4b7482812e1