From f045a68a78f251af836c6b0a18a22a6d9e5f55a0 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Thu, 25 Jul 2013 10:42:22 -0700 Subject: [PATCH] Added the git-describe implementation CQ: 7609 Bug: 339246 Change-Id: I689bc0578ce3a430b9800ad84122e221c69829f4 Signed-off-by: Kohsuke Kawaguchi Also-By: Robin Stocker Also-By: Matthias Sohn Also-By: Christian Halstrick --- .../eclipse/jgit/api/DescribeCommandTest.java | 242 +++++++++++++++ .../eclipse/jgit/internal/JGitText.properties | 1 + .../org/eclipse/jgit/api/DescribeCommand.java | 280 ++++++++++++++++++ .../src/org/eclipse/jgit/api/Git.java | 11 + .../org/eclipse/jgit/internal/JGitText.java | 1 + 5 files changed, 535 insertions(+) create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java new file mode 100644 index 000000000..88f610892 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2013, CloudBees, Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.api; + +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lib.ObjectId; +import org.junit.Test; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +import static org.junit.Assert.*; + +public class DescribeCommandTest extends RepositoryTestCase { + + private Git git; + + @Override + public void setUp() throws Exception { + super.setUp(); + git = new Git(db); + } + + @Test(expected = IllegalArgumentException.class) + public void noTargetSet() throws Exception { + git.describe().call(); + } + + @Test + public void testDescribe() throws Exception { + ObjectId c1 = modify("aaa"); + + ObjectId c2 = modify("bbb"); + tag("t1"); + + ObjectId c3 = modify("ccc"); + tag("t2"); + + ObjectId c4 = modify("ddd"); + + assertNull(describe(c1)); + assertEquals("t1", describe(c2)); + assertEquals("t2", describe(c3)); + + assertNameStartsWith(c4, "3e563c5"); + // the value verified with git-describe(1) + assertEquals("t2-1-g3e563c5", describe(c4)); + } + + /** + * Make sure it finds a tag when not all ancestries include a tag. + * + *
+	 * c1 -+-> T  -
+	 *     |       |
+	 *     +-> c3 -+-> c4
+	 * 
+ * + * @throws Exception + */ + @Test + public void testDescribeBranch() throws Exception { + ObjectId c1 = modify("aaa"); + + ObjectId c2 = modify("bbb"); + tag("t"); + + branch("b", c1); + + ObjectId c3 = modify("ccc"); + + ObjectId c4 = merge(c2); + + assertNameStartsWith(c4, "119892b"); + assertEquals("t-2-g119892b", describe(c4)); // 2 commits: c4 and c3 + assertNull(describe(c3)); + } + + private void branch(String name, ObjectId base) throws GitAPIException { + git.checkout().setCreateBranch(true).setName(name) + .setStartPoint(base.name()).call(); + } + + /** + * When t2 dominates t1, it's clearly preferable to describe by using t2. + * + *
+	 * t1 -+-> t2  -
+	 *     |       |
+	 *     +-> c3 -+-> c4
+	 * 
+ * + * @throws Exception + */ + @Test + public void t1DominatesT2() throws Exception { + ObjectId c1 = modify("aaa"); + tag("t1"); + + ObjectId c2 = modify("bbb"); + tag("t2"); + + branch("b", c1); + + ObjectId c3 = modify("ccc"); + + ObjectId c4 = merge(c2); + + assertNameStartsWith(c4, "119892b"); + assertEquals("t2-2-g119892b", describe(c4)); // 2 commits: c4 and c3 + + assertNameStartsWith(c3, "0244e7f"); + assertEquals("t1-1-g0244e7f", describe(c3)); + } + + /** + * When t1 is nearer than t2, t2 should be found + * + *
+	 * c1 -+-> c2 -> t1 -+
+	 *     |             |
+	 *     +-> t2 -> c3 -+-> c4
+	 * 
+ * + * @throws Exception + */ + @Test + public void t1nearerT2() throws Exception { + ObjectId c1 = modify("aaa"); + modify("bbb"); + ObjectId t1 = modify("ccc"); + tag("t1"); + + branch("b", c1); + modify("ddd"); + tag("t2"); + modify("eee"); + ObjectId c4 = merge(t1); + + assertNameStartsWith(c4, "bb389a4"); + assertEquals("t1-3-gbb389a4", describe(c4)); + } + + /** + * When t1 and t2 have same depth native git seems to add the depths of both + * paths + * + *
+	 * c1 -+-> t1 -> c2 -+
+	 *     |             |
+	 *     +-> t2 -> c3 -+-> c4
+	 * 
+ * + * @throws Exception + */ + @Test + public void t1sameDepthT2() throws Exception { + ObjectId c1 = modify("aaa"); + modify("bbb"); + tag("t1"); + ObjectId c2 = modify("ccc"); + + branch("b", c1); + modify("ddd"); + tag("t2"); + modify("eee"); + ObjectId c4 = merge(c2); + + assertNameStartsWith(c4, "bb389a4"); + assertEquals("t2-4-gbb389a4", describe(c4)); + } + + private ObjectId merge(ObjectId c2) throws GitAPIException { + return git.merge().include(c2).call().getNewHead(); + } + + private ObjectId modify(String content) throws Exception { + File a = new File(db.getWorkTree(), "a.txt"); + touch(a, content); + return git.commit().setAll(true).setMessage(content).call().getId(); + } + + private void tag(String tag) throws GitAPIException { + git.tag().setName(tag).setMessage(tag).call(); + } + + private static void touch(File f, String contents) throws Exception { + FileWriter w = new FileWriter(f); + w.write(contents); + w.close(); + } + + private String describe(ObjectId c1) throws GitAPIException, IOException { + return git.describe().setTarget(c1).call(); + } + + private static void assertNameStartsWith(ObjectId c4, String prefix) { + assertTrue(c4.name(), c4.name().startsWith(prefix)); + } +} diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index 706dce7ce..e0daa4d12 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -478,6 +478,7 @@ systemConfigFileInvalid=Systen wide config file {0} is invalid {1} tagAlreadyExists=tag ''{0}'' already exists tagNameInvalid=tag name {0} is invalid tagOnRepoWithoutHEADCurrentlyNotSupported=Tag on repository without HEAD currently not supported +targetIsNotSet=Target is not set theFactoryMustNotBeNull=The factory must not be null timerAlreadyTerminated=Timer already terminated topologicalSortRequired=Topological sort required. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java new file mode 100644 index 000000000..cc5cfdce3 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2013, CloudBees, Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.api; + +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.api.errors.RefNotFoundException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.*; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.eclipse.jgit.lib.Constants.R_TAGS; + +/** + * Given a commit, show the most recent tag that is reachable from a commit. + * + * @since 3.1 + */ +public class DescribeCommand extends GitCommand { + private final RevWalk w; + + /** + * Commit to describe. + */ + private RevCommit target; + + /** + * How many tags we'll consider as candidates. + * This can only go up to the number of flags JGit can support in a walk, + * which is 24. + */ + private int maxCandidates = 10; + + /** + * + * @param repo + */ + protected DescribeCommand(Repository repo) { + super(repo); + w = new RevWalk(repo); + w.setRetainBody(false); + } + + /** + * Sets the commit to be described. + * + * @param target + * A non-null object ID to be described. + * @return {@code this} + * @throws MissingObjectException + * the supplied commit does not exist. + * @throws IncorrectObjectTypeException + * the supplied id is not a commit or an annotated tag. + * @throws IOException + * a pack file or loose object could not be read. + */ + DescribeCommand setTarget(ObjectId target) throws IOException { + this.target = w.parseCommit(target); + return this; + } + + /** + * Sets the commit to be described. + * + * @param rev + * Commit ID, tag, branch, ref, etc. + * See {@link Repository#resolve(String)} for allowed syntax. + * @return {@code this} + * @throws IncorrectObjectTypeException + * the supplied id is not a commit or an annotated tag. + * @throws RefNotFoundException + * the given rev didn't resolve to any object. + * @throws IOException + * a pack file or loose object could not be read. + */ + DescribeCommand setTarget(String rev) throws IOException, RefNotFoundException { + ObjectId id = repo.resolve(rev); + if (id == null) + throw new RefNotFoundException(MessageFormat.format(JGitText.get().refNotResolved, rev)); + return setTarget(id); + } + + /** + * Describes the specified commit. + * + * @return if there's a tag that points to the commit being described, this tag name + * is returned. Otherwise additional suffix is added to the nearest tag, just + * like git-describe(1). + *

+ * If none of the ancestors of the commit being described has any tags at all, + * then this method returns null, indicating that there's no way to describe this tag. + */ + @Override + public String call() throws GitAPIException { + try { + checkCallable(); + + if (target == null) + throw new IllegalArgumentException(JGitText.get().targetIsNotSet); + + Map tags = new HashMap(); + for (Ref r : repo.getTags().values()) { + ObjectId key = repo.peel(r).getPeeledObjectId(); + if (key == null) + key = r.getObjectId(); + tags.put(key, r); + } + + // combined flags of all the candidate instances + final RevFlagSet allFlags = new RevFlagSet(); + + /** + * Tracks the depth of each tag as we find them. + */ + class Candidate { + final Ref tag; + final RevFlag flag; + + /** + * This field counts number of commits that are reachable from + * the tip but not reachable from the tag. + */ + int depth; + + Candidate(RevCommit commit, Ref tag) { + this.tag = tag; + this.flag = w.newFlag(tag.getName()); + // we'll mark all the nodes reachable from this tag accordingly + allFlags.add(flag); + w.carry(flag); + commit.add(flag); + // As of this writing, JGit carries a flag from a child to its parents + // right before RevWalk.next() returns, so all the flags that are added + // must be manually carried to its parents. If that gets fixed, + // this will be unnecessary. + commit.carry(flag); + } + + /** + * Does this tag contain the given commit? + */ + boolean reaches(RevCommit c) { + return c.has(flag); + } + + String describe(ObjectId tip) throws IOException { + return String.format("%s-%d-g%s", tag.getName().substring(R_TAGS.length()), //$NON-NLS-1$ + Integer.valueOf(depth), w.getObjectReader().abbreviate(tip).name()); + } + } + List candidates = new ArrayList(); // all the candidates we find + + // is the target already pointing to a tag? if so, we are done! + Ref lucky = tags.get(target); + if (lucky != null) + return lucky.getName().substring(R_TAGS.length()); + + w.markStart(target); + + int seen = 0; // commit seen thus far + RevCommit c; + while ((c = w.next()) != null) { + if (!c.hasAny(allFlags)) { + // if a tag already dominates this commit, + // then there's no point in picking a tag on this commit + // since the one that dominates it is always more preferable + Ref t = tags.get(c); + if (t != null) { + Candidate cd = new Candidate(c, t); + candidates.add(cd); + cd.depth = seen; + } + } + + // if the newly discovered commit isn't reachable from a tag that we've seen + // it counts toward the total depth. + for (Candidate cd : candidates) { + if (!cd.reaches(c)) + cd.depth++; + } + + // if we have search going for enough tags, we will start + // closing down. JGit can only give us a finite number of bits, + // so we can't track all tags even if we wanted to. + if (candidates.size() >= maxCandidates) + break; + + // TODO: if all the commits in the queue of RevWalk has allFlags + // there's no point in continuing search as we'll not discover any more + // tags. But RevWalk doesn't expose this. + seen++; + } + + // at this point we aren't adding any more tags to our search, + // but we still need to count all the depths correctly. + while ((c = w.next()) != null) { + if (c.hasAll(allFlags)) { + // no point in visiting further from here, so cut the search here + for (RevCommit p : c.getParents()) + p.add(RevFlag.SEEN); + } else { + for (Candidate cd : candidates) { + if (!cd.reaches(c)) + cd.depth++; + } + } + } + + // if all the nodes are dominated by all the tags, the walk stops + if (candidates.isEmpty()) + return null; + + Candidate best = Collections.min(candidates, new Comparator() { + public int compare(Candidate o1, Candidate o2) { + return o1.depth - o2.depth; + } + }); + + return best.describe(target); + } catch (IOException e) { + throw new JGitInternalException(e.getMessage(), e); + } finally { + setCallable(false); + w.release(); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java index b643cbe25..dc54e7e3b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java @@ -663,6 +663,17 @@ public class Git { return new NameRevCommand(repo); } + /** + * Returns a command object to come up with a short name that describes a + * commit in terms of the nearest git tag. + * + * @return a {@link DescribeCommand}. + * @since 3.1 + */ + public DescribeCommand describe() { + return new DescribeCommand(repo); + } + /** * @return the git repository this class is interacting with */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index 8ac971ab6..7b8809020 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -540,6 +540,7 @@ public class JGitText extends TranslationBundle { /***/ public String tagAlreadyExists; /***/ public String tagNameInvalid; /***/ public String tagOnRepoWithoutHEADCurrentlyNotSupported; + /***/ public String targetIsNotSet; /***/ public String theFactoryMustNotBeNull; /***/ public String timerAlreadyTerminated; /***/ public String topologicalSortRequired;