diff --git a/README.md b/README.md index 91baf9b..7999b64 100644 --- a/README.md +++ b/README.md @@ -191,17 +191,22 @@ Either use add a ```true``` configuration in the inheriting project On the `initialize` maven phase, `git-code-format:install-hooks` installs a git `pre-commit` hook that looks like this : ```bash #!/bin/bash -"./.git/hooks/${project.artifactId}.git-code-format.pre-commit.sh" +"$(git rev-parse --git-common-dir)/hooks/${project.artifactId}.git-code-format.pre-commit.sh" ``` and `.git/hooks/${project.artifactId}.git-code-format.pre-commit.sh` has the following content: ```bash #!/bin/bash set -e -"${env.M2_HOME}/bin/mvn" -f "${project.basedir}/pom.xml" git-code-format:on-pre-commit +"${env.M2_HOME}/bin/mvn" -f "$(git rev-parse --show-toplevel)/" git-code-format:on-pre-commit ``` On `pre-commit` git phase, the hook triggers the `git-code-format:on-pre-commit` which formats the code of the modified files. +The hooks live in the directory shared by all the [linked worktrees](https://git-scm.com/docs/git-worktree) +of the repository, and the paths inside the repository are resolved against the worktree being +committed. A commit from a linked worktree therefore formats the staged files of that worktree, +with the pom and the maven wrapper of that worktree. + # Maven wrapper When the repository holds a [maven wrapper](https://maven.apache.org/wrapper/), the hook runs that diff --git a/core/src/main/java/com/cosium/code/format/AbstractMavenGitCodeFormatMojo.java b/core/src/main/java/com/cosium/code/format/AbstractMavenGitCodeFormatMojo.java index 11619cf..c27d2de 100644 --- a/core/src/main/java/com/cosium/code/format/AbstractMavenGitCodeFormatMojo.java +++ b/core/src/main/java/com/cosium/code/format/AbstractMavenGitCodeFormatMojo.java @@ -119,7 +119,8 @@ protected final boolean isExecutionRoot() { * @return The git hooks directory */ protected final Path getOrCreateHooksDirectory() { - Path hooksDirectory = gitRepository().getDirectory().toPath().resolve(HOOKS_DIR); + // Linked worktrees share the hooks of the common directory, which is where git runs them from. + Path hooksDirectory = gitRepository().getCommonDirectory().toPath().resolve(HOOKS_DIR); if (!Files.exists(hooksDirectory)) { getLog().debug("Creating directory " + hooksDirectory); try { @@ -134,6 +135,6 @@ protected final Path getOrCreateHooksDirectory() { } protected final Path gitBaseDir() { - return gitRepository().getDirectory().getParentFile().toPath(); + return gitRepository().getWorkTree().toPath(); } } diff --git a/core/src/main/java/com/cosium/code/format/InstallHooksMojo.java b/core/src/main/java/com/cosium/code/format/InstallHooksMojo.java index 4e615cf..4b38593 100644 --- a/core/src/main/java/com/cosium/code/format/InstallHooksMojo.java +++ b/core/src/main/java/com/cosium/code/format/InstallHooksMojo.java @@ -133,8 +133,8 @@ private void writePluginHooks(Path hooksDirectory) throws IOException { .truncateWithTemplate( () -> getClass().getResourceAsStream(BASE_PLUGIN_PRE_COMMIT_HOOK), StandardCharsets.UTF_8.toString(), - mavenExecutables.select(debug, preferMavenWrapper).toAbsolutePath(), - pomFile().toAbsolutePath(), + resolveFromWorkTreeRoot(mavenExecutables.select(debug, preferMavenWrapper)), + resolveFromWorkTreeRoot(pomFile()), mavenCliArguments()); getLog().debug("Written plugin pre commit hook file"); } @@ -179,8 +179,27 @@ private Path prepareHooksDirectory() { return hooksDirectory; } + /** + * The plugin hook is shared by all the worktrees of the repository. A path inside the worktree + * that installed it is therefore resolved against the worktree being committed. + */ + private String resolveFromWorkTreeRoot(Path path) { + Path absolutePath = path.toAbsolutePath().normalize(); + Path workTree = gitBaseDir().toAbsolutePath().normalize(); + if (!absolutePath.startsWith(workTree)) { + return "\"" + replaceBackslashes(absolutePath) + "\""; + } + return "\"$(git rev-parse --show-toplevel)/" + + replaceBackslashes(workTree.relativize(absolutePath)) + + "\""; + } + + private String replaceBackslashes(Path path) { + return path.toString().replace('\\', '/'); + } + private String preCommitHookBaseScriptCall() { - return "$(git rev-parse --git-dir)/" + HOOKS_DIR + "/" + pluginPreCommitHookFileName(); + return "$(git rev-parse --git-common-dir)/" + HOOKS_DIR + "/" + pluginPreCommitHookFileName(); } private List legacyPreCommitHookBaseScriptCalls() { @@ -195,6 +214,7 @@ private List legacyPreCommitHookBaseScriptCalls() { + gitBaseDir().relativize(getOrCreateHooksDirectory()) + "/" + pluginPreCommitHookFileName()); + calls.add("$(git rev-parse --git-dir)/" + HOOKS_DIR + "/" + pluginPreCommitHookFileName()); return calls; } diff --git a/core/src/test/java/com/cosium/code/format/MavenWrapperTest.java b/core/src/test/java/com/cosium/code/format/MavenWrapperTest.java index e354daf..15330e8 100644 --- a/core/src/test/java/com/cosium/code/format/MavenWrapperTest.java +++ b/core/src/test/java/com/cosium/code/format/MavenWrapperTest.java @@ -26,6 +26,8 @@ public class MavenWrapperTest extends AbstractTest { private static final String MAVEN_WRAPPER = "mvnw"; + private static final String WORK_TREE_MAVEN_WRAPPER = + "\"$(git rev-parse --show-toplevel)/" + MAVEN_WRAPPER + "\""; private static final String BAD_FORMAT_JAVA = "src/main/java/BadFormat.java"; private static final Path MAVEN_WRAPPER_PROPERTIES = Paths.get(".mvn/wrapper/maven-wrapper.properties"); @@ -37,11 +39,11 @@ public MavenWrapperTest(MavenRuntime.MavenRuntimeBuilder mavenBuilder) throws Ex @MavenPluginTest @DisplayName("GIVEN a maven wrapper WHEN installing the hooks THEN the hook runs the wrapper") public void test1() throws Exception { - Path wrapper = installMavenWrapper(projectRoot()); + installMavenWrapper(projectRoot()); installHooks(); - assertThat(readHookScript()).contains(wrapper.toAbsolutePath().toString()); + assertThat(readHookScript()).contains(WORK_TREE_MAVEN_WRAPPER); } @MavenPluginTest @@ -50,13 +52,13 @@ public void test1() throws Exception { + " wrapper") public void test2() throws Exception { // The build is run from the module, while the wrapper sits at the root of the repository. - Path wrapper = installMavenWrapper(projectRoot()); + installMavenWrapper(projectRoot()); Path module = Files.createDirectories(projectRoot().resolve("module")); Files.copy(projectRoot().resolve("pom.xml"), module.resolve("pom.xml")); buildMavenExecution(module).execute("initialize").assertErrorFreeLog(); - assertThat(readHookScript()).contains(wrapper.toAbsolutePath().toString()); + assertThat(readHookScript()).contains(WORK_TREE_MAVEN_WRAPPER); } @MavenPluginTest @@ -142,7 +144,7 @@ private void installHooks() throws Exception { * pins the maven version the build already runs with, hence no distribution left to download. The * executable bit has to be set back, as a plain copy drops it. */ - private Path installMavenWrapper(Path directory) throws IOException { + private void installMavenWrapper(Path directory) throws IOException { Path repositoryRoot = Paths.get(".."); Path wrapper = directory.resolve(MAVEN_WRAPPER); @@ -152,8 +154,6 @@ private Path installMavenWrapper(Path directory) throws IOException { Path properties = directory.resolve(MAVEN_WRAPPER_PROPERTIES); Files.createDirectories(properties.getParent()); Files.copy(repositoryRoot.resolve(MAVEN_WRAPPER_PROPERTIES), properties); - - return wrapper; } private String readHookScript() throws IOException { diff --git a/core/src/test/java/com/cosium/code/format/WorktreeTest.java b/core/src/test/java/com/cosium/code/format/WorktreeTest.java new file mode 100644 index 0000000..a0d109d --- /dev/null +++ b/core/src/test/java/com/cosium/code/format/WorktreeTest.java @@ -0,0 +1,102 @@ +package com.cosium.code.format; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.takari.maven.testing.executor.MavenRuntime; +import io.takari.maven.testing.executor.MavenVersions; +import io.takari.maven.testing.executor.junit.MavenPluginTest; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.io.TempDir; + +/** + * Covers issue 390. + * + *

JGit cannot add a linked worktree, hence the git command line. + * + * @author Réda Housni Alaoui + */ +@MavenVersions({"3.5.0"}) +public class WorktreeTest extends AbstractTest { + + private static final String BAD_FORMAT_JAVA = "src/main/java/BadFormat.java"; + private static final String FORMATTED_CONTENT = + "public class BadFormat {\n" + "\n" + " void a() {}\n" + "}\n"; + + @TempDir private Path worktreeParent; + private Path worktree; + + public WorktreeTest(MavenRuntime.MavenRuntimeBuilder mavenBuilder) throws Exception { + super(mavenBuilder, "single-module"); + } + + @BeforeEach + void addWorktree() throws Exception { + worktree = worktreeParent.resolve("wt"); + runGit(projectRoot(), "worktree", "add", "-b", "wt", worktree.toString()); + } + + @MavenPluginTest + @DisplayName( + "GIVEN hooks installed from the main checkout WHEN committing a bad formatted file from a" + + " linked worktree THEN the file of the worktree has the correct format") + public void test1() throws Exception { + buildMavenExecution(projectRoot()).execute("initialize").assertErrorFreeLog(); + + commitBadFormattedFileFromWorktree(); + + assertThat(worktree.resolve(BAD_FORMAT_JAVA)) + .content(StandardCharsets.UTF_8) + .isEqualTo(FORMATTED_CONTENT); + } + + @MavenPluginTest + @DisplayName( + "GIVEN hooks installed from a linked worktree WHEN committing a bad formatted file from the" + + " worktree THEN the file of the worktree has the correct format") + public void test2() throws Exception { + buildMavenExecution(worktree).execute("initialize").assertErrorFreeLog(); + + commitBadFormattedFileFromWorktree(); + + assertThat(worktree.resolve(BAD_FORMAT_JAVA)) + .content(StandardCharsets.UTF_8) + .isEqualTo(FORMATTED_CONTENT); + } + + private void commitBadFormattedFileFromWorktree() throws Exception { + Files.write( + worktree.resolve(BAD_FORMAT_JAVA), + ("public class BadFormat {\n" + "\n" + " void a( ){}\n" + "}\n") + .getBytes(StandardCharsets.UTF_8)); + runGit(worktree, "add", BAD_FORMAT_JAVA); + runGit( + worktree, + "-c", + "user.name=" + gitIdentity().getName(), + "-c", + "user.email=" + gitIdentity().getEmailAddress(), + "commit", + "-m", + "Committing a badly formatted file"); + } + + private void runGit(Path directory, String... arguments) + throws IOException, InterruptedException { + List command = new ArrayList<>(); + command.add("git"); + command.addAll(Arrays.asList(arguments)); + Process process = + new ProcessBuilder(command).directory(directory.toFile()).redirectErrorStream(true).start(); + String output = IOUtils.toString(process.getInputStream(), StandardCharsets.UTF_8); + assertThat(process.waitFor()).as(output).isZero(); + } +}