All Downloads are FREE. Search and download functionalities are using the official Maven repository.

pl.project13.maven.git.NativeGitProvider Maven / Gradle / Ivy

Go to download

This plugin makes basic repository information available through maven resources. This can be used to display "what version is this?" or "who has deployed this and when, from which branch?" information at runtime, making it easy to find things like "oh, that isn't deployed yet, I'll test it tomorrow" and making both testers and developers life easier. See https://github.com/git-commit-id/maven-git-commit-id-plugin

There is a newer version: 4.9.10
Show newest version
/*
 * This file is part of git-commit-id-plugin by Konrad 'ktoso' 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 java.lang.String.format;

import pl.project13.maven.git.log.LoggerBridge;

import javax.annotation.Nonnull;

import com.google.common.annotations.VisibleForTesting;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Optional;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
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 LoggerBridge log) {
    return new NativeGitProvider(dotGitDirectory, nativeGitTimeoutInMs, log);
  }

  NativeGitProvider(@Nonnull File dotGitDirectory, long nativeGitTimeoutInMs, @Nonnull LoggerBridge 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 RuntimeException(e);
        }
      } else {
        throw new RuntimeException(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 " + evaluateOnCommit);
  }

  @Override
  public String getCommitAuthorEmail() throws GitCommitIdExecutionException {
    return runQuietGitCommand(canonical, nativeGitTimeoutInMs, "log -1 --pretty=format:%ae " + evaluateOnCommit);
  }

  @Override
  public String getCommitMessageFull() throws GitCommitIdExecutionException {
    return runQuietGitCommand(canonical, nativeGitTimeoutInMs, "log -1 --pretty=format:%B " + evaluateOnCommit);
  }

  @Override
  public String getCommitMessageShort() throws GitCommitIdExecutionException {
    return runQuietGitCommand(canonical, nativeGitTimeoutInMs, "log -1 --pretty=format:%s " + evaluateOnCommit);
  }

  @Override
  public String getCommitTime() throws GitCommitIdExecutionException {
    String value =  runQuietGitCommand(canonical, nativeGitTimeoutInMs, "log -1 --pretty=format:%ct " + 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 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 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, 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 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) {
    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 RuntimeException(e);
    }
  }

  private String runGitCommand(File directory, long nativeGitTimeoutInMs, String gitCommand) throws NativeCommandException {
    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 RuntimeException(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, long nativeGitTimeoutInMs, String command) throws IOException;

    /** Run a command and return false if it contains at least one output line*/
    boolean runEmpty(File directory, long nativeGitTimeoutInMs, String command) throws IOException;
  }

  public static class NativeCommandException extends IOException {
    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 {
      String output = "";
      try {
        final StringBuilder commandResult = new StringBuilder();

        final Function stdoutConsumer = line -> {
          if (line != null) {
            commandResult.append(line).append("\n");
          }
          // return true to indicate we want to read more content
          return true;
        };
        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 {
      final AtomicBoolean empty = new AtomicBoolean(true);

      try {
        final Function stdoutConsumer = line -> {
          if (line != null) {
            empty.set(false);
          }
          // return false to indicate we don't need to read more content
          return false;
        };
        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 Function stdoutConsumer) throws InterruptedException, IOException {

      final ProcessBuilder builder = new ProcessBuilder(command.split("\\s"));
      final Process proc = builder.directory(directory).start();

      final ExecutorService executorService = Executors.newFixedThreadPool(2);
      final StringBuilder errMsg = new StringBuilder();

      final Future> stdoutFuture = executorService.submit(
              new CallableBufferedStreamReader(proc.getInputStream(), stdoutConsumer));
      final Future> stderrFuture = executorService.submit(
              new CallableBufferedStreamReader(proc.getErrorStream(),
                  line -> {
                    errMsg.append(line);
                    // return true to indicate we want to read more content
                    return true;
                  }));

      if (!proc.waitFor(nativeGitTimeoutInMs, TimeUnit.MILLISECONDS)) {
        proc.destroy();
        executorService.shutdownNow();
        throw new RuntimeException(String.format("GIT-Command '%s' did not finish in %d milliseconds", command, nativeGitTimeoutInMs));
      }

      try {
        stdoutFuture.get()
            .ifPresent(e -> {
              throw e;
            });
        stderrFuture.get()
            .ifPresent(e -> {
              throw e;
            });
      } catch (final ExecutionException e) {
        throw new RuntimeException(String.format("Executing GIT-Command '%s' threw an '%s' exception.", command, e.getMessage()), e);
      }

      executorService.shutdown();
      if (proc.exitValue() != 0) {
        throw new NativeCommandException(proc.exitValue(), command, directory, "", errMsg.toString());
      }
    }

    private static class CallableBufferedStreamReader implements Callable> {
      private final InputStream is;
      private final Function streamConsumer;

      CallableBufferedStreamReader(final InputStream is, final Function streamConsumer) {
        this.is = is;
        this.streamConsumer = streamConsumer;
      }

      @Override
      public Optional call() {
        RuntimeException thrownException = null;
        try (final BufferedReader br = new BufferedReader(
                new InputStreamReader(is, StandardCharsets.UTF_8))) {
          for (String line = br.readLine();
               line != null;
               line = br.readLine()) {
            if (!streamConsumer.apply(line)) {
              break;
            }
          }
        } catch (final IOException e) {
          thrownException = new RuntimeException(String.format("Executing GIT-Command threw an '%s' exception.", e.getMessage()), e);
        }

        return Optional.ofNullable(thrownException);
      }
    }
  }

  @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);
    }
  }
  
  @VisibleForTesting
  public void setEvaluateOnCommit(String evaluateOnCommit) {
    this.evaluateOnCommit = evaluateOnCommit;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy