Merge "Added the git-describe implementation"
This commit is contained in:
commit
0be59ab033
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -663,6 +663,17 @@ public NameRevCommand nameRev() {
|
|||
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
|
||||
*/
|
||||
|
|
|
@ -540,6 +540,7 @@ public static JGitText get() {
|
|||
/***/ public String tagAlreadyExists;
|
||||
/***/ public String tagNameInvalid;
|
||||
/***/ public String tagOnRepoWithoutHEADCurrentlyNotSupported;
|
||||
/***/ public String targetIsNotSet;
|
||||
/***/ public String theFactoryMustNotBeNull;
|
||||
/***/ public String timerAlreadyTerminated;
|
||||
/***/ public String topologicalSortRequired;
|
||||
|
|
Loading…
Reference in New Issue