diff --git a/src/main/java/pl/project13/maven/git/JGitProvider.java b/src/main/java/pl/project13/maven/git/JGitProvider.java index 891b0d6b..838068bf 100644 --- a/src/main/java/pl/project13/maven/git/JGitProvider.java +++ b/src/main/java/pl/project13/maven/git/JGitProvider.java @@ -3,8 +3,10 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Joiner; +import com.google.common.base.Objects; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; + import org.apache.maven.plugin.MojoExecutionException; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; @@ -18,6 +20,7 @@ import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.jetbrains.annotations.NotNull; + import pl.project13.jgit.DescribeCommand; import pl.project13.jgit.DescribeResult; import pl.project13.maven.git.log.LoggerBridge; @@ -66,13 +69,13 @@ protected void init() throws MojoExecutionException { @Override protected String getBuildAuthorName() { String userName = git.getConfig().getString("user", null, "name"); - return userName; + return Objects.firstNonNull(userName, ""); } @Override protected String getBuildAuthorEmail() { String userEmail = git.getConfig().getString("user", null, "email"); - return userEmail; + return Objects.firstNonNull(userEmail, ""); } @Override @@ -142,13 +145,13 @@ protected String getCommitAuthorEmail() { @Override protected String getCommitMessageFull() { String fullMessage = headCommit.getFullMessage(); - return fullMessage; + return fullMessage.trim(); } @Override protected String getCommitMessageShort() { String shortMessage = headCommit.getShortMessage(); - return shortMessage; + return shortMessage.trim(); } @Override diff --git a/src/main/java/pl/project13/maven/git/NativeGitProvider.java b/src/main/java/pl/project13/maven/git/NativeGitProvider.java index 87413e8b..3cfdd6e9 100644 --- a/src/main/java/pl/project13/maven/git/NativeGitProvider.java +++ b/src/main/java/pl/project13/maven/git/NativeGitProvider.java @@ -1,18 +1,18 @@ package pl.project13.maven.git; -import com.google.common.base.Function; -import com.google.common.base.Joiner; -import com.google.common.base.Predicate; +import static java.lang.String.format; + import com.google.common.base.Splitter; -import com.google.common.collect.FluentIterable; -import com.google.common.collect.ImmutableList; +import com.google.common.base.Throwables; import com.google.common.collect.Lists; + import org.apache.maven.plugin.MojoExecutionException; import org.jetbrains.annotations.NotNull; + import pl.project13.maven.git.log.LoggerBridge; import java.io.*; -import java.util.Arrays; +import java.text.SimpleDateFormat; public class NativeGitProvider extends GitDataProvider { @@ -55,12 +55,26 @@ protected void init() throws MojoExecutionException { @Override protected String getBuildAuthorName() { - return tryToRunGitCommand(canonical, "log -1 --pretty=format:\"%an\""); + try { + return runGitCommand(canonical, "config --get user.name"); + } catch (NativeCommandException e) { + if (e.getExitCode() == 1) { // No config file found + return ""; + } + throw Throwables.propagate(e); + } } @Override protected String getBuildAuthorEmail() { - return tryToRunGitCommand(canonical, "log -1 --pretty=format:\"%ae\""); + try { + return runGitCommand(canonical, "config --get user.email"); + } catch (NativeCommandException e) { + if (e.getExitCode() == 1) { // No config file found + return ""; + } + throw Throwables.propagate(e); + } } @Override @@ -75,21 +89,26 @@ protected String getBranchName() throws IOException { private String getBranch(File canonical) { String branch = null; try{ - branch = tryToRunGitCommand(canonical, "symbolic-ref HEAD"); + branch = runGitCommand(canonical, "symbolic-ref HEAD"); if (branch != null) { branch = branch.replace("refs/heads/", ""); } - }catch(RuntimeException e){ + } catch(NativeCommandException e) { // it seems that git repro is in 'DETACHED HEAD'-State, using Commid-Id as Branch - branch = getCommitId(); + String err = e.getStderr(); + if (err != null && err.contains("ref HEAD is not a symbolic ref")) { + branch = getCommitId(); + } else { + throw Throwables.propagate(e); + } } return branch; } @Override - protected String getGitDescribe() throws MojoExecutionException { + protected String getGitDescribe() { final String argumentsForGitDescribe = getArgumentsForGitDescribe(gitDescribe); - final String gitDescribe = tryToRunGitCommand(canonical, "describe" + argumentsForGitDescribe); + final String gitDescribe = runQuietGitCommand(canonical, "describe" + argumentsForGitDescribe); return gitDescribe; } @@ -126,11 +145,11 @@ private String getArgumentsForGitDescribe(GitDescribeConfig describeConfig) { @Override protected String getCommitId() { - return tryToRunGitCommand(canonical, "rev-parse HEAD"); + return runQuietGitCommand(canonical, "rev-parse HEAD"); } @Override - protected String getAbbrevCommitId() throws MojoExecutionException { + protected String getAbbrevCommitId() { // we could run: tryToRunGitCommand(canonical, "rev-parse --short="+abbrevLength+" HEAD"); // but minimum length for --short is 4, our abbrevLength could be 2 String commitId = getCommitId(); @@ -150,58 +169,35 @@ protected boolean isDirty() throws MojoExecutionException { @Override protected String getCommitAuthorName() { - return tryToRunGitCommand(canonical, "log -1 --pretty=format:\"%cn\""); + return runQuietGitCommand(canonical, "log -1 --pretty=format:%an"); } @Override protected String getCommitAuthorEmail() { - return tryToRunGitCommand(canonical, "log -1 --pretty=format:\"%ce\""); + return runQuietGitCommand(canonical, "log -1 --pretty=format:%ae"); } @Override protected String getCommitMessageFull() { - return tryToRunGitCommand(canonical, "log -1 --pretty=format:\"%B\""); + return runQuietGitCommand(canonical, "log -1 --pretty=format:%B"); } @Override protected String getCommitMessageShort() { - return tryToRunGitCommand(canonical, "log -1 --pretty=format:\"%s\""); + return runQuietGitCommand(canonical, "log -1 --pretty=format:%s"); } @Override protected String getCommitTime() { - return tryToRunGitCommand(canonical, "log -1 --pretty=format:\"%ci\""); + String value = runQuietGitCommand(canonical, "log -1 --pretty=format:%ct"); + SimpleDateFormat smf = new SimpleDateFormat(dateFormat); + return smf.format(Long.parseLong(value)*1000L); } @Override - protected String getTags() throws MojoExecutionException { - final String branch = tryToRunGitCommand(canonical, "rev-parse --abbrev-ref HEAD"); - - String out = tryToRunGitCommand(canonical, "log -n 1 --pretty=format:'%d'"); - String[] nms = out - .replaceAll("HEAD", "") - .replaceAll("\\)", "") - .replaceAll("\\(", "") - .replaceAll("'", "") - .replaceAll("tag: ", "") - .replaceAll(",", "") - .trim() - .split(" "); - - - ImmutableList cleanTags = FluentIterable.from(Arrays.asList(nms)). - transform(new Function() { - @Override public String apply(String input) { - return input.trim(); - } - }). - filter(new Predicate() { - @Override public boolean apply(String input) { - return !input.equals(branch); - } - }).toList(); - - return Joiner.on(",").join(cleanTags); + protected String getTags() { + final String result = runQuietGitCommand(canonical, "tag --contains"); + return result.replace('\n', ','); } @Override @@ -215,70 +211,67 @@ protected void finalCleanUp() { private String getOriginRemote(File directory) throws MojoExecutionException { String remoteUrl = null; - try { - String remotes = runGitCommand(directory, "remote -v"); + String remotes = runQuietGitCommand(directory, "remote -v"); - // welcome to text output parsing hell! - no `\n` is not enough - for (String line : Splitter.onPattern("\\((fetch|push)\\)?").split(remotes)) { - String trimmed = line.trim(); + // welcome to text output parsing hell! - no `\n` is not enough + for (String line : Splitter.onPattern("\\((fetch|push)\\)?").split(remotes)) { + String trimmed = line.trim(); - if (trimmed.startsWith("origin")) { - String[] splited = trimmed.split("\\s+"); - if (splited.length != REMOTE_COLS - 1) { // because (fetch/push) was trimmed - throw new MojoExecutionException("Unsupported GIT output (verbose remote address): " + line); - } - remoteUrl = splited[1]; + if (trimmed.startsWith("origin")) { + String[] splited = trimmed.split("\\s+"); + if (splited.length != REMOTE_COLS - 1) { // because (fetch/push) was trimmed + throw new MojoExecutionException("Unsupported GIT output (verbose remote address): " + line); } + remoteUrl = splited[1]; } - } catch (Exception e) { - throw new MojoExecutionException("Error while obtaining origin remote", e); } return remoteUrl; } - private String tryToRunGitCommand(File directory, String gitCommand) { - String retValue = ""; - try { - retValue = runGitCommand(directory, gitCommand); - } catch (MojoExecutionException ex) { - // do nothing - } - return retValue; - } - /** * Runs a maven command and returns {@code true} if output was non empty. * Can be used to short cut reading output from command when we know it may be a rather long one. - * */ + * + * Return true if the result is empty. + * + **/ private boolean tryCheckEmptyRunGitCommand(File directory, String gitCommand) { try { String env = System.getenv("GIT_PATH"); String exec = (env == null) ? "git" : env; String command = String.format("%s %s", exec, gitCommand); - boolean empty = getRunner().runEmpty(directory, command); - return !empty; + return getRunner().runEmpty(directory, command); } catch (IOException ex) { + // Error means "non-empty" return false; // do nothing... } } - private String runGitCommand(File directory, String gitCommand) throws MojoExecutionException { + private String runQuietGitCommand(File directory, String gitCommand) { + final String env = System.getenv("GIT_PATH"); + final String exec = (env == null) ? "git" : env; + final String command = String.format("%s %s", exec, gitCommand); + try { - final String env = System.getenv("GIT_PATH"); - final String exec = (env == null) ? "git" : env; - final String command = String.format("%s %s", exec, gitCommand); + return getRunner().run(directory, command.trim()).trim(); + } catch (IOException e) { + throw Throwables.propagate(e); + } + } - final String result = getRunner().run(directory, command.trim()).trim(); - return result; - } catch (IOException ex) { - if (ex.getMessage().contains("exited with invalid status")) { - throw new RuntimeException("Failed to execute git command (`git " + gitCommand + "` @ " + directory +")!", ex); - } else { - throw new MojoExecutionException("Could not run GIT command - GIT is not installed or not exists in system path? " + - "Tried to run: 'git " + gitCommand + "'", ex); - } + private String runGitCommand(File directory, String gitCommand) throws NativeCommandException { + final String env = System.getenv("GIT_PATH"); + final String exec = (env == null) ? "git" : env; + final String command = String.format("%s %s", exec, gitCommand); + + try { + return getRunner().run(directory, command.trim()).trim(); + } catch (NativeCommandException e) { + throw e; + } catch (IOException e) { + throw Throwables.propagate(e); } } @@ -296,6 +289,52 @@ public interface ProcessRunner { boolean runEmpty(File directory, String command) throws IOException; } + public static class NativeCommandException extends IOException + { + private final int exitCode; + private final String command; + private final File directory; + private final String stdout; + private final String stderr; + + public NativeCommandException(int exitCode, + String command, + File directory, + String stdout, + String stderr) { + this.exitCode = exitCode; + this.command = command; + this.directory = directory; + this.stdout = stdout; + this.stderr = stderr; + } + + public int getExitCode() { + return exitCode; + } + + public String getCommand() { + return command; + } + + public File getDirectory() { + return directory; + } + + public String getStdout() { + return stdout; + } + + public String getStderr() { + return stderr; + } + + @Override + public String getMessage() { + return format("Git command exited with invalid status [%d]: stdout: `%s`, stderr: `%s`", exitCode, stdout, stderr); + } + } + protected static class JavaProcessRunner implements ProcessRunner { @Override public String run(File directory, String command) throws IOException { @@ -311,14 +350,12 @@ public String run(File directory, String command) throws IOException { final StringBuilder commandResult = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { - commandResult.append(line); + commandResult.append(line).append("\n"); } if (proc.exitValue() != 0) { final StringBuilder errMsg = readStderr(err); - - final String message = String.format("Git command exited with invalid status [%d]: stdout: `%s`, stderr: `%s`", proc.exitValue(), output, errMsg.toString()); - throw new IOException(message); + throw new NativeCommandException(proc.exitValue(), command, directory, output, errMsg.toString()); } output = commandResult.toString(); } catch (InterruptedException ex) { diff --git a/src/test/java/pl/project13/maven/git/FileSystemMavenSandbox.java b/src/test/java/pl/project13/maven/git/FileSystemMavenSandbox.java index 580d9981..0ffaba82 100644 --- a/src/test/java/pl/project13/maven/git/FileSystemMavenSandbox.java +++ b/src/test/java/pl/project13/maven/git/FileSystemMavenSandbox.java @@ -25,6 +25,8 @@ import java.io.File; import java.io.IOException; +import com.google.common.io.Files; + /** * Quick and dirty maven projects tree structure to create on disk during integration tests * Can have both parent and child projects set up @@ -118,7 +120,14 @@ public FileSystemMavenSandbox create(CleanUp cleanupMode) throws RuntimeExceptio private void createGitRepoIfRequired() throws IOException { if (gitRepoTargetDir != null) { - FileUtils.copyDirectory(gitRepoSourceDir, new File(gitRepoTargetDir, ".git")); + File gitFolder = new File(gitRepoTargetDir, ".git"); + FileUtils.copyDirectory(gitRepoSourceDir, gitFolder); + // As the WITH_NO_CHANGES and WITH_CHANGES git trees contain empty + // folders whose existence is crucial for the native git to run (jgit does not mind) + // *and* because empty folders are silently omitted from git checkins, ensure that + // these folders exist + Files.createParentDirs(new File(gitFolder, "refs/heads")); + Files.createParentDirs(new File(gitFolder, "refs/tags")); } } diff --git a/src/test/java/pl/project13/maven/git/GitCommitIdMojoIntegrationTest.java b/src/test/java/pl/project13/maven/git/GitCommitIdMojoIntegrationTest.java index 3c2dbfd9..38943057 100644 --- a/src/test/java/pl/project13/maven/git/GitCommitIdMojoIntegrationTest.java +++ b/src/test/java/pl/project13/maven/git/GitCommitIdMojoIntegrationTest.java @@ -622,17 +622,17 @@ public void shouldExtractTagsOnGivenCommit(boolean useNativeGit) throws Exceptio @Parameters(method = "useNativeGit") public void runGitDescribeWithMatchOption(boolean useNativeGit) throws Exception { // given - mavenSandbox.withParentProject("my-pom-project", "pom") - .withChildProject("my-jar-module", "jar") - .withGitRepoInChild(AvailableGitTestRepo.MAVEN_GIT_COMMIT_ID_PLUGIN) + mavenSandbox.withParentProject("my-plugin-project", "jar") + .withNoChildProject() + .withGitRepoInParent(AvailableGitTestRepo.MAVEN_GIT_COMMIT_ID_PLUGIN) .create(CleanUp.CLEANUP_FIRST); - MavenProject targetProject = mavenSandbox.getChildProject(); + MavenProject targetProject = mavenSandbox.getParentProject(); setProjectToExecuteMojoIn(targetProject); Map gitTagMap = new HashMap(); - gitTagMap.put("v2.1.8", "4f787aa37d5d9c06780278f0cf92553d304820a2"); - gitTagMap.put("v2.1.9", "a9dba4a25b64ab288d90cd503785b830d2e189a2"); + gitTagMap.put("v2.1.11", "56c5a491720ce35ae8f8626be1d3414728f1b953"); + gitTagMap.put("v2.1.12", "e9879658209ee81d7bf50ceedd028737f0b1cd0c"); for (Map.Entry entry : gitTagMap.entrySet()) { String gitDescribeMatchNeedle = entry.getKey(); diff --git a/src/test/java/pl/project13/maven/git/NativeAndJGitProviderTest.java b/src/test/java/pl/project13/maven/git/NativeAndJGitProviderTest.java new file mode 100644 index 00000000..cea32faa --- /dev/null +++ b/src/test/java/pl/project13/maven/git/NativeAndJGitProviderTest.java @@ -0,0 +1,165 @@ +/* + * This file is part of git-commit-id-plugin by Konrad Malawski + * + * git-commit-id-plugin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * git-commit-id-plugin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with git-commit-id-plugin. If not, see . + */ + +package pl.project13.maven.git; + +import static org.fest.assertions.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.mockito.internal.util.reflection.Whitebox.setInternalState; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Properties; + +import org.apache.maven.project.MavenProject; +import org.junit.Assert; +import org.junit.Test; + +import pl.project13.maven.git.FileSystemMavenSandbox.CleanUp; + +public class NativeAndJGitProviderTest extends GitIntegrationTest +{ + public static final String[] GIT_KEYS = new String[] { + "git.build.time", + "git.branch", + "git.commit.id", + "git.commit.id.abbrev", + "git.commit.id.describe", + "git.build.user.name", + "git.build.user.email", + "git.commit.user.name", + "git.commit.user.email", + "git.commit.message.full", + "git.commit.message.short", + "git.commit.time", + "git.remote.origin.url" + }; + + public static final String DEFAULT_FORMAT_STRING = "dd.MM.yyyy '@' HH:mm:ss z"; + public static final String ISO8601_FORMAT_STRING = "yyyy-MM-dd'T'HH:mm:ssZZ"; + + @Test + public void testCompareBasic() throws Exception + { + // Test on all available basic repos to ensure that the output is identical. + for (AvailableGitTestRepo testRepo : AvailableGitTestRepo.values()) { + mavenSandbox.withParentProject("my-basic-project", "jar").withNoChildProject().withGitRepoInParent(testRepo).create(CleanUp.CLEANUP_FIRST); + MavenProject targetProject = mavenSandbox.getParentProject(); + verifyNativeAndJGit(targetProject, DEFAULT_FORMAT_STRING); + } + } + + @Test + public void testCompareSubrepoInRoot() throws Exception + { + for (AvailableGitTestRepo testRepo : AvailableGitTestRepo.values()) { + if (testRepo == AvailableGitTestRepo.MAVEN_GIT_COMMIT_ID_PLUGIN) { + continue; // Don't create a subrepo based off the plugin repo itself. + } + mavenSandbox.withParentProject("my-pom-project", "pom").withChildProject("my-jar-module", "jar").withGitRepoInParent(testRepo).create(CleanUp.CLEANUP_FIRST); + MavenProject targetProject = mavenSandbox.getParentProject(); + verifyNativeAndJGit(targetProject, DEFAULT_FORMAT_STRING); + } + } + + @Test + public void testCompareSubrepoInChild() throws Exception + { + for (AvailableGitTestRepo testRepo : AvailableGitTestRepo.values()) { + if (testRepo == AvailableGitTestRepo.MAVEN_GIT_COMMIT_ID_PLUGIN) { + continue; // Don't create a subrepo based off the plugin repo itself. + } + mavenSandbox.withParentProject("my-pom-project", "pom").withChildProject("my-jar-module", "jar").withGitRepoInParent(testRepo).create(CleanUp.CLEANUP_FIRST); + MavenProject targetProject = mavenSandbox.getChildProject(); + verifyNativeAndJGit(targetProject, DEFAULT_FORMAT_STRING); + } + } + + @Test + public void testCompareISO8601Time() throws Exception + { + // Test on all available basic repos to ensure that the output is identical. + for (AvailableGitTestRepo testRepo : AvailableGitTestRepo.values()) { + mavenSandbox.withParentProject("my-basic-project", "jar").withNoChildProject().withGitRepoInParent(testRepo).create(CleanUp.CLEANUP_FIRST); + MavenProject targetProject = mavenSandbox.getParentProject(); + verifyNativeAndJGit(targetProject, ISO8601_FORMAT_STRING); + } + } + + private void verifyNativeAndJGit(MavenProject targetProject, String formatString) throws Exception + { + setProjectToExecuteMojoIn(targetProject); + + alterMojoSettings("skipPoms", false); + alterMojoSettings("dateFormat", formatString); + + DateFormat format = new SimpleDateFormat(formatString); + + alterMojoSettings("useNativeGit", false); + mojo.execute(); + Properties jgitProps = createCopy(targetProject.getProperties()); + + alterMojoSettings("useNativeGit", true); + mojo.execute(); + Properties nativeProps = createCopy(targetProject.getProperties()); + + assertGitPropertiesPresentInProject(jgitProps); + assertGitPropertiesPresentInProject(nativeProps); + + for (String key : GIT_KEYS) { + if (!key.equals("git.build.time")) { // git.build.time is excused because the two runs happened at different times. + assertEquals("Key difference for key: '" + key + "'", jgitProps.getProperty(key), nativeProps.getProperty(key)); + } + else { + // Ensure that the date formats are parseable and within reason. If running all the git commands on the + // native provider takes more than 60 seconds, then something is seriously wrong. + long jGitBuildTimeInMs = format.parse(jgitProps.getProperty(key)).getTime(); + long nativeBuildTimeInMs = format.parse(nativeProps.getProperty(key)).getTime(); + Assert.assertTrue("Time ran backwards, jgitTime after nativeTime!", jGitBuildTimeInMs <= nativeBuildTimeInMs); + Assert.assertTrue("Build ran too slow.", (nativeBuildTimeInMs - jGitBuildTimeInMs) < 60000L); // If native takes more than 1 minute, something is wrong. + } + } + + // Check the commit time to be equal in ms, too. + long jGitCommitTimeInMs = format.parse(jgitProps.getProperty("git.commit.time")).getTime(); + long nativeCommitTimeInMs = format.parse(nativeProps.getProperty("git.commit.time")).getTime(); + + assertEquals("commit times parse to different time stamps", jGitCommitTimeInMs, nativeCommitTimeInMs); + } + + private void alterMojoSettings(String parameterName, Object parameterValue) + { + setInternalState(mojo, parameterName, parameterValue); + } + + private Properties createCopy(Properties orig) + { + Properties p = new Properties(); + for (String key : orig.stringPropertyNames()) { + p.setProperty(key, orig.getProperty(key)); + } + + return p; + } + + private void assertGitPropertiesPresentInProject(Properties properties) + { + for (String key : GIT_KEYS) { + assertThat(properties).satisfies(new ContainsKeyCondition(key)); + } + } +}