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

pl.project13.core.jgit.JGitCommon 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.jgit;

import java.io.IOException;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.Status;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTag;
import org.eclipse.jgit.revwalk.RevWalk;

import pl.project13.core.jgit.dummy.DatedRevTag;

import pl.project13.core.git.GitDescribeConfig;
import pl.project13.core.log.LogInterface;
import pl.project13.core.util.Pair;

import javax.annotation.Nonnull;

public class JGitCommon {

  private final LogInterface log;

  public JGitCommon(LogInterface log) {
    this.log = log;
  }

  public Collection getTags(Repository repo, final ObjectId objectId) throws GitAPIException {
    try (Git git = Git.wrap(repo)) {
      try (RevWalk walk = new RevWalk(repo)) {
        Collection tags = getTags(git, objectId, walk);
        walk.dispose();
        return tags;
      }
    }
  }

  private Collection getTags(final Git git, final ObjectId objectId, final RevWalk finalWalk) throws GitAPIException {
    return git.tagList().call()
            .stream()
            .filter(tagRef -> {
              try {
                final RevCommit tagCommit = finalWalk.parseCommit(tagRef.getObjectId());
                final RevCommit objectCommit = finalWalk.parseCommit(objectId);
                if (finalWalk.isMergedInto(objectCommit, tagCommit)) {
                  return true;
                }
              } catch (Exception ignored) {
                log.debug(String.format("Failed while getTags [%s] -- ", tagRef));
              }
              return false;
            })
            .map(tagRef -> trimFullTagName(tagRef.getName()))
            .collect(Collectors.toList());
  }

  public Collection getTag(Repository repo, final ObjectId objectId) throws GitAPIException {
    try (Git git = Git.wrap(repo)) {
      Collection tags = getTagsOnObjectId(git, objectId);
      return tags;
    }
  }

  private Collection getTagsOnObjectId(final Git git, final ObjectId objectId) throws GitAPIException {
    return git.tagList().call()
            .stream()
            .filter(tagRef -> {
              try {
                if (objectId.name().equals(tagRef.getObjectId().name())) {
                  return true;
                }
              } catch (Exception ignored) {
                log.debug(String.format("Failed while getTagOnObjectId [%s] -- ", tagRef));
              }
              return false;
            })
            .map(tagRef -> trimFullTagName(tagRef.getName()))
            .collect(Collectors.toList());
  }

  public String getClosestTagName(@Nonnull String evaluateOnCommit, @Nonnull Repository repo, GitDescribeConfig gitDescribe) {
    // TODO: Why does some tests fail when it gets headCommit from JGitprovider?
    RevCommit headCommit = findEvalCommitObjectId(evaluateOnCommit, repo);
    Pair pair = getClosestRevCommit(repo, headCommit, gitDescribe);
    return pair.second;
  }

  public String getClosestTagCommitCount(@Nonnull String evaluateOnCommit, @Nonnull Repository repo, GitDescribeConfig gitDescribe) {
    // TODO: Why does some tests fail when it gets headCommit from JGitprovider?
    RevCommit headCommit = findEvalCommitObjectId(evaluateOnCommit, repo);
    Pair pair = getClosestRevCommit(repo, headCommit, gitDescribe);
    RevCommit revCommit = pair.first;
    int distance = distanceBetween(repo, headCommit, revCommit);
    return String.valueOf(distance);
  }

  private Pair getClosestRevCommit(@Nonnull Repository repo, RevCommit headCommit, GitDescribeConfig gitDescribe) {
    boolean includeLightweightTags = false;
    String matchPattern = ".*";
    if (gitDescribe != null) {
      includeLightweightTags = gitDescribe.getTags();
      if (!"*".equals(gitDescribe.getMatch())) {
        matchPattern = createMatchPattern(gitDescribe.getMatch());
      }
    }
    Map> tagObjectIdToName = findTagObjectIds(repo, includeLightweightTags, matchPattern);
    if (tagObjectIdToName.containsKey(headCommit)) {
      String tagName = tagObjectIdToName.get(headCommit).iterator().next();
      return Pair.of(headCommit, tagName);
    }
    List commits = findCommitsUntilSomeTag(repo, headCommit, tagObjectIdToName);
    RevCommit revCommit = commits.get(0);
    String tagName = tagObjectIdToName.get(revCommit).iterator().next();

    return Pair.of(revCommit, tagName);
  }

  protected String createMatchPattern(String pattern) {
    return "^refs/tags/\\Q" +
            pattern.replace("*", "\\E.*\\Q").replace("?", "\\E.\\Q") +
            "\\E$";
  }

  protected Map> findTagObjectIds(@Nonnull Repository repo, boolean includeLightweightTags, String matchPattern) {
    Map> commitIdsToTags = getCommitIdsToTags(repo, includeLightweightTags, matchPattern);
    Map> commitIdsToTagNames = transformRevTagsMapToDateSortedTagNames(commitIdsToTags);
    log.debug(String.format("Created map: [%s]", commitIdsToTagNames));

    return commitIdsToTagNames;
  }

  protected RevCommit findEvalCommitObjectId(@Nonnull String evaluateOnCommit, @Nonnull Repository repo) throws RuntimeException {
    try {
      ObjectId evalCommitId = repo.resolve(evaluateOnCommit);

      try (RevWalk walk = new RevWalk(repo)) {
        RevCommit evalCommit = walk.parseCommit(evalCommitId);
        walk.dispose();

        log.debug(String.format("evalCommit is [%s]", evalCommit.getName()));
        return evalCommit;
      }
    } catch (IOException ex) {
      throw new RuntimeException("Unable to obtain " + evaluateOnCommit + " commit!", ex);
    }
  }

  protected Map> getCommitIdsToTags(@Nonnull Repository repo, boolean includeLightweightTags, String matchPattern) {
    Map> commitIdsToTags = new HashMap<>();

    try (RevWalk walk = new RevWalk(repo)) {
      walk.markStart(walk.parseCommit(repo.resolve("HEAD")));

      List tagRefs = Git.wrap(repo).tagList().call();
      Pattern regex = Pattern.compile(matchPattern);
      log.debug(String.format("Tag refs [%s]", tagRefs));

      for (Ref tagRef : tagRefs) {
        walk.reset();
        String name = tagRef.getName();
        if (!regex.matcher(name).matches()) {
          log.info(String.format("Skipping tagRef with name [%s] as it doesn't match [%s]", name, matchPattern));
          continue;
        }
        ObjectId resolvedCommitId = repo.resolve(name);

        // TODO that's a bit of a hack...
        try {
          final RevTag revTag = walk.parseTag(resolvedCommitId);
          ObjectId taggedCommitId = revTag.getObject().getId();
          log.debug(String.format("Resolved tag [%s] [%s], points at [%s] ", revTag.getTagName(), revTag.getTaggerIdent(), taggedCommitId));

          // sometimes a tag, may point to another tag, so we need to unpack it
          while (isTagId(taggedCommitId)) {
            taggedCommitId = walk.parseTag(taggedCommitId).getObject().getId();
          }

          if (commitIdsToTags.containsKey(taggedCommitId)) {
            commitIdsToTags.get(taggedCommitId).add(new DatedRevTag(revTag));
          } else {
            commitIdsToTags.put(taggedCommitId, new ArrayList<>(Collections.singletonList(new DatedRevTag(revTag))));
          }

        } catch (IncorrectObjectTypeException ex) {
          // it's an lightweight tag! (yeah, really)
          if (includeLightweightTags) {
            // --tags means "include lightweight tags"
            log.debug(String.format("Including lightweight tag [%s]", name));

            DatedRevTag datedRevTag = new DatedRevTag(resolvedCommitId, name);

            if (commitIdsToTags.containsKey(resolvedCommitId)) {
              commitIdsToTags.get(resolvedCommitId).add(datedRevTag);
            } else {
              commitIdsToTags.put(resolvedCommitId, new ArrayList<>(Collections.singletonList(datedRevTag)));
            }
          }
        } catch (Exception ignored) {
          log.info(String.format("Failed while parsing [%s] -- ", tagRef));
        }
      }

      for (Map.Entry> entry : commitIdsToTags.entrySet()) {
        log.debug(String.format("key [%s], tags => [%s] ", entry.getKey(), entry.getValue()));
      }
      return commitIdsToTags;
    } catch (Exception e) {
      log.error("Unable to locate tags", e);
    }
    return Collections.emptyMap();
  }

  /** Checks if the given object id resolved to a tag object */
  private boolean isTagId(ObjectId objectId) {
    return objectId.toString().startsWith("tag ");
  }

  protected HashMap> transformRevTagsMapToDateSortedTagNames(Map> commitIdsToTags) {
    HashMap> commitIdsToTagNames = new HashMap<>();
    for (Map.Entry> objectIdListEntry : commitIdsToTags.entrySet()) {
      List tagNames = transformRevTagsMapEntryToDateSortedTagNames(objectIdListEntry);

      commitIdsToTagNames.put(objectIdListEntry.getKey(), tagNames);
    }
    return commitIdsToTagNames;
  }

  private List transformRevTagsMapEntryToDateSortedTagNames(Map.Entry> objectIdListEntry) {
    return objectIdListEntry.getValue()
            .stream()
            .sorted(datedRevTagComparator())
            .map(datedRevTag -> trimFullTagName(datedRevTag.tagName))
            .collect(Collectors.toList());
  }

  private Comparator datedRevTagComparator() {
    return (revTag, revTag2) -> revTag2.date.compareTo(revTag.date);
  }

  // Visible for testing
  protected String trimFullTagName(@Nonnull String tagName) {
    return tagName.replaceFirst("refs/tags/", "");
  }

  public List findCommitsUntilSomeTag(Repository repo, RevCommit head, @Nonnull Map> tagObjectIdToName) {
    try (RevWalk revWalk = new RevWalk(repo)) {
      revWalk.markStart(head);

      for (RevCommit commit : revWalk) {
        ObjectId objId = commit.getId();
        if (tagObjectIdToName.size() > 0) {
          List maybeList = tagObjectIdToName.get(objId);
          if (maybeList != null && maybeList.get(0) != null) {
            return Collections.singletonList(commit);
          }
        }
      }

      return Collections.emptyList();
    } catch (Exception e) {
      throw new RuntimeException("Unable to find commits until some tag", e);
    }
  }

  /**
   * Calculates the distance (number of commits) between the given parent and child commits.
   * @param repo the {@link Repository} this command should interact with
   * @param child the child commit (starting point)
   * @param parent the parent commit (end point)
   * @return distance (number of commits) between the given commits
   * @see mdonoughe/jgit-describe/blob/master/src/org/mdonoughe/JGitDescribeTask.java
   */
  protected int distanceBetween(@Nonnull Repository repo, @Nonnull RevCommit child, @Nonnull RevCommit parent) {
    try (RevWalk revWalk = new RevWalk(repo)) {
      revWalk.markStart(child);

      Set seena = new HashSet<>();
      Set seenb = new HashSet<>();
      Queue q = new ArrayDeque<>();

      q.add(revWalk.parseCommit(child));
      int distance = 0;
      ObjectId parentId = parent.getId();

      while (q.size() > 0) {
        RevCommit commit = q.remove();
        ObjectId commitId = commit.getId();

        if (seena.contains(commitId)) {
          continue;
        }
        seena.add(commitId);

        if (parentId.equals(commitId)) {
          // don't consider commits that are included in this commit
          seeAllParents(revWalk, commit, seenb);
          // remove things we shouldn't have included
          for (ObjectId oid : seenb) {
            if (seena.contains(oid)) {
              distance--;
            }
          }
          seena.addAll(seenb);
          continue;
        }

        for (ObjectId oid : commit.getParents()) {
          if (!seena.contains(oid)) {
            q.add(revWalk.parseCommit(oid));
          }
        }
        distance++;
      }
      return distance;
    } catch (Exception e) {
      throw new RuntimeException(String.format("Unable to calculate distance between [%s] and [%s]", child, parent), e);
    }
  }

  private void seeAllParents(@Nonnull RevWalk revWalk, RevCommit child, @Nonnull Set seen) throws IOException {
    Queue q = new ArrayDeque<>();
    q.add(child);

    while (q.size() > 0) {
      RevCommit commit = q.remove();
      for (ObjectId oid : commit.getParents()) {
        if (seen.contains(oid)) {
          continue;
        }
        seen.add(oid);
        q.add(revWalk.parseCommit(oid));
      }
    }
  }

  public static boolean isRepositoryInDirtyState(Repository repo) throws GitAPIException {
    Git git = Git.wrap(repo);
    Status status = git.status().call();

    // Git describe doesn't mind about untracked files when checking if
    // repo is dirty. JGit does this, so we cannot use the isClean method
    // to get the same behaviour. Instead check dirty state without
    // status.getUntracked().isEmpty()
    return !(status.getAdded().isEmpty()
        && status.getChanged().isEmpty()
        && status.getRemoved().isEmpty()
        && status.getMissing().isEmpty()
        && status.getModified().isEmpty()
        && status.getConflicting().isEmpty());
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy