summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java242
-rw-r--r--org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties1
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java280
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java11
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java1
5 files changed, 535 insertions, 0 deletions
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 0000000000..88f6108926
--- /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.
+ *
+ * <pre>
+ * c1 -+-> T -
+ * | |
+ * +-> c3 -+-> c4
+ * </pre>
+ *
+ * @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.
+ *
+ * <pre>
+ * t1 -+-> t2 -
+ * | |
+ * +-> c3 -+-> c4
+ * </pre>
+ *
+ * @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
+ *
+ * <pre>
+ * c1 -+-> c2 -> t1 -+
+ * | |
+ * +-> t2 -> c3 -+-> c4
+ * </pre>
+ *
+ * @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
+ *
+ * <pre>
+ * c1 -+-> t1 -> c2 -+
+ * | |
+ * +-> t2 -> c3 -+-> c4
+ * </pre>
+ *
+ * @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 706dce7ce1..e0daa4d126 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 0000000000..cc5cfdce30
--- /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<String> {
+ 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).
+ * <p/>
+ * 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<ObjectId, Ref> tags = new HashMap<ObjectId, Ref>();
+ 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<Candidate> candidates = new ArrayList<Candidate>(); // 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<Candidate>() {
+ 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 b643cbe25d..dc54e7e3b5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
@@ -664,6 +664,17 @@ public class Git {
}
/**
+ * 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
*/
public Repository getRepository() {
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 8ac971ab62..7b88090201 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;