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