/* * This file is part of git-commit-id-plugin by Konrad 'ktoso' Malawski <konrad.malawski@java.pl> * * 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 <http://www.gnu.org/licenses/>. */ package pl.project13.maven.git; import static java.lang.String.format; import com.google.common.base.Throwables; import org.jetbrains.annotations.NotNull; import pl.project13.maven.git.log.LoggerBridge; import java.io.*; import java.text.SimpleDateFormat; public class NativeGitProvider extends GitDataProvider { private transient ProcessRunner runner; final File dotGitDirectory; final File canonical; @NotNull public static NativeGitProvider on(@NotNull File dotGitDirectory, @NotNull LoggerBridge log) { return new NativeGitProvider(dotGitDirectory, log); } NativeGitProvider(@NotNull File dotGitDirectory, @NotNull LoggerBridge log) { super(log); this.dotGitDirectory = dotGitDirectory; try { this.canonical = dotGitDirectory.getCanonicalFile(); } catch (IOException ex) { throw new RuntimeException(new GitCommitIdExecutionException("Passed a invalid directory, not a GIT repository: " + dotGitDirectory, ex)); } } @Override protected void init() throws GitCommitIdExecutionException { // noop ... } @Override protected String getBuildAuthorName() throws GitCommitIdExecutionException { 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() throws GitCommitIdExecutionException { 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 protected void prepareGitToExtractMoreDetailedRepoInformation() throws GitCommitIdExecutionException { } @Override protected String getBranchName() throws GitCommitIdExecutionException { return getBranch(canonical); } private String getBranch(File canonical) throws GitCommitIdExecutionException { String branch; try{ branch = runGitCommand(canonical, "symbolic-ref HEAD"); if (branch != null) { branch = branch.replace("refs/heads/", ""); } } catch (NativeCommandException e) { // it seems that git repo is in 'DETACHED HEAD'-State, using Commit-Id as Branch 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 GitCommitIdExecutionException { final String argumentsForGitDescribe = getArgumentsForGitDescribe(gitDescribe); return runQuietGitCommand(canonical, "describe" + argumentsForGitDescribe); } private String getArgumentsForGitDescribe(GitDescribeConfig describeConfig) { if (describeConfig == null) return ""; StringBuilder argumentsForGitDescribe = new StringBuilder(); if (describeConfig.isAlways()) { argumentsForGitDescribe.append(" --always"); } final String dirtyMark = describeConfig.getDirty(); if (dirtyMark != null && !dirtyMark.isEmpty()) { argumentsForGitDescribe.append(" --dirty=").append(dirtyMark); } final String matchOption = describeConfig.getMatch(); if (matchOption != null && !matchOption.isEmpty()) { argumentsForGitDescribe.append(" --match=").append(matchOption); } argumentsForGitDescribe.append(" --abbrev=").append(describeConfig.getAbbrev()); if (describeConfig.getTags()) { argumentsForGitDescribe.append(" --tags"); } if (describeConfig.getForceLongFormat()) { argumentsForGitDescribe.append(" --long"); } return argumentsForGitDescribe.toString(); } @Override protected String getCommitId() throws GitCommitIdExecutionException { return runQuietGitCommand(canonical, "rev-parse HEAD"); } @Override protected String getAbbrevCommitId() throws GitCommitIdExecutionException { // 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(); String abbrevCommitId = ""; if (commitId != null && !commitId.isEmpty()) { abbrevCommitId = commitId.substring(0, abbrevLength); } return abbrevCommitId; } @Override protected boolean isDirty() throws GitCommitIdExecutionException { return !tryCheckEmptyRunGitCommand(canonical, "status -s"); } @Override protected String getCommitAuthorName() throws GitCommitIdExecutionException { return runQuietGitCommand(canonical, "log -1 --pretty=format:%an"); } @Override protected String getCommitAuthorEmail() throws GitCommitIdExecutionException { return runQuietGitCommand(canonical, "log -1 --pretty=format:%ae"); } @Override protected String getCommitMessageFull() throws GitCommitIdExecutionException { return runQuietGitCommand(canonical, "log -1 --pretty=format:%B"); } @Override protected String getCommitMessageShort() throws GitCommitIdExecutionException { return runQuietGitCommand(canonical, "log -1 --pretty=format:%s"); } @Override protected String getCommitTime() throws GitCommitIdExecutionException { String value = runQuietGitCommand(canonical, "log -1 --pretty=format:%ct"); SimpleDateFormat smf = getSimpleDateFormatWithTimeZone(); return smf.format(Long.parseLong(value)*1000L); } @Override protected String getTags() throws GitCommitIdExecutionException { final String result = runQuietGitCommand(canonical, "tag --contains"); return result.replace('\n', ','); } @Override protected String getRemoteOriginUrl() throws GitCommitIdExecutionException { return getOriginRemote(canonical); } @Override protected String getClosestTagName() throws GitCommitIdExecutionException { try { return runGitCommand(canonical, "describe --abbrev=0 --tags"); } catch (NativeCommandException ignore) { // could not find any tags to describe } return ""; } @Override protected String getClosestTagCommitCount() throws GitCommitIdExecutionException { String closestTagName = getClosestTagName(); if(closestTagName != null && !closestTagName.trim().isEmpty()){ return runQuietGitCommand(canonical, "rev-list "+closestTagName+"..HEAD --count"); } return ""; } @Override protected void finalCleanUp() throws GitCommitIdExecutionException { } private String getOriginRemote(File directory) throws GitCommitIdExecutionException { try { String remoteUrl = runGitCommand(directory, "ls-remote --get-url"); return stripCredentialsFromOriginUrl(remoteUrl); } catch (NativeCommandException ignore) { // No remote configured to list refs from } return null; } /** * 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); return getRunner().runEmpty(directory, command); } catch (IOException ex) { // Error means "non-empty" return false; // do nothing... } } 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 { return getRunner().run(directory, command.trim()).trim(); } catch (IOException e) { throw Throwables.propagate(e); } } 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); } } private ProcessRunner getRunner() { if (runner == null) { runner = new JavaProcessRunner(); } return runner; } public interface ProcessRunner { /** Run a command and return the entire output as a String - naive, we know. */ String run(File directory, String command) throws IOException; /** Run a command and return false if it contains at least one output line*/ 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 { String output = ""; try { ProcessBuilder builder = new ProcessBuilder(command.split("\\s")); final Process proc = builder.directory(directory).start(); proc.waitFor(); final InputStream is = proc.getInputStream(); final InputStream err = proc.getErrorStream(); final BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8")); final StringBuilder commandResult = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { commandResult.append(line).append("\n"); } if (proc.exitValue() != 0) { final StringBuilder errMsg = readStderr(err); throw new NativeCommandException(proc.exitValue(), command, directory, output, errMsg.toString()); } output = commandResult.toString(); } catch (InterruptedException ex) { throw new IOException(ex); } return output; } private StringBuilder readStderr(InputStream err) throws IOException { String line; final BufferedReader errReader = new BufferedReader(new InputStreamReader(err)); final StringBuilder errMsg = new StringBuilder(); while((line = errReader.readLine())!=null){ errMsg.append(line); } return errMsg; } @Override public boolean runEmpty(File directory, String command) throws IOException { boolean empty = true; try { // this only works on UNIX like system not on Windows // ProcessBuilder builder = new ProcessBuilder(Arrays.asList("/bin/sh", "-c", command)); // so use the same protocol as used in the run() method ProcessBuilder builder = new ProcessBuilder(command.split("\\s")); final Process proc = builder.directory(directory).start(); proc.waitFor(); final InputStream is = proc.getInputStream(); final InputStream err = proc.getErrorStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(is)); if (reader.readLine() != null) { empty = false; } if (proc.exitValue() != 0) { final StringBuilder errMsg = readStderr(err); throw new NativeCommandException(proc.exitValue(), command, directory, "", errMsg.toString()); } } catch (InterruptedException ex) { throw new IOException(ex); } return empty; // was non-empty } } }