Skip to content

Commit

Permalink
Native vs JGit 2 (#89)
Browse files Browse the repository at this point in the history
* native vs jgit

* jgit

* fix jgit

* remove safe git

* refactor

* dual impl

* comparator

* address comments

* readme

* add debug logs

* Smaller scope variables

* return jgit

* map to single ref
  • Loading branch information
delta003 authored and iamdanfox committed Apr 19, 2018
1 parent 61c0f7d commit 5830663
Show file tree
Hide file tree
Showing 9 changed files with 444 additions and 63 deletions.
6 changes: 3 additions & 3 deletions readme.md
Expand Up @@ -5,9 +5,9 @@ Git-Version Gradle Plugin

When applied, Git-Version adds two methods to the target project.

The first, called `gitVersion()`, runs `git describe` to determine a version string.
It behaves exactly as `git describe` method behaves, except that when the repository is in a dirty
state, appends `.dirty` to the version string.
The first, called `gitVersion()`, mimics `git describe --tags --always --first-parent` to determine a version string.
It behaves exactly as `git describe --tags --always --first-parent` method behaves, except that when the repository is
in a dirty state, appends `.dirty` to the version string.

The second, called `versionDetails()`, returns an object containing the specific details of the version string:
the tag name, the commit count since the tag, the current commit hash of HEAD, and an optional branch name of HEAD.
Expand Down
10 changes: 10 additions & 0 deletions src/main/groovy/com/palantir/gradle/gitversion/GitDescribe.groovy
@@ -0,0 +1,10 @@
package com.palantir.gradle.gitversion

interface GitDescribe {

/**
* Mimics behaviour of 'git describe --tags --always --first-parent --match=${prefix}*'
* Method returns null if repository is empty.
*/
String describe(String prefix)
}
35 changes: 35 additions & 0 deletions src/main/groovy/com/palantir/gradle/gitversion/GitUtils.groovy
@@ -0,0 +1,35 @@
package com.palantir.gradle.gitversion

import org.eclipse.jgit.api.DescribeCommand
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.lib.Ref

class GitUtils {

static final int SHA_ABBR_LENGTH = 7

static String abbrevHash(String s) {
return s.substring(0, SHA_ABBR_LENGTH)
}

static boolean isRepoEmpty(Git git) {
// back-compat: the JGit "describe" command throws an exception in repositories with no commits, so call it
// first to preserve this behavior in cases where this call would fail but native "git" call does not.
try {
new DescribeCommand(git.getRepository()).call()
return true
} catch (Exception ignored) {
return false
}
}

// getPeeledObjectId returns:
// "if this ref is an annotated tag the id of the commit (or tree or blob) that the annotated tag refers to;
// null if this ref does not refer to an annotated tag."
// We use this to check if tag is annotated.
static boolean isAnnotatedTag(Ref ref) {
ObjectId peeledObjectId = ref.getPeeledObjectId()
return peeledObjectId != null
}
}
Expand Up @@ -15,11 +15,7 @@
*/
package com.palantir.gradle.gitversion

import com.google.common.base.Preconditions
import com.google.common.base.Splitter
import com.google.common.collect.Sets
import groovy.transform.Memoized
import org.eclipse.jgit.api.DescribeCommand
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.internal.storage.file.FileRepository
import org.eclipse.jgit.lib.Constants
Expand All @@ -30,11 +26,8 @@ import org.gradle.api.Project

class GitVersionPlugin implements Plugin<Project> {

private static final int SHA_ABBR_LENGTH = 7
private static final int VERSION_ABBR_LENGTH = 10
private static final String PREFIX_REGEX = "[/@]?([A-Za-z]+[/@-])+"
private static final Splitter LINE_SPLITTER = Splitter.on(System.getProperty("line.separator")).omitEmptyStrings()
private static final Splitter WORD_SPLITTER = Splitter.on(" ").omitEmptyStrings()

void apply(Project project) {
project.ext.gitVersion = {
Expand Down Expand Up @@ -79,65 +72,35 @@ class GitVersionPlugin implements Plugin<Project> {

@Memoized
private Git gitRepo(Project project) {
File gitDir = GitCli.getRootGitDir(project.projectDir);
File gitDir = GitCli.getRootGitDir(project.projectDir)
return Git.wrap(new FileRepository(gitDir))
}

@Memoized
private String gitDescribe(Project project, String prefix) {
// verify that "git" command exists (throws exception if it does not)
GitCli.verifyGitCommandExists()

def runGitCmd = { String... commands ->
return GitCli.runGitCommand(project.projectDir, commands);
// This used to be implemented with JGit and replaced with shelling out to installed git (#46) because JGit
// didn't support required behavior. Using installed git doesn't work in some environments or
// with older versions of git client. We're switching back to implementation with JGit. To make sure we don't
// make breaking change, we're keeping both implementations. Plan is to get rid of installed git implementation.
// TODO(mbakovic): Use JGit only implementation #87

String nativeGitDescribe = new NativeGitDescribe(project.projectDir).describe(prefix)
String jgitDescribe = new JGitDescribe(project.projectDir).describe(prefix)

// If native failed, return JGit one
if (nativeGitDescribe == null) {
return jgitDescribe
}

Git git = gitRepo(project)
try {
// back-compat: the JGit "describe" command throws an exception in repositories with no commits, so call it
// first to preserve this behavior in cases where this call would fail but native "git" call does not.
new DescribeCommand(git.getRepository()).call()

/*
* Mimick 'git describe --tags --always --first-parent --match=${prefix}*' by using rev-list to
* support versions of git < 1.8.4
*/

// Get SHAs of all tags, we only need to search for these later on
Set<String> tagRefs = Sets.newHashSet()
for (String tag : getLines(runGitCmd("show-ref", "--tags", "-d"))) {
List<String> parts = WORD_SPLITTER.splitToList(tag)
Preconditions.checkArgument(parts.size() == 2, "Could not parse output of `git show-ref`: %s", parts)
tagRefs.add(parts.get(0))
}

List<String> revs = getLines(runGitCmd("rev-list", "--first-parent", "HEAD"))
for (int depth = 0; depth < revs.size(); depth++) {
String rev = revs.get(depth)
if (tagRefs.contains(rev)) {
String exactTag = runGitCmd("describe", "--tags", "--exact-match", "--match=${prefix}*", rev)
if (exactTag != "") {
return depth == 0 ?
exactTag : String.format("%s-%s-g%s", exactTag, depth, abbrevHash(revs.get(0)))
}
}
}

// No tags found, so return commit hash of HEAD
return abbrevHash(runGitCmd("rev-parse", "HEAD"))
} catch (Throwable t) {
return null
// If native succeeded, make sure it's same as JGit one
if (!nativeGitDescribe.equals(jgitDescribe)) {
throw new IllegalStateException(String.format(
"Inconsistent git describe: native was %s and jgit was %s. "
+ "Please report this on github.com/palantir/gradle-git-version",
nativeGitDescribe, jgitDescribe))
}
}

@Memoized
private List<String> getLines(String s) {
return LINE_SPLITTER.splitToList(s)
}

@Memoized
private String abbrevHash(String s) {
return s.substring(0, SHA_ABBR_LENGTH)
return jgitDescribe
}

@Memoized
Expand All @@ -152,7 +115,7 @@ class GitVersionPlugin implements Plugin<Project> {
@Memoized
private String gitHashFull(Project project) {
Git git = gitRepo(project)
ObjectId objectId = git.getRepository().getRef("HEAD").getObjectId();
ObjectId objectId = git.getRepository().getRef("HEAD").getObjectId()
if (objectId == null) {
return null
}
Expand All @@ -172,6 +135,6 @@ class GitVersionPlugin implements Plugin<Project> {
@Memoized
private boolean isClean(Project project) {
Git git = gitRepo(project)
return git.status().call().isClean();
return git.status().call().isClean()
}
}
117 changes: 117 additions & 0 deletions src/main/groovy/com/palantir/gradle/gitversion/JGitDescribe.groovy
@@ -0,0 +1,117 @@
package com.palantir.gradle.gitversion

import org.eclipse.jgit.api.Git
import org.eclipse.jgit.internal.storage.file.FileRepository
import org.eclipse.jgit.lib.Constants
import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.revwalk.RevCommit
import org.eclipse.jgit.revwalk.RevWalk
import org.slf4j.Logger
import org.slf4j.LoggerFactory

/**
* JGit implementation of git describe with required flags. JGit support for describe is minimal and there is no support
* for --first-parent behavior.
*/
class JGitDescribe implements GitDescribe {
private static final Logger log = LoggerFactory.getLogger(JGitDescribe.class)

private File directory

JGitDescribe(File directory) {
this.directory = directory
}

@Override
String describe(String prefix) {
Git git = Git.wrap(new FileRepository(GitCli.getRootGitDir(directory)))
if (!GitUtils.isRepoEmpty(git)) {
log.debug("Repository is empty")
return null
}

RevCommit headCommit
RefWithTagNameComparator comparator
try {
ObjectId headObjectId = git.getRepository().resolve(Constants.HEAD)
RevWalk walk = new RevWalk(git.getRepository())
headCommit = walk.parseCommit(headObjectId)
comparator = new RefWithTagNameComparator(walk)
} catch (Exception e) {
log.debug("HEAD not found: {}", e)
return null
}

try {
List<String> revs = revList(headCommit)

Map<String, RefWithTagName> commitHashToTag = mapCommitsToTags(git, comparator)

// Walk back commit ancestors looking for tagged one
for (int depth = 0; depth < revs.size(); depth++) {
String rev = revs.get(depth)
if (commitHashToTag.containsKey(rev)) {
String exactTag = commitHashToTag.get(rev).getTag()
// Mimics '--match=${prefix}*' flag in 'git describe --tags --exact-match'
if (exactTag.startsWith(prefix)) {
return depth == 0 ?
exactTag : String.format("%s-%s-g%s", exactTag, depth, GitUtils.abbrevHash(revs.get(0)))
}
}
}

// No tags found, so return commit hash of HEAD
return GitUtils.abbrevHash(headCommit.toObjectId().getName())
} catch (Exception e) {
log.debug("JGit describe failed with {}", e)
return null
}
}

// Mimics 'git rev-list --first-parent <commit>'
private List<String> revList(RevCommit commit) {
List<String> revs = new ArrayList<>()
while (commit) {
revs.add(commit.getName())
try {
// There is no way to check if this exists without failing
commit = commit.getParent(0)
} catch (Exception ignored) {
break
}
}
return revs
}

// Maps all commits returned by 'git show-ref --tags -d' to output of 'git describe --tags --exact-match <commit>'
private Map<String, RefWithTagName> mapCommitsToTags(Git git, RefWithTagNameComparator comparator) {
// Maps commit hash to list of all refs pointing to given commit hash.
// All keys in this map should be same as commit hashes in 'git show-ref --tags -d'
Map<String, RefWithTagName> commitHashToTag = new HashMap<>()
for (Map.Entry<String, Ref> entry : git.getRepository().getTags()) {
RefWithTagName refWithTagName = new RefWithTagName(entry.getValue(), entry.getKey())
updateCommitHashMap(commitHashToTag, comparator, entry.getValue().getObjectId(), refWithTagName)
// Also add dereferenced commit hash if exists
ObjectId peeledRef = refWithTagName.getRef().getPeeledObjectId()
if (peeledRef) {
updateCommitHashMap(commitHashToTag, comparator, peeledRef, refWithTagName)
}
}
return commitHashToTag
}

private void updateCommitHashMap(Map<String, RefWithTagName> map, RefWithTagNameComparator comparator,
ObjectId objectId, RefWithTagName ref) {
// Smallest ref (ordered by this comparator) from list of refs is chosen for each commit.
// This ensures we get same behavior as in 'git describe --tags --exact-match <commit>'
String commitHash = objectId.getName()
if (map.containsKey(commitHash)) {
if (comparator.compare(ref, map.get(commitHash)) < 0) {
map.put(commitHash, ref)
}
} else {
map.put(commitHash, ref)
}
}
}
@@ -0,0 +1,81 @@
package com.palantir.gradle.gitversion

import com.google.common.base.Preconditions
import com.google.common.base.Splitter
import com.google.common.collect.Sets
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.internal.storage.file.FileRepository
import org.slf4j.Logger
import org.slf4j.LoggerFactory

/**
* Mimics git describe by using rev-list to support versions of git < 1.8.4
*/
class NativeGitDescribe implements GitDescribe {
private static final Logger log = LoggerFactory.getLogger(NativeGitDescribe.class)

private static final Splitter LINE_SPLITTER = Splitter.on(System.getProperty("line.separator")).omitEmptyStrings()
private static final Splitter WORD_SPLITTER = Splitter.on(" ").omitEmptyStrings()

private File directory

NativeGitDescribe(File directory) {
this.directory = directory
}

@Override
String describe(String prefix) {
if (!gitCommandExists()) {
return null
}

def runGitCmd = { String... commands ->
return GitCli.runGitCommand(directory, commands)
}

Git git = Git.wrap(new FileRepository(GitCli.getRootGitDir(directory)))
if (!GitUtils.isRepoEmpty(git)) {
log.debug("Repository is empty")
return null
}

try {
// Get SHAs of all tags, we only need to search for these later on
Set<String> tagRefs = Sets.newHashSet()
for (String tag : LINE_SPLITTER.splitToList(runGitCmd("show-ref", "--tags", "-d"))) {
List<String> parts = WORD_SPLITTER.splitToList(tag)
Preconditions.checkArgument(parts.size() == 2, "Could not parse output of `git show-ref`: %s", parts)
tagRefs.add(parts.get(0))
}

List<String> revs = LINE_SPLITTER.splitToList(runGitCmd("rev-list", "--first-parent", "HEAD"))
for (int depth = 0; depth < revs.size(); depth++) {
String rev = revs.get(depth)
if (tagRefs.contains(rev)) {
String exactTag = runGitCmd("describe", "--tags", "--exact-match", "--match=${prefix}*", rev)
if (exactTag != "") {
return depth == 0 ?
exactTag : String.format("%s-%s-g%s", exactTag, depth, GitUtils.abbrevHash(revs.get(0)))
}
}
}

// No tags found, so return commit hash of HEAD
return GitUtils.abbrevHash(runGitCmd("rev-parse", "HEAD"))
} catch (Exception e) {
log.debug("Native git describe failed: {}", e)
return null
}
}

private boolean gitCommandExists() {
try {
// verify that "git" command exists (throws exception if it does not)
GitCli.verifyGitCommandExists()
return true
} catch (Exception e) {
log.debug("Native git command not found: {}", e)
return false
}
}
}

0 comments on commit 5830663

Please sign in to comment.