Skip to content

Commit a8dbc9b

Browse files
nmiyakemarkelliot
authored andcommitted
Match output of 'git describe --first-parent' (#48)
Makes it so that, if a merge causes a tag to be added to the history, the tags from the merge will not be used as part of the output for the description. Fixes #46
1 parent fd6eb23 commit a8dbc9b

File tree

3 files changed

+440
-1
lines changed

3 files changed

+440
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
1+
/*
2+
* Copyright 2017 Palantir Technologies
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* <http://www.apache.org/licenses/LICENSE-2.0>
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/*
18+
* Derived from: https://github.com/eclipse/jgit/blob/3b4448637fbb9d74e0c9d44048ba76bb7c1214ce/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java
19+
*
20+
* Copyright (C) 2013, CloudBees, Inc.
21+
* and other copyright owners as documented in the project's IP log.
22+
*
23+
* This program and the accompanying materials are made available
24+
* under the terms of the Eclipse Distribution License v1.0 which
25+
* accompanies this distribution, is reproduced below, and is
26+
* available at http://www.eclipse.org/org/documents/edl-v10.php
27+
*
28+
* All rights reserved.
29+
*
30+
* Redistribution and use in source and binary forms, with or
31+
* without modification, are permitted provided that the following
32+
* conditions are met:
33+
*
34+
* - Redistributions of source code must retain the above copyright
35+
* notice, this list of conditions and the following disclaimer.
36+
*
37+
* - Redistributions in binary form must reproduce the above
38+
* copyright notice, this list of conditions and the following
39+
* disclaimer in the documentation and/or other materials provided
40+
* with the distribution.
41+
*
42+
* - Neither the name of the Eclipse Foundation, Inc. nor the
43+
* names of its contributors may be used to endorse or promote
44+
* products derived from this software without specific prior
45+
* written permission.
46+
*
47+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
48+
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
49+
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
50+
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
51+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
52+
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
53+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
54+
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
55+
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
56+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
57+
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
58+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
59+
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
60+
*/
61+
package com.palantir.gradle.gitversion;
62+
63+
import org.eclipse.jgit.api.GitCommand;
64+
import org.eclipse.jgit.api.errors.GitAPIException;
65+
import org.eclipse.jgit.api.errors.JGitInternalException;
66+
import org.eclipse.jgit.api.errors.RefNotFoundException;
67+
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
68+
import org.eclipse.jgit.errors.MissingObjectException;
69+
import org.eclipse.jgit.internal.JGitText;
70+
import org.eclipse.jgit.lib.Constants;
71+
import org.eclipse.jgit.lib.ObjectId;
72+
import org.eclipse.jgit.lib.Ref;
73+
import org.eclipse.jgit.lib.Repository;
74+
import org.eclipse.jgit.revwalk.RevCommit;
75+
import org.eclipse.jgit.revwalk.RevFlag;
76+
import org.eclipse.jgit.revwalk.RevFlagSet;
77+
import org.eclipse.jgit.revwalk.RevWalk;
78+
import org.eclipse.jgit.revwalk.filter.RevFilter;
79+
80+
import java.io.IOException;
81+
import java.text.MessageFormat;
82+
import java.util.*;
83+
84+
import static org.eclipse.jgit.lib.Constants.R_TAGS;
85+
86+
/**
87+
* Given a commit, show the most recent tag that is reachable from a commit. If a commit has multiple parent commits,
88+
* only consider the first one (match the behavior of "git describe --first-parent").
89+
*
90+
* Based on the org.eclipse.jgit.api.DescribeCommand. The only difference is that the FirstParentFilter is set as a
91+
* filter on the RevWalk used internally by the command.
92+
*
93+
* @since 3.2
94+
*/
95+
public class DescribeFirstParentCommand extends GitCommand<String> {
96+
private final RevWalk w;
97+
98+
/**
99+
* Commit to describe.
100+
*/
101+
private RevCommit target;
102+
103+
/**
104+
* How many tags we'll consider as candidates.
105+
* This can only go up to the number of flags JGit can support in a walk,
106+
* which is 24.
107+
*/
108+
private int maxCandidates = 10;
109+
110+
/**
111+
* Whether to always use long output format or not.
112+
*/
113+
private boolean longDesc;
114+
115+
/**
116+
*
117+
* @param repo
118+
*/
119+
protected DescribeFirstParentCommand(Repository repo) {
120+
super(repo);
121+
w = new RevWalk(repo);
122+
w.setRevFilter(new FirstParentFilter());
123+
w.setRetainBody(false);
124+
}
125+
126+
/**
127+
* Sets the commit to be described.
128+
*
129+
* @param target
130+
* A non-null object ID to be described.
131+
* @return {@code this}
132+
* @throws MissingObjectException
133+
* the supplied commit does not exist.
134+
* @throws IncorrectObjectTypeException
135+
* the supplied id is not a commit or an annotated tag.
136+
* @throws IOException
137+
* a pack file or loose object could not be read.
138+
*/
139+
public DescribeFirstParentCommand setTarget(ObjectId target) throws IOException {
140+
this.target = w.parseCommit(target);
141+
return this;
142+
}
143+
144+
/**
145+
* Sets the commit to be described.
146+
*
147+
* @param rev
148+
* Commit ID, tag, branch, ref, etc.
149+
* See {@link Repository#resolve(String)} for allowed syntax.
150+
* @return {@code this}
151+
* @throws IncorrectObjectTypeException
152+
* the supplied id is not a commit or an annotated tag.
153+
* @throws RefNotFoundException
154+
* the given rev didn't resolve to any object.
155+
* @throws IOException
156+
* a pack file or loose object could not be read.
157+
*/
158+
public DescribeFirstParentCommand setTarget(String rev) throws IOException,
159+
RefNotFoundException {
160+
ObjectId id = repo.resolve(rev);
161+
if (id == null)
162+
throw new RefNotFoundException(MessageFormat.format(JGitText.get().refNotResolved, rev));
163+
return setTarget(id);
164+
}
165+
166+
/**
167+
* Determine whether always to use the long format or not. When set to
168+
* <code>true</code> the long format is used even the commit matches a tag.
169+
*
170+
* @param longDesc
171+
* <code>true</code> if always the long format should be used.
172+
* @return {@code this}
173+
*
174+
* @see <a
175+
* href="https://www.kernel.org/pub/software/scm/git/docs/git-describe.html"
176+
* >Git documentation about describe</a>
177+
* @since 4.0
178+
*/
179+
public DescribeFirstParentCommand setLong(boolean longDesc) {
180+
this.longDesc = longDesc;
181+
return this;
182+
}
183+
184+
private String longDescription(Ref tag, int depth, ObjectId tip)
185+
throws IOException {
186+
return String.format(
187+
"%s-%d-g%s", tag.getName().substring(R_TAGS.length()), //$NON-NLS-1$
188+
Integer.valueOf(depth), w.getObjectReader().abbreviate(tip)
189+
.name());
190+
}
191+
192+
/**
193+
* Describes the specified commit. Target defaults to HEAD if no commit was
194+
* set explicitly.
195+
*
196+
* @return if there's a tag that points to the commit being described, this
197+
* tag name is returned. Otherwise additional suffix is added to the
198+
* nearest tag, just like git-describe(1).
199+
* <p>
200+
* If none of the ancestors of the commit being described has any
201+
* tags at all, then this method returns null, indicating that
202+
* there's no way to describe this tag.
203+
*/
204+
@Override
205+
public String call() throws GitAPIException {
206+
try {
207+
checkCallable();
208+
209+
if (target == null)
210+
setTarget(Constants.HEAD);
211+
212+
Map<ObjectId, Ref> tags = new HashMap<ObjectId, Ref>();
213+
214+
for (Ref r : repo.getRefDatabase().getRefs(R_TAGS).values()) {
215+
ObjectId key = repo.peel(r).getPeeledObjectId();
216+
if (key == null)
217+
key = r.getObjectId();
218+
tags.put(key, r);
219+
}
220+
221+
// combined flags of all the candidate instances
222+
final RevFlagSet allFlags = new RevFlagSet();
223+
224+
/**
225+
* Tracks the depth of each tag as we find them.
226+
*/
227+
class Candidate {
228+
final Ref tag;
229+
final RevFlag flag;
230+
231+
/**
232+
* This field counts number of commits that are reachable from
233+
* the tip but not reachable from the tag.
234+
*/
235+
int depth;
236+
237+
Candidate(RevCommit commit, Ref tag) {
238+
this.tag = tag;
239+
this.flag = w.newFlag(tag.getName());
240+
// we'll mark all the nodes reachable from this tag accordingly
241+
allFlags.add(flag);
242+
w.carry(flag);
243+
commit.add(flag);
244+
// As of this writing, JGit carries a flag from a child to its parents
245+
// right before RevWalk.next() returns, so all the flags that are added
246+
// must be manually carried to its parents. If that gets fixed,
247+
// this will be unnecessary.
248+
commit.carry(flag);
249+
}
250+
251+
/**
252+
* Does this tag contain the given commit?
253+
*/
254+
boolean reaches(RevCommit c) {
255+
return c.has(flag);
256+
}
257+
258+
String describe(ObjectId tip) throws IOException {
259+
return longDescription(tag, depth, tip);
260+
}
261+
262+
}
263+
List<Candidate> candidates = new ArrayList<Candidate>(); // all the candidates we find
264+
265+
// is the target already pointing to a tag? if so, we are done!
266+
Ref lucky = tags.get(target);
267+
if (lucky != null) {
268+
return longDesc ? longDescription(lucky, 0, target) : lucky
269+
.getName().substring(R_TAGS.length());
270+
}
271+
272+
w.markStart(target);
273+
274+
int seen = 0; // commit seen thus far
275+
RevCommit c;
276+
while ((c = w.next()) != null) {
277+
if (!c.hasAny(allFlags)) {
278+
// if a tag already dominates this commit,
279+
// then there's no point in picking a tag on this commit
280+
// since the one that dominates it is always more preferable
281+
Ref t = tags.get(c);
282+
if (t != null) {
283+
Candidate cd = new Candidate(c, t);
284+
candidates.add(cd);
285+
cd.depth = seen;
286+
}
287+
}
288+
289+
// if the newly discovered commit isn't reachable from a tag that we've seen
290+
// it counts toward the total depth.
291+
for (Candidate cd : candidates) {
292+
if (!cd.reaches(c))
293+
cd.depth++;
294+
}
295+
296+
// if we have search going for enough tags, we will start
297+
// closing down. JGit can only give us a finite number of bits,
298+
// so we can't track all tags even if we wanted to.
299+
if (candidates.size() >= maxCandidates)
300+
break;
301+
302+
// TODO: if all the commits in the queue of RevWalk has allFlags
303+
// there's no point in continuing search as we'll not discover any more
304+
// tags. But RevWalk doesn't expose this.
305+
seen++;
306+
}
307+
308+
// at this point we aren't adding any more tags to our search,
309+
// but we still need to count all the depths correctly.
310+
while ((c = w.next()) != null) {
311+
if (c.hasAll(allFlags)) {
312+
// no point in visiting further from here, so cut the search here
313+
for (RevCommit p : c.getParents())
314+
p.add(RevFlag.SEEN);
315+
} else {
316+
for (Candidate cd : candidates) {
317+
if (!cd.reaches(c))
318+
cd.depth++;
319+
}
320+
}
321+
}
322+
323+
// if all the nodes are dominated by all the tags, the walk stops
324+
if (candidates.isEmpty())
325+
return null;
326+
327+
Candidate best = Collections.min(candidates, new Comparator<Candidate>() {
328+
public int compare(Candidate o1, Candidate o2) {
329+
return o1.depth - o2.depth;
330+
}
331+
});
332+
333+
return best.describe(target);
334+
} catch (IOException e) {
335+
throw new JGitInternalException(e.getMessage(), e);
336+
} finally {
337+
setCallable(false);
338+
w.close();
339+
}
340+
}
341+
342+
private static final class FirstParentFilter extends RevFilter {
343+
private Set<RevCommit> ignoreCommits = new HashSet<>();
344+
345+
@Override
346+
public boolean include(RevWalk revWalk, RevCommit commit) throws IOException {
347+
for (int i = 1; i < commit.getParentCount(); i++) {
348+
// if a commit has more than one parent, ignore all parents except the first
349+
ignoreCommits.add(commit.getParent(i));
350+
}
351+
boolean include = true;
352+
if (ignoreCommits.contains(commit)) {
353+
include = false;
354+
ignoreCommits.remove(commit);
355+
}
356+
return include;
357+
}
358+
359+
@Override
360+
public RevFilter clone() {
361+
return new FirstParentFilter();
362+
}
363+
}
364+
}

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ class GitVersionPlugin implements Plugin<Project> {
4444
private String gitDesc(Project project) {
4545
Git git = gitRepo(project)
4646
try {
47-
String version = git.describe().call() ?: UNSPECIFIED_VERSION
47+
DescribeFirstParentCommand describe = new DescribeFirstParentCommand(git.getRepository())
48+
String version = describe.call() ?: UNSPECIFIED_VERSION
4849
boolean isClean = git.status().call().isClean()
4950
return version + (isClean ? '' : '.dirty')
5051
} catch (Throwable t) {

0 commit comments

Comments
 (0)