|
| 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 | +} |
0 commit comments