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

+3-3
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.
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+
}
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

+22-59
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
}
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+
}
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)