pl.project13.core.NativeGitProvider Maven / Gradle / Ivy
The newest version!
/*
* This file is part of git-commit-id-plugin-core by Konrad 'ktoso' Malawski
*
* git-commit-id-plugin-core 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-core 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-core. If not, see .
*/
package pl.project13.core;
import static java.lang.String.format;
import pl.project13.core.git.GitDescribeConfig;
import pl.project13.core.log.LogInterface;
import javax.annotation.Nonnull;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Optional;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class NativeGitProvider extends GitDataProvider {
private transient ProcessRunner runner;
final File dotGitDirectory;
final long nativeGitTimeoutInMs;
final File canonical;
@Nonnull
public static NativeGitProvider on(@Nonnull File dotGitDirectory, long nativeGitTimeoutInMs, @Nonnull LogInterface log) {
return new NativeGitProvider(dotGitDirectory, nativeGitTimeoutInMs, log);
}
NativeGitProvider(@Nonnull File dotGitDirectory, long nativeGitTimeoutInMs, @Nonnull LogInterface log) {
super(log);
this.dotGitDirectory = dotGitDirectory;
this.nativeGitTimeoutInMs = nativeGitTimeoutInMs;
try {
this.canonical = dotGitDirectory.getCanonicalFile();
} catch (IOException ex) {
throw new RuntimeException(new GitCommitIdExecutionException("Passed a invalid directory, not a GIT repository: " + dotGitDirectory, ex));
}
}
@Override
public void init() throws GitCommitIdExecutionException {
// noop ...
}
@Override
public String getBuildAuthorName() throws GitCommitIdExecutionException {
try {
return runGitCommand(canonical, nativeGitTimeoutInMs, "config --get user.name");
} catch (NativeCommandException e) {
if (e.getExitCode() == 1) { // No config file found
return "";
}
throw new RuntimeException(e);
}
}
@Override
public String getBuildAuthorEmail() throws GitCommitIdExecutionException {
try {
return runGitCommand(canonical, nativeGitTimeoutInMs, "config --get user.email");
} catch (NativeCommandException e) {
if (e.getExitCode() == 1) { // No config file found
return "";
}
throw new RuntimeException(e);
}
}
@Override
public void prepareGitToExtractMoreDetailedRepoInformation() throws GitCommitIdExecutionException {
}
@Override
public String getBranchName() throws GitCommitIdExecutionException {
if (evalCommitIsNotHead()) {
// git branch --points-at $evaluateOnCommit
return getBranchForCommitish(canonical);
} else {
// git symbolic-ref --short HEAD
return getBranchForHead(canonical);
}
}
private boolean evalCommitIsNotHead() {
return (evaluateOnCommit != null) && !evaluateOnCommit.equals("HEAD");
}
private String getBranchForHead(File canonical) throws GitCommitIdExecutionException {
String branch;
try {
branch = runGitCommand(canonical, nativeGitTimeoutInMs, "symbolic-ref --short " + evaluateOnCommit);
} 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) {
boolean noSymbolicRef = err.contains("ref " + evaluateOnCommit + " is not a symbolic ref");
boolean noSuchRef = err.contains("No such ref: " + evaluateOnCommit);
if (noSymbolicRef || noSuchRef) {
branch = getCommitId();
} else {
throw new GitCommitIdExecutionException(e);
}
} else {
throw new GitCommitIdExecutionException(e);
}
}
return branch;
}
private String getBranchForCommitish(File canonical) throws GitCommitIdExecutionException {
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"))
.map(s -> s.replaceAll("[\\* ]+", ""))
// --points-at returns detached HEAD as branch, we don't like that
.filter(s -> !s.startsWith("(HEAD detached at"))
.collect(Collectors.joining(","));
} else {
// it seems that nothing is pointing to the commit, using Commit-Id as Branch
branch = getCommitId();
}
return branch;
}
@Override
public String getGitDescribe() throws GitCommitIdExecutionException {
final String argumentsForGitDescribe = getArgumentsForGitDescribe(gitDescribe);
return runQuietGitCommand(canonical, nativeGitTimeoutInMs, "describe" + argumentsForGitDescribe);
}
private String getArgumentsForGitDescribe(GitDescribeConfig describeConfig) {
if (describeConfig == null) {
return "";
}
StringBuilder argumentsForGitDescribe = new StringBuilder();
boolean hasCommitish = evalCommitIsNotHead();
if (hasCommitish) {
argumentsForGitDescribe.append(" " + evaluateOnCommit);
}
if (describeConfig.isAlways()) {
argumentsForGitDescribe.append(" --always");
}
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");
} else {
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
public String getCommitId() throws GitCommitIdExecutionException {
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);
} else {
return runQuietGitCommand(canonical, nativeGitTimeoutInMs, "rev-parse HEAD");
}
}
@Override
public 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
public boolean isDirty() throws GitCommitIdExecutionException {
return !tryCheckEmptyRunGitCommand(canonical, nativeGitTimeoutInMs, "status -s");
}
@Override
public String getCommitAuthorName() throws GitCommitIdExecutionException {
return runQuietGitCommand(canonical, nativeGitTimeoutInMs, "log -1 --pretty=format:%an --no-show-signature " + evaluateOnCommit);
}
@Override
public String getCommitAuthorEmail() throws GitCommitIdExecutionException {
return runQuietGitCommand(canonical, nativeGitTimeoutInMs, "log -1 --pretty=format:%ae --no-show-signature " + evaluateOnCommit);
}
@Override
public String getCommitMessageFull() throws GitCommitIdExecutionException {
return runQuietGitCommand(canonical, nativeGitTimeoutInMs, "log -1 --pretty=format:%B --no-show-signature " + evaluateOnCommit);
}
@Override
public String getCommitMessageShort() throws GitCommitIdExecutionException {
return runQuietGitCommand(canonical, nativeGitTimeoutInMs, "log -1 --pretty=format:%s --no-show-signature " + evaluateOnCommit);
}
@Override
public String getCommitTime() throws GitCommitIdExecutionException {
String value = runQuietGitCommand(canonical, nativeGitTimeoutInMs, "log -1 --pretty=format:%ct --no-show-signature " + evaluateOnCommit);
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);
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);
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);
return result.replace('\n', ',');
}
@Override
public String getTag() throws GitCommitIdExecutionException {
final String result = runQuietGitCommand(canonical, nativeGitTimeoutInMs, "tag --points-at " + evaluateOnCommit);
return result.replace('\n', ',');
}
@Override
public String getRemoteOriginUrl() throws GitCommitIdExecutionException {
return getOriginRemote(canonical, nativeGitTimeoutInMs);
}
@Override
public String getClosestTagName() throws GitCommitIdExecutionException {
try {
StringBuilder argumentsForGitDescribe = new StringBuilder();
argumentsForGitDescribe.append("describe " + evaluateOnCommit + " --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 (NativeCommandException ignore) {
// could not find any tags to describe
}
return "";
}
@Override
public String getClosestTagCommitCount() throws GitCommitIdExecutionException {
String closestTagName = getClosestTagName();
if (closestTagName != null && !closestTagName.trim().isEmpty()) {
return runQuietGitCommand(canonical, nativeGitTimeoutInMs, "rev-list " + closestTagName + ".." + evaluateOnCommit + " --count");
}
return "";
}
@Override
public String getTotalCommitCount() throws GitCommitIdExecutionException {
return runQuietGitCommand(canonical, nativeGitTimeoutInMs, "rev-list " + evaluateOnCommit + " --count");
}
@Override
public void finalCleanUp() throws GitCommitIdExecutionException {
}
private String getOriginRemote(File directory, long nativeGitTimeoutInMs) throws GitCommitIdExecutionException {
try {
String remoteUrl = runGitCommand(directory, nativeGitTimeoutInMs, "ls-remote --get-url");
return stripCredentialsFromOriginUrl(remoteUrl);
} catch (NativeCommandException ignore) {
// No remote configured to list refs from
}
return null;
}
/**
* Runs a Git 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, long nativeGitTimeoutInMs, 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, nativeGitTimeoutInMs, command);
} catch (IOException | GitCommitIdExecutionException ex) {
log.error("Failed to run git command", ex);
// Error means "non-empty"
return false;
// do nothing...
}
}
private String runQuietGitCommand(File directory, long nativeGitTimeoutInMs, String gitCommand) throws GitCommitIdExecutionException {
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, nativeGitTimeoutInMs, command.trim()).trim();
} catch (IOException e) {
throw new GitCommitIdExecutionException(e);
}
}
private String runGitCommand(File directory, long nativeGitTimeoutInMs, String gitCommand) throws GitCommitIdExecutionException {
final String env = System.getenv("GIT_PATH");
String exec = env == null ? "git" : env;
final String command = String.format("%s %s", exec, gitCommand);
try {
return getRunner().run(directory, nativeGitTimeoutInMs, command.trim()).trim();
} catch (NativeCommandException e) {
throw e;
} catch (IOException e) {
throw new GitCommitIdExecutionException(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.
*
* @param directory the directory where the command should be executed in
* @param nativeGitTimeoutInMs the timeout in milliseconds before the command get's terminated
* @param command the command to execute
* @return the output obtained from stdout by running the command
* @throws IOException if any underlying IOError occurred
* @throws GitCommitIdExecutionException the command execution failed
*/
String run(File directory, long nativeGitTimeoutInMs, String command) throws IOException, GitCommitIdExecutionException;
/** Run a command and return false if it contains at least one output line
*
* @param directory the directory where the command should be executed in
* @param nativeGitTimeoutInMs the timeout in milliseconds before the command get's terminated
* @param command the command to execute
* @return false if the output of the command contains at least one line on stdout, true otherwise
* @throws IOException if any underlying IOError occurred
* @throws GitCommitIdExecutionException the command execution failed
*/
boolean runEmpty(File directory, long nativeGitTimeoutInMs, String command) throws IOException, GitCommitIdExecutionException;
}
public static class NativeCommandException extends GitCommitIdExecutionException {
private static final long serialVersionUID = 3511033422542257748L;
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]: directory: `%s`, command: `%s`, stdout: `%s`, stderr: `%s`", exitCode, directory, command, stdout, stderr);
}
}
protected static class JavaProcessRunner implements ProcessRunner {
@Override
public String run(File directory, long nativeGitTimeoutInMs, String command) throws IOException, GitCommitIdExecutionException {
String output = "";
try {
final StringBuilder commandResult = new StringBuilder();
final Consumer stdoutConsumer = line -> {
if (line != null) {
commandResult.append(line).append('\n');
}
};
runProcess(directory, nativeGitTimeoutInMs, command, stdoutConsumer);
output = commandResult.toString();
} catch (final InterruptedException ex) {
throw new IOException(ex);
}
return output;
}
@Override
public boolean runEmpty(File directory, long nativeGitTimeoutInMs, String command) throws IOException, GitCommitIdExecutionException {
final AtomicBoolean empty = new AtomicBoolean(true);
try {
final Consumer stdoutConsumer = line -> {
empty.set(false);
// Ignore the content of the line
};
runProcess(directory, nativeGitTimeoutInMs, command, stdoutConsumer);
} catch (final InterruptedException ex) {
throw new IOException(ex);
}
return empty.get(); // was non-empty
}
private void runProcess(
File directory,
long nativeGitTimeoutInMs,
String command,
final Consumer stdoutLineConsumer) throws InterruptedException, IOException, GitCommitIdExecutionException {
final ProcessBuilder builder = new ProcessBuilder(command.split("\\s"));
final Process proc = builder.directory(directory).start();
try (ProcessHandler processHandler = new ProcessHandler(proc, stdoutLineConsumer)) {
int exitValue = processHandler.exitValue(nativeGitTimeoutInMs, TimeUnit.MILLISECONDS);
if (exitValue != 0) {
throw new NativeCommandException(exitValue, command, directory, "", processHandler.getStderr());
}
} catch (TimeoutException e) {
throw new RuntimeException(String.format("GIT-Command '%s' did not finish in %d milliseconds", command, nativeGitTimeoutInMs), e);
} catch (ExecutionException e) {
throw new RuntimeException(String.format("Executing GIT-Command '%s' threw an '%s' exception.", command, e.getMessage()), e);
}
}
}
@Override
public AheadBehind getAheadBehind() throws GitCommitIdExecutionException {
try {
Optional remoteBranch = remoteBranch();
if (!remoteBranch.isPresent()) {
return AheadBehind.NO_REMOTE;
}
if (!offline) {
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);
return AheadBehind.of(ahead, behind);
} catch (Exception e) {
throw new GitCommitIdExecutionException("Failed to read ahead behind count: " + e.getMessage(), e);
}
}
private Optional remoteBranch() {
try {
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);
return Optional.ofNullable(remoteBranch.isEmpty() ? null : remoteBranch);
} catch (Exception e) {
return Optional.empty();
}
}
private void fetch(String remoteBranch) {
try {
runQuietGitCommand(canonical, nativeGitTimeoutInMs, "fetch " + remoteBranch.replaceFirst("/", " "));
} catch (Exception e) {
log.error("Failed to execute fetch", e);
}
}
// Visible for testing
public void setEvaluateOnCommit(String evaluateOnCommit) {
this.evaluateOnCommit = evaluateOnCommit;
}
}