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());
}
}