
pl.project13.jgit.DescribeCommand Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of git-commit-id-plugin Show documentation
Show all versions of git-commit-id-plugin Show documentation
git-commit-id-plugin is a plugin quite similar to
https://fisheye.codehaus.org/browse/mojo/tags/buildnumber-maven-plugin-1.0-beta-4 for example but as buildnumber
only supports svn (which is very sad) and cvs (which is even more sad).
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.
The data currently exported is like this (that's the end effect from the GitRepositoryState Bean):
{
"branch" : "testing-maven-git-plugin",
"commitTime" : "06.01.1970 @ 16:16:26 CET",
"commitId" : "787e39f61f99110e74deed68ab9093088d64b969",
"commitUserName" : "Konrad Malawski",
"commitUserEmail" : "[email protected]",
"commitMessageFull" : "releasing my fun plugin :-) + fixed some typos + cleaned up directory structure + added
license etc",
"commitMessageShort" : "releasing my fun plugin :-)",
"buildTime" : "06.01.1970 @ 16:17:53 CET",
"buildUserName" : "Konrad Malawski",
"buildUserEmail" : "[email protected]"
}
Note that the data is exported via maven resource filtering and is really easy to use with spring -
which I've explained in detail in this readme https://github.com/ktoso/maven-git-commit-id-plugin
The newest version!
/*
* This file is part of git-commit-id-plugin by Konrad 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.jgit;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.GitCommand;
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.ObjectReader;
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 org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import pl.project13.jgit.dummy.DatedRevTag;
import pl.project13.maven.git.GitDescribeConfig;
import pl.project13.maven.git.log.LoggerBridge;
import pl.project13.maven.git.log.StdOutLoggerBridge;
import pl.project13.maven.git.util.Pair;
import java.io.IOException;
import java.util.*;
import java.util.regex.Pattern;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Lists.newLinkedList;
import static com.google.common.collect.Maps.newHashMap;
import static com.google.common.collect.Sets.newHashSet;
/**
* Implements git's describe
command.
*
*
* usage: git describe [options] *
* or: git describe [options] --dirty
*
* --contains find the tag that comes after the commit
* --debug debug search strategy on stderr
* --all use any ref in .git/refs
* --tags use any tag in .git/refs/tags
* --long always use long format
* --abbrev[=] use digits to display SHA-1s
* --exact-match only output exact matches
* --candidates consider most recent tags (default: 10)
* --match only consider tags matching
* --always show abbreviated commit object as fallback
* --dirty[=] append on dirty working tree (default: "-dirty")
*
*
* @author Konrad 'ktoso' Malawski
*/
public class DescribeCommand extends GitCommand {
private LoggerBridge loggerBridge;
// todo not yet implemented options:
// private boolean containsFlag = false;
// private boolean allFlag = false;
// private boolean tagsFlag = false;
// private Optional candidatesOption = Optional.of(10);
// private boolean exactMatchFlag = false;
private Optional matchOption = Optional.absent();
/**
* How many chars of the commit hash should be displayed? 7 is the default used by git.
*/
private int abbrev = 7;
/**
* Skipping lightweight tags by default - that's how git-describe works by default.
* {@link DescribeCommand#tags(Boolean)} for more details.
*/
private boolean tagsFlag = false;
private boolean alwaysFlag = true;
/**
* Corresponds to --long
. Always use the TAG-N-HASH
format, even when ON a tag.
*/
private boolean forceLongFormat = false;
/**
* The string marker (such as "DEV") to be suffixed to the describe result when the working directory is dirty
*/
private Optional dirtyOption = Optional.absent();
/**
* Creates a new describe command which interacts with a single repository
*
* @param repo the {@link org.eclipse.jgit.lib.Repository} this command should interact with
*/
@NotNull
public static DescribeCommand on(Repository repo) {
return new DescribeCommand(repo);
}
/**
* Creates a new describe command which interacts with a single repository
*
* @param repo the {@link org.eclipse.jgit.lib.Repository} this command should interact with
*/
private DescribeCommand(Repository repo) {
this(repo, true);
}
private DescribeCommand(Repository repo, boolean verbose) {
super(repo);
initDefaultLoggerBridge(verbose);
setVerbose(verbose);
}
private void initDefaultLoggerBridge(boolean verbose) {
loggerBridge = new StdOutLoggerBridge(verbose);
}
@NotNull
public DescribeCommand setVerbose(boolean verbose) {
loggerBridge.setVerbose(verbose);
return this;
}
@NotNull
public DescribeCommand withLoggerBridge(LoggerBridge bridge) {
this.loggerBridge = bridge;
return this;
}
/**
* --always
*
* Show uniquely abbreviated commit object as fallback.
*
* true
by default.
*/
@NotNull
public DescribeCommand always(boolean always) {
this.alwaysFlag = always;
log("--always =", always);
return this;
}
/**
* --long
*
* Always output the long format (the tag, the number of commits and the abbreviated commit name)
* even when it matches a tag. This is useful when you want to see parts of the commit object name
* in "describe" output, even when the commit in question happens to be a tagged version. Instead
* of just emitting the tag name, it will describe such a commit as v1.2-0-gdeadbee (0th commit
* since tag v1.2 that points at object deadbee....).
*
* false
by default.
*/
@NotNull
public DescribeCommand forceLongFormat(@Nullable Boolean forceLongFormat) {
if (forceLongFormat != null) {
this.forceLongFormat = forceLongFormat;
log("--long =", forceLongFormat);
}
return this;
}
/**
* --abbrev=N
*
* Instead of using the default 7 hexadecimal digits as the abbreviated object name,
* use N digits, or as many digits as needed to form a unique object name.
*
* An of 0 will suppress long format, only showing the closest tag.
*/
@NotNull
public DescribeCommand abbrev(@Nullable Integer n) {
if (n != null) {
Preconditions.checkArgument(n < 41, String.format("N (commit abbres length) must be < 41. (Was:[%s])", n));
Preconditions.checkArgument(n >= 0, String.format("N (commit abbrev length) must be positive! (Was [%s])", n));
log("--abbrev =", n);
abbrev = n;
}
return this;
}
/**
* --tags
*
* Instead of using only the annotated tags, use any tag found in .git/refs/tags.
* This option enables matching a lightweight (non-annotated) tag.
*
*
* Searching for lightweight tags is false by default.
*
* Example:
*
* b6a73ed - (HEAD, master)
* d37a598 - (v1.0-fixed-stuff) - a lightweight tag (with no message)
* 9597545 - (v1.0) - an annotated tag
*
* > git describe
* annotated-tag-2-gb6a73ed # the nearest "annotated" tag is found
*
* > git describe --tags
* lightweight-tag-1-gb6a73ed # the nearest tag (including lightweights) is found
*
*
*
* Using only annotated tags to mark builds may be useful if you're using tags to help yourself with annotating
* things like "i'll get back to that" etc - you don't need such tags to be exposed. But if you want lightweight
* tags to be included in the search, enable this option.
*
*/
@NotNull
public DescribeCommand tags(@Nullable Boolean includeLightweightTagsInSearch) {
if (includeLightweightTagsInSearch != null) {
tagsFlag = includeLightweightTagsInSearch;
log("--tags =", includeLightweightTagsInSearch);
}
return this;
}
/**
* Alias for {@link DescribeCommand#tags(Boolean)} with true value
*/
public DescribeCommand tags() {
return tags(true);
}
/**
* Apply all configuration options passed in with {@param config}.
* If a setting is null, it will not be applied - so for abbrev for example, the default 7 would be used.
*
* @return itself, after applying the settings
*/
@NotNull
public DescribeCommand apply(@Nullable GitDescribeConfig config) {
if (config != null) {
always(config.isAlways());
dirty(config.getDirty());
abbrev(config.getAbbrev());
forceLongFormat(config.getForceLongFormat());
tags(config.getTags());
match(config.getMatch());
}
return this;
}
/**
* --dirty[=mark]
* Describe the working tree. It means describe HEAD and appends mark (-dirty
by default) if the
* working tree is dirty.
*
* @param dirtyMarker the marker name to be appended to the describe output when the workspace is dirty
* @return itself, to allow fluent configuration
*/
@NotNull
public DescribeCommand dirty(@Nullable String dirtyMarker) {
Optional option = Optional.fromNullable(dirtyMarker);
log("--dirty =", option.or(""));
this.dirtyOption = option;
return this;
}
/**
* --match glob-pattern
* Consider only those tags which match the given glob pattern.
*
* @param pattern the glob style pattern to match against the tag names
* @return itself, to allow fluent configuration
*/
@NotNull
public DescribeCommand match(@Nullable String pattern) {
matchOption = Optional.fromNullable(pattern);
log("--match =", matchOption.or(""));
return this;
}
@Override
public DescribeResult call() throws GitAPIException {
// needed for abbrev id's calculation
ObjectReader objectReader = repo.newObjectReader();
// get tags
Map> tagObjectIdToName = findTagObjectIds(repo, tagsFlag);
// get current commit
RevCommit headCommit = findHeadObjectId(repo);
ObjectId headCommitId = headCommit.getId();
// check if dirty
boolean dirty = findDirtyState(repo);
if (hasTags(headCommit, tagObjectIdToName) && !forceLongFormat) {
String tagName = tagObjectIdToName.get(headCommit).iterator().next();
log("The commit we're on is a Tag ([",tagName,"]) and forceLongFormat == false, returning.");
return new DescribeResult(tagName, dirty, dirtyOption);
}
// get commits, up until the nearest tag
List commits = findCommitsUntilSomeTag(repo, headCommit, tagObjectIdToName);
// if there is no tags or any tag is not on that branch then return generic describe
if (foundZeroTags(tagObjectIdToName) || commits.isEmpty()) {
return new DescribeResult(objectReader, headCommitId, dirty, dirtyOption)
.withCommitIdAbbrev(abbrev);
}
// check how far away from a tag we are
int distance = distanceBetween(repo, headCommit, commits.get(0));
String tagName = tagObjectIdToName.get(commits.get(0)).iterator().next();
Pair howFarFromWhichTag = Pair.of(distance, tagName);
// if it's null, no tag's were found etc, so let's return just the commit-id
return createDescribeResult(objectReader, headCommitId, dirty, howFarFromWhichTag);
}
/**
* Prepares the final result of this command.
* It tries to put as much information as possible into the result,
* and will fallback to a plain commit hash if nothing better is returnable.
*
* The exact logic is following what git-describe
would do.
*/
private DescribeResult createDescribeResult(ObjectReader objectReader, ObjectId headCommitId, boolean dirty, @Nullable Pair howFarFromWhichTag) {
if (howFarFromWhichTag == null) {
return new DescribeResult(objectReader, headCommitId, dirty, dirtyOption)
.withCommitIdAbbrev(abbrev);
} else if (howFarFromWhichTag.first > 0 || forceLongFormat) {
return new DescribeResult(objectReader, howFarFromWhichTag.second, howFarFromWhichTag.first, headCommitId, dirty, dirtyOption, forceLongFormat)
.withCommitIdAbbrev(abbrev); // we're a bit away from a tag
} else if (howFarFromWhichTag.first == 0) {
return new DescribeResult(howFarFromWhichTag.second)
.withCommitIdAbbrev(abbrev); // we're ON a tag
} else if (alwaysFlag) {
return new DescribeResult(objectReader, headCommitId)
.withCommitIdAbbrev(abbrev); // we have no tags! display the commit
} else {
return DescribeResult.EMPTY;
}
}
private static boolean foundZeroTags(@NotNull Map> tags) {
return tags.isEmpty();
}
@VisibleForTesting
boolean findDirtyState(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()
boolean isDirty = !(status.getAdded().isEmpty()
&& status.getChanged().isEmpty()
&& status.getRemoved().isEmpty()
&& status.getMissing().isEmpty()
&& status.getModified().isEmpty()
&& status.getConflicting().isEmpty());
log("Repo is in dirty state [", isDirty, "]");
return isDirty;
}
@VisibleForTesting
static boolean hasTags(ObjectId headCommit, @NotNull Map> tagObjectIdToName) {
return tagObjectIdToName.containsKey(headCommit);
}
RevCommit findHeadObjectId(@NotNull Repository repo) throws RuntimeException {
try {
ObjectId headId = repo.resolve("HEAD");
RevWalk walk = new RevWalk(repo);
RevCommit headCommit = walk.lookupCommit(headId);
walk.dispose();
log("HEAD is [",headCommit.getName(),"] ");
return headCommit;
} catch (IOException ex) {
throw new RuntimeException("Unable to obtain HEAD commit!", ex);
}
}
private List findCommitsUntilSomeTag(Repository repo, RevCommit head, @NotNull Map> tagObjectIdToName) {
RevWalk revWalk = new RevWalk(repo);
try {
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.
*
* @return distance (number of commits) between the given commits
* @see mdonoughe/jgit-describe/blob/master/src/org/mdonoughe/JGitDescribeTask.java
*/
private int distanceBetween(@NotNull Repository repo, @NotNull RevCommit child, @NotNull RevCommit parent) {
RevWalk revWalk = new RevWalk(repo);
try {
revWalk.markStart(child);
Set seena = newHashSet();
Set seenb = newHashSet();
Queue q = newLinkedList();
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);
} finally {
revWalk.dispose();
}
}
private static void seeAllParents(@NotNull RevWalk revWalk, RevCommit child, @NotNull Set seen) throws IOException {
Queue q = newLinkedList();
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));
}
}
}
// git commit id -> its tag (or tags)
private Map> findTagObjectIds(@NotNull Repository repo, boolean tagsFlag) {
Map> commitIdsToTags = newHashMap();
RevWalk walk = new RevWalk(repo);
try {
walk.markStart(walk.parseCommit(repo.resolve("HEAD")));
List tagRefs = Git.wrap(repo).tagList().call();
String matchPattern = createMatchPattern();
Pattern regex = Pattern.compile(matchPattern);
log("Tag refs [", tagRefs, "]");
for (Ref tagRef : tagRefs) {
walk.reset();
String name = tagRef.getName();
if (!regex.matcher(name).matches()) {
log("Skipping tagRef with name [", name, "] as it doesn't match [", 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("Resolved tag [",revTag.getTagName(),"] [",revTag.getTaggerIdent(),"], points at [",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, newArrayList(new DatedRevTag(revTag)));
}
} catch (IncorrectObjectTypeException ex) {
// it's an lightweight tag! (yeah, really)
if (tagsFlag) {
// --tags means "include lightweight tags"
log("Including lightweight tag [", name, "]");
DatedRevTag datedRevTag = new DatedRevTag(resolvedCommitId, name);
if (commitIdsToTags.containsKey(resolvedCommitId)) {
commitIdsToTags.get(resolvedCommitId).add(datedRevTag);
} else {
commitIdsToTags.put(resolvedCommitId, newArrayList(datedRevTag));
}
}
} catch (Exception ignored) {
error("Failed while parsing [",tagRef,"] -- ", Throwables.getStackTraceAsString(ignored));
}
}
for (Map.Entry> entry : commitIdsToTags.entrySet()) {
log("key [",entry.getKey(),"], tags => [",entry.getValue(),"] ");
}
Map> commitIdsToTagNames = transformRevTagsMapToDateSortedTagNames(commitIdsToTags);
log("Created map: [",commitIdsToTagNames,"] ");
return commitIdsToTagNames;
} catch (Exception e) {
log("Unable to locate tags\n[",Throwables.getStackTraceAsString(e),"]");
} finally {
walk.release();
}
return Collections.emptyMap();
}
/** Checks if the given object id resolved to a tag object */
private boolean isTagId(ObjectId objectId) {
return objectId.toString().startsWith("tag ");
}
private HashMap> transformRevTagsMapToDateSortedTagNames(Map> commitIdsToTags) {
HashMap> commitIdsToTagNames = newHashMap();
for (Map.Entry> objectIdListEntry : commitIdsToTags.entrySet()) {
List tags = objectIdListEntry.getValue();
List newTags = newArrayList(tags);
Collections.sort(newTags, new Comparator() {
@Override
public int compare(DatedRevTag revTag, DatedRevTag revTag2) {
return revTag2.date.compareTo(revTag.date);
}
});
List tagNames = Lists.transform(newTags, new Function() {
@Override
public String apply(DatedRevTag input) {
return trimFullTagName(input.tagName);
}
});
commitIdsToTagNames.put(objectIdListEntry.getKey(), tagNames);
}
return commitIdsToTagNames;
}
private String createMatchPattern() {
if (!matchOption.isPresent()) {
return ".*";
}
StringBuffer buf = new StringBuffer();
buf.append("^refs/tags/\\Q");
buf.append(matchOption.get().replace("*", "\\E.*\\Q").replace("?", "\\E.\\Q"));
buf.append("\\E$");
return buf.toString();
}
@VisibleForTesting
static String trimFullTagName(@NotNull String tagName) {
return tagName.replaceFirst("refs/tags/", "");
}
private void log(Object... parts) {
loggerBridge.log(parts);
}
private void error(Object... parts) {
loggerBridge.error(parts);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy