Skip to content

Commit 5830663

Browse files
delta003iamdanfox
authored andcommitted
Native vs JGit 2 (#89)
* 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
1 parent 61c0f7d commit 5830663

File tree

9 files changed

+444
-63
lines changed

9 files changed

+444
-63
lines changed

readme.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ Git-Version Gradle Plugin
55

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

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

1212
The second, called `versionDetails()`, returns an object containing the specific details of the version string:
1313
the tag name, the commit count since the tag, the current commit hash of HEAD, and an optional branch name of HEAD.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.palantir.gradle.gitversion
2+
3+
interface GitDescribe {
4+
5+
/**
6+
* Mimics behaviour of 'git describe --tags --always --first-parent --match=${prefix}*'
7+
* Method returns null if repository is empty.
8+
*/
9+
String describe(String prefix)
10+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.palantir.gradle.gitversion
2+
3+
import org.eclipse.jgit.api.DescribeCommand
4+
import org.eclipse.jgit.api.Git
5+
import org.eclipse.jgit.lib.ObjectId
6+
import org.eclipse.jgit.lib.Ref
7+
8+
class GitUtils {
9+
10+
static final int SHA_ABBR_LENGTH = 7
11+
12+
static String abbrevHash(String s) {
13+
return s.substring(0, SHA_ABBR_LENGTH)
14+
}
15+
16+
static boolean isRepoEmpty(Git git) {
17+
// back-compat: the JGit "describe" command throws an exception in repositories with no commits, so call it
18+
// first to preserve this behavior in cases where this call would fail but native "git" call does not.
19+
try {
20+
new DescribeCommand(git.getRepository()).call()
21+
return true
22+
} catch (Exception ignored) {
23+
return false
24+
}
25+
}
26+
27+
// getPeeledObjectId returns:
28+
// "if this ref is an annotated tag the id of the commit (or tree or blob) that the annotated tag refers to;
29+
// null if this ref does not refer to an annotated tag."
30+
// We use this to check if tag is annotated.
31+
static boolean isAnnotatedTag(Ref ref) {
32+
ObjectId peeledObjectId = ref.getPeeledObjectId()
33+
return peeledObjectId != null
34+
}
35+
}

src/main/groovy/com/palantir/gradle/gitversion/GitVersionPlugin.groovy

Lines changed: 22 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,7 @@
1515
*/
1616
package com.palantir.gradle.gitversion
1717

18-
import com.google.common.base.Preconditions
19-
import com.google.common.base.Splitter
20-
import com.google.common.collect.Sets
2118
import groovy.transform.Memoized
22-
import org.eclipse.jgit.api.DescribeCommand
2319
import org.eclipse.jgit.api.Git
2420
import org.eclipse.jgit.internal.storage.file.FileRepository
2521
import org.eclipse.jgit.lib.Constants
@@ -30,11 +26,8 @@ import org.gradle.api.Project
3026

3127
class GitVersionPlugin implements Plugin<Project> {
3228

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

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

8073
@Memoized
8174
private Git gitRepo(Project project) {
82-
File gitDir = GitCli.getRootGitDir(project.projectDir);
75+
File gitDir = GitCli.getRootGitDir(project.projectDir)
8376
return Git.wrap(new FileRepository(gitDir))
8477
}
8578

8679
@Memoized
8780
private String gitDescribe(Project project, String prefix) {
88-
// verify that "git" command exists (throws exception if it does not)
89-
GitCli.verifyGitCommandExists()
90-
91-
def runGitCmd = { String... commands ->
92-
return GitCli.runGitCommand(project.projectDir, commands);
81+
// This used to be implemented with JGit and replaced with shelling out to installed git (#46) because JGit
82+
// didn't support required behavior. Using installed git doesn't work in some environments or
83+
// with older versions of git client. We're switching back to implementation with JGit. To make sure we don't
84+
// make breaking change, we're keeping both implementations. Plan is to get rid of installed git implementation.
85+
// TODO(mbakovic): Use JGit only implementation #87
86+
87+
String nativeGitDescribe = new NativeGitDescribe(project.projectDir).describe(prefix)
88+
String jgitDescribe = new JGitDescribe(project.projectDir).describe(prefix)
89+
90+
// If native failed, return JGit one
91+
if (nativeGitDescribe == null) {
92+
return jgitDescribe
9393
}
9494

95-
Git git = gitRepo(project)
96-
try {
97-
// back-compat: the JGit "describe" command throws an exception in repositories with no commits, so call it
98-
// first to preserve this behavior in cases where this call would fail but native "git" call does not.
99-
new DescribeCommand(git.getRepository()).call()
100-
101-
/*
102-
* Mimick 'git describe --tags --always --first-parent --match=${prefix}*' by using rev-list to
103-
* support versions of git < 1.8.4
104-
*/
105-
106-
// Get SHAs of all tags, we only need to search for these later on
107-
Set<String> tagRefs = Sets.newHashSet()
108-
for (String tag : getLines(runGitCmd("show-ref", "--tags", "-d"))) {
109-
List<String> parts = WORD_SPLITTER.splitToList(tag)
110-
Preconditions.checkArgument(parts.size() == 2, "Could not parse output of `git show-ref`: %s", parts)
111-
tagRefs.add(parts.get(0))
112-
}
113-
114-
List<String> revs = getLines(runGitCmd("rev-list", "--first-parent", "HEAD"))
115-
for (int depth = 0; depth < revs.size(); depth++) {
116-
String rev = revs.get(depth)
117-
if (tagRefs.contains(rev)) {
118-
String exactTag = runGitCmd("describe", "--tags", "--exact-match", "--match=${prefix}*", rev)
119-
if (exactTag != "") {
120-
return depth == 0 ?
121-
exactTag : String.format("%s-%s-g%s", exactTag, depth, abbrevHash(revs.get(0)))
122-
}
123-
}
124-
}
125-
126-
// No tags found, so return commit hash of HEAD
127-
return abbrevHash(runGitCmd("rev-parse", "HEAD"))
128-
} catch (Throwable t) {
129-
return null
95+
// If native succeeded, make sure it's same as JGit one
96+
if (!nativeGitDescribe.equals(jgitDescribe)) {
97+
throw new IllegalStateException(String.format(
98+
"Inconsistent git describe: native was %s and jgit was %s. "
99+
+ "Please report this on github.com/palantir/gradle-git-version",
100+
nativeGitDescribe, jgitDescribe))
130101
}
131-
}
132-
133-
@Memoized
134-
private List<String> getLines(String s) {
135-
return LINE_SPLITTER.splitToList(s)
136-
}
137102

138-
@Memoized
139-
private String abbrevHash(String s) {
140-
return s.substring(0, SHA_ABBR_LENGTH)
103+
return jgitDescribe
141104
}
142105

143106
@Memoized
@@ -152,7 +115,7 @@ class GitVersionPlugin implements Plugin<Project> {
152115
@Memoized
153116
private String gitHashFull(Project project) {
154117
Git git = gitRepo(project)
155-
ObjectId objectId = git.getRepository().getRef("HEAD").getObjectId();
118+
ObjectId objectId = git.getRepository().getRef("HEAD").getObjectId()
156119
if (objectId == null) {
157120
return null
158121
}
@@ -172,6 +135,6 @@ class GitVersionPlugin implements Plugin<Project> {
172135
@Memoized
173136
private boolean isClean(Project project) {
174137
Git git = gitRepo(project)
175-
return git.status().call().isClean();
138+
return git.status().call().isClean()
176139
}
177140
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package com.palantir.gradle.gitversion
2+
3+
import org.eclipse.jgit.api.Git
4+
import org.eclipse.jgit.internal.storage.file.FileRepository
5+
import org.eclipse.jgit.lib.Constants
6+
import org.eclipse.jgit.lib.ObjectId
7+
import org.eclipse.jgit.lib.Ref
8+
import org.eclipse.jgit.revwalk.RevCommit
9+
import org.eclipse.jgit.revwalk.RevWalk
10+
import org.slf4j.Logger
11+
import org.slf4j.LoggerFactory
12+
13+
/**
14+
* JGit implementation of git describe with required flags. JGit support for describe is minimal and there is no support
15+
* for --first-parent behavior.
16+
*/
17+
class JGitDescribe implements GitDescribe {
18+
private static final Logger log = LoggerFactory.getLogger(JGitDescribe.class)
19+
20+
private File directory
21+
22+
JGitDescribe(File directory) {
23+
this.directory = directory
24+
}
25+
26+
@Override
27+
String describe(String prefix) {
28+
Git git = Git.wrap(new FileRepository(GitCli.getRootGitDir(directory)))
29+
if (!GitUtils.isRepoEmpty(git)) {
30+
log.debug("Repository is empty")
31+
return null
32+
}
33+
34+
RevCommit headCommit
35+
RefWithTagNameComparator comparator
36+
try {
37+
ObjectId headObjectId = git.getRepository().resolve(Constants.HEAD)
38+
RevWalk walk = new RevWalk(git.getRepository())
39+
headCommit = walk.parseCommit(headObjectId)
40+
comparator = new RefWithTagNameComparator(walk)
41+
} catch (Exception e) {
42+
log.debug("HEAD not found: {}", e)
43+
return null
44+
}
45+
46+
try {
47+
List<String> revs = revList(headCommit)
48+
49+
Map<String, RefWithTagName> commitHashToTag = mapCommitsToTags(git, comparator)
50+
51+
// Walk back commit ancestors looking for tagged one
52+
for (int depth = 0; depth < revs.size(); depth++) {
53+
String rev = revs.get(depth)
54+
if (commitHashToTag.containsKey(rev)) {
55+
String exactTag = commitHashToTag.get(rev).getTag()
56+
// Mimics '--match=${prefix}*' flag in 'git describe --tags --exact-match'
57+
if (exactTag.startsWith(prefix)) {
58+
return depth == 0 ?
59+
exactTag : String.format("%s-%s-g%s", exactTag, depth, GitUtils.abbrevHash(revs.get(0)))
60+
}
61+
}
62+
}
63+
64+
// No tags found, so return commit hash of HEAD
65+
return GitUtils.abbrevHash(headCommit.toObjectId().getName())
66+
} catch (Exception e) {
67+
log.debug("JGit describe failed with {}", e)
68+
return null
69+
}
70+
}
71+
72+
// Mimics 'git rev-list --first-parent <commit>'
73+
private List<String> revList(RevCommit commit) {
74+
List<String> revs = new ArrayList<>()
75+
while (commit) {
76+
revs.add(commit.getName())
77+
try {
78+
// There is no way to check if this exists without failing
79+
commit = commit.getParent(0)
80+
} catch (Exception ignored) {
81+
break
82+
}
83+
}
84+
return revs
85+
}
86+
87+
// Maps all commits returned by 'git show-ref --tags -d' to output of 'git describe --tags --exact-match <commit>'
88+
private Map<String, RefWithTagName> mapCommitsToTags(Git git, RefWithTagNameComparator comparator) {
89+
// Maps commit hash to list of all refs pointing to given commit hash.
90+
// All keys in this map should be same as commit hashes in 'git show-ref --tags -d'
91+
Map<String, RefWithTagName> commitHashToTag = new HashMap<>()
92+
for (Map.Entry<String, Ref> entry : git.getRepository().getTags()) {
93+
RefWithTagName refWithTagName = new RefWithTagName(entry.getValue(), entry.getKey())
94+
updateCommitHashMap(commitHashToTag, comparator, entry.getValue().getObjectId(), refWithTagName)
95+
// Also add dereferenced commit hash if exists
96+
ObjectId peeledRef = refWithTagName.getRef().getPeeledObjectId()
97+
if (peeledRef) {
98+
updateCommitHashMap(commitHashToTag, comparator, peeledRef, refWithTagName)
99+
}
100+
}
101+
return commitHashToTag
102+
}
103+
104+
private void updateCommitHashMap(Map<String, RefWithTagName> map, RefWithTagNameComparator comparator,
105+
ObjectId objectId, RefWithTagName ref) {
106+
// Smallest ref (ordered by this comparator) from list of refs is chosen for each commit.
107+
// This ensures we get same behavior as in 'git describe --tags --exact-match <commit>'
108+
String commitHash = objectId.getName()
109+
if (map.containsKey(commitHash)) {
110+
if (comparator.compare(ref, map.get(commitHash)) < 0) {
111+
map.put(commitHash, ref)
112+
}
113+
} else {
114+
map.put(commitHash, ref)
115+
}
116+
}
117+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.palantir.gradle.gitversion
2+
3+
import com.google.common.base.Preconditions
4+
import com.google.common.base.Splitter
5+
import com.google.common.collect.Sets
6+
import org.eclipse.jgit.api.Git
7+
import org.eclipse.jgit.internal.storage.file.FileRepository
8+
import org.slf4j.Logger
9+
import org.slf4j.LoggerFactory
10+
11+
/**
12+
* Mimics git describe by using rev-list to support versions of git < 1.8.4
13+
*/
14+
class NativeGitDescribe implements GitDescribe {
15+
private static final Logger log = LoggerFactory.getLogger(NativeGitDescribe.class)
16+
17+
private static final Splitter LINE_SPLITTER = Splitter.on(System.getProperty("line.separator")).omitEmptyStrings()
18+
private static final Splitter WORD_SPLITTER = Splitter.on(" ").omitEmptyStrings()
19+
20+
private File directory
21+
22+
NativeGitDescribe(File directory) {
23+
this.directory = directory
24+
}
25+
26+
@Override
27+
String describe(String prefix) {
28+
if (!gitCommandExists()) {
29+
return null
30+
}
31+
32+
def runGitCmd = { String... commands ->
33+
return GitCli.runGitCommand(directory, commands)
34+
}
35+
36+
Git git = Git.wrap(new FileRepository(GitCli.getRootGitDir(directory)))
37+
if (!GitUtils.isRepoEmpty(git)) {
38+
log.debug("Repository is empty")
39+
return null
40+
}
41+
42+
try {
43+
// Get SHAs of all tags, we only need to search for these later on
44+
Set<String> tagRefs = Sets.newHashSet()
45+
for (String tag : LINE_SPLITTER.splitToList(runGitCmd("show-ref", "--tags", "-d"))) {
46+
List<String> parts = WORD_SPLITTER.splitToList(tag)
47+
Preconditions.checkArgument(parts.size() == 2, "Could not parse output of `git show-ref`: %s", parts)
48+
tagRefs.add(parts.get(0))
49+
}
50+
51+
List<String> revs = LINE_SPLITTER.splitToList(runGitCmd("rev-list", "--first-parent", "HEAD"))
52+
for (int depth = 0; depth < revs.size(); depth++) {
53+
String rev = revs.get(depth)
54+
if (tagRefs.contains(rev)) {
55+
String exactTag = runGitCmd("describe", "--tags", "--exact-match", "--match=${prefix}*", rev)
56+
if (exactTag != "") {
57+
return depth == 0 ?
58+
exactTag : String.format("%s-%s-g%s", exactTag, depth, GitUtils.abbrevHash(revs.get(0)))
59+
}
60+
}
61+
}
62+
63+
// No tags found, so return commit hash of HEAD
64+
return GitUtils.abbrevHash(runGitCmd("rev-parse", "HEAD"))
65+
} catch (Exception e) {
66+
log.debug("Native git describe failed: {}", e)
67+
return null
68+
}
69+
}
70+
71+
private boolean gitCommandExists() {
72+
try {
73+
// verify that "git" command exists (throws exception if it does not)
74+
GitCli.verifyGitCommandExists()
75+
return true
76+
} catch (Exception e) {
77+
log.debug("Native git command not found: {}", e)
78+
return false
79+
}
80+
}
81+
}

0 commit comments

Comments
 (0)