Add a handler for /+describe similar to "git describe"
Options parallel options to "git describe", e.g.
$ curl http://gitiles/repo/+describe/deadbeef?contains&all&format=JSON
master~3
Only JSON and TEXT formats are supported, there is no HTML template.
For now, only --contains is supported, using JGit's NameRevCommand
(since "git describe --contains" calls "git name-rev" internally).
Change-Id: Ia71cb546645f93eb19eac4db69103835e46ae678
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/BaseServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/BaseServlet.java
index a2bc65c..e47e3df 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/BaseServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/BaseServlet.java
@@ -258,6 +258,27 @@
return res.getWriter();
}
+ /**
+ * Render an error as plain text.
+ *
+ * @param req in-progress request.
+ * @param res in-progress response.
+ * @param statusCode HTTP status code.
+ * @param message full message text.
+ *
+ * @throws IOException
+ */
+ protected void renderTextError(HttpServletRequest req, HttpServletResponse res, int statusCode,
+ String message) throws IOException {
+ res.setStatus(statusCode);
+ res.setContentType(TEXT.getMimeType());
+ res.setCharacterEncoding("UTF-8");
+ setCacheHeaders(req, res);
+ PrintWriter out = res.getWriter();
+ out.write(message);
+ out.close();
+ }
+
protected void setCacheHeaders(HttpServletRequest req, HttpServletResponse res) {
setNotCacheable(res);
}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/DescribeServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/DescribeServlet.java
new file mode 100644
index 0000000..7b49f63
--- /dev/null
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/DescribeServlet.java
@@ -0,0 +1,149 @@
+// Copyright 2013 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gitiles;
+
+import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.NameRevCommand;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.errors.AmbiguousObjectException;
+import org.eclipse.jgit.errors.RevisionSyntaxException;
+import org.eclipse.jgit.http.server.ServletUtils;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableMap;
+import com.google.gson.reflect.TypeToken;
+
+/** Serves an API result describing an object. */
+public class DescribeServlet extends BaseServlet {
+ private static final long serialVersionUID = 1L;
+
+ private static final String ALL_PARAM = "all";
+ private static final String CONTAINS_PARAM = "contains";
+ private static final String TAGS_PARAM = "tags";
+
+ private static boolean getBooleanParam(GitilesView view, String name) {
+ List<String> values = view.getParameters().get(name);
+ return !values.isEmpty()
+ && (values.get(0).equals("") || values.get(0).equals("1"));
+ }
+
+ protected DescribeServlet() {
+ super(null);
+ }
+
+ @Override
+ protected void doGetText(HttpServletRequest req, HttpServletResponse res)
+ throws IOException {
+ String name = describe(ServletUtils.getRepository(req), ViewFilter.getView(req), req, res);
+ if (name == null) {
+ return;
+ }
+ PrintWriter out = startRenderText(req, res);
+ out.write(RefServlet.sanitizeRefForText(name));
+ out.close();
+ }
+
+ @Override
+ protected void doGetJson(HttpServletRequest req, HttpServletResponse res)
+ throws IOException {
+ String name = describe(ServletUtils.getRepository(req), ViewFilter.getView(req), req, res);
+ if (name == null) {
+ return;
+ }
+ renderJson(req, res,
+ ImmutableMap.of(ViewFilter.getView(req).getPathPart(), name),
+ new TypeToken<Map<String, String>>() {}.getType());
+ }
+
+ private ObjectId resolve(Repository repo, GitilesView view, HttpServletRequest req,
+ HttpServletResponse res) throws IOException {
+ String rev = view.getPathPart();
+ try {
+ return repo.resolve(rev);
+ } catch (RevisionSyntaxException e) {
+ renderTextError(req, res, SC_BAD_REQUEST,
+ "Invalid revision syntax: " + RefServlet.sanitizeRefForText(rev));
+ return null;
+ } catch (AmbiguousObjectException e) {
+ renderTextError(req, res, SC_BAD_REQUEST, String.format(
+ "Ambiguous short SHA-1 %s (%s)",
+ e.getAbbreviatedObjectId(), Joiner.on(", ").join(e.getCandidates())));
+ return null;
+ }
+ }
+
+ private String describe(Repository repo, GitilesView view, HttpServletRequest req,
+ HttpServletResponse res) throws IOException {
+ if (!getBooleanParam(view, CONTAINS_PARAM)) {
+ res.setStatus(SC_BAD_REQUEST);
+ return null;
+ }
+ ObjectId id = resolve(repo, view, req, res);
+ if (id == null) {
+ return null;
+ }
+ NameRevCommand cmd = nameRevCommand(id, req, res);
+ if (cmd == null) {
+ return null;
+ }
+ String name;
+ try {
+ name = cmd.call().get(id);
+ } catch (GitAPIException e) {
+ throw new IOException(e);
+ }
+ if (name == null) {
+ res.setStatus(SC_NOT_FOUND);
+ return null;
+ }
+ return name;
+ }
+
+ private NameRevCommand nameRevCommand(ObjectId id, HttpServletRequest req,
+ HttpServletResponse res) throws IOException {
+ Repository repo = ServletUtils.getRepository(req);
+ GitilesView view = ViewFilter.getView(req);
+ NameRevCommand cmd = new Git(repo).nameRev();
+ boolean all = getBooleanParam(view, ALL_PARAM);
+ boolean tags = getBooleanParam(view, TAGS_PARAM);
+ if (all && tags) {
+ renderTextError(req, res, SC_BAD_REQUEST, "Cannot specify both \"all\" and \"tags\"");
+ return null;
+ }
+ if (all) {
+ cmd.addPrefix(Constants.R_REFS);
+ } else if (tags) {
+ cmd.addPrefix(Constants.R_TAGS);
+ } else {
+ cmd.addAnnotatedTags();
+ }
+ cmd.add(id);
+ return cmd;
+ }
+}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java
index a69835c..1955fff 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java
@@ -240,6 +240,8 @@
return new DiffServlet(renderer, linkifier());
case LOG:
return new LogServlet(renderer, linkifier());
+ case DESCRIBE:
+ return new DescribeServlet();
default:
throw new IllegalArgumentException("Invalid view type: " + view);
}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesView.java b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesView.java
index 16f5a54..885074d 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesView.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesView.java
@@ -55,7 +55,8 @@
REVISION,
PATH,
DIFF,
- LOG;
+ LOG,
+ DESCRIBE;
}
/** Exception thrown when building a view that is invalid. */
@@ -98,6 +99,7 @@
case REVISION:
revision = other.revision;
// Fallthrough.
+ case DESCRIBE:
case REFS:
case REPOSITORY_INDEX:
repositoryName = other.repositoryName;
@@ -151,6 +153,7 @@
case HOST_INDEX:
case REPOSITORY_INDEX:
case REFS:
+ case DESCRIBE:
throw new IllegalStateException(String.format("cannot set revision on %s view", type));
default:
this.revision = checkNotNull(revision);
@@ -204,6 +207,7 @@
case DIFF:
this.path = maybeTrimLeadingAndTrailingSlash(checkNotNull(path));
return this;
+ case DESCRIBE:
case REFS:
case LOG:
this.path = path != null ? maybeTrimLeadingAndTrailingSlash(path) : null;
@@ -260,6 +264,9 @@
case REFS:
checkRefs();
break;
+ case DESCRIBE:
+ checkDescribe();
+ break;
case REVISION:
checkRevision();
break;
@@ -301,6 +308,10 @@
checkRepositoryIndex();
}
+ private void checkDescribe() {
+ checkRepositoryIndex();
+ }
+
private void checkRevision() {
checkView(revision != Revision.NULL, "missing revision on %s view", type);
checkRepositoryIndex();
@@ -332,6 +343,10 @@
return new Builder(Type.REFS);
}
+ public static Builder describe() {
+ return new Builder(Type.DESCRIBE);
+ }
+
public static Builder revision() {
return new Builder(Type.REVISION);
}
@@ -459,6 +474,9 @@
case REFS:
url.append(repositoryName).append("/+refs");
break;
+ case DESCRIBE:
+ url.append(repositoryName).append("/+describe");
+ break;
case REVISION:
url.append(repositoryName).append("/+/").append(revision.getName());
break;
@@ -522,6 +540,8 @@
* auto-diving into one-entry subtrees.
*/
public List<Map<String, String>> getBreadcrumbs(List<Boolean> hasSingleTree) {
+ checkArgument(type != Type.DESCRIBE,
+ "breadcrumbs for DESCRIBE view not supported");
checkArgument(type != Type.REFS || Strings.isNullOrEmpty(path),
"breadcrumbs for REFS view with path not supported");
checkArgument(hasSingleTree == null || type == Type.PATH,
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/RefServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/RefServlet.java
index ba52d37..8d259a8 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/RefServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/RefServlet.java
@@ -167,7 +167,7 @@
return result;
}
- private static String sanitizeRefForText(String refName) {
+ static String sanitizeRefForText(String refName) {
return refName.replace("&", "&")
.replace("<", "<")
.replace(">", ">");
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java b/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java
index 6dd167c..33b53a1 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java
@@ -39,6 +39,7 @@
private static final String VIEW_ATTIRBUTE = ViewFilter.class.getName() + "/View";
private static final String CMD_AUTO = "+";
+ private static final String CMD_DESCRIBE = "+describe";
private static final String CMD_DIFF = "+diff";
private static final String CMD_LOG = "+log";
private static final String CMD_REFS = "+refs";
@@ -98,14 +99,17 @@
String repoName = trimLeadingSlash(getRegexGroup(req, 1));
String command = getRegexGroup(req, 2);
String path = getRegexGroup(req, 3);
+ boolean emptyPath = (path.isEmpty() || path.equals("/"));
// Non-path cases.
if (repoName.isEmpty()) {
return GitilesView.hostIndex();
} else if (command.equals(CMD_REFS)) {
return GitilesView.refs().setRepositoryName(repoName).setPathPart(path);
- } else if (command.equals(CMD_LOG) && (path.isEmpty() || path.equals("/"))) {
+ } else if (command.equals(CMD_LOG) && emptyPath) {
return GitilesView.log().setRepositoryName(repoName);
+ } else if (command.equals(CMD_DESCRIBE) && !emptyPath) {
+ return GitilesView.describe().setRepositoryName(repoName).setPathPart(path);
} else if (command.isEmpty()) {
return GitilesView.repositoryIndex().setRepositoryName(repoName);
} else if (path.isEmpty()) {
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/GitilesViewTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/GitilesViewTest.java
index 49d8405..fb28533 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/GitilesViewTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/GitilesViewTest.java
@@ -165,6 +165,22 @@
view.getBreadcrumbs());
}
+ public void testDescribe() throws Exception {
+ GitilesView view = GitilesView.describe()
+ .copyFrom(HOST)
+ .setRepositoryName("foo/bar")
+ .setPathPart("deadbeef")
+ .build();
+
+ assertEquals("/b", view.getServletPath());
+ assertEquals(Type.DESCRIBE, view.getType());
+ assertEquals("host", view.getHostName());
+ assertEquals("foo/bar", view.getRepositoryName());
+ assertEquals(Revision.NULL, view.getRevision());
+ assertEquals("deadbeef", view.getPathPart());
+ assertTrue(HOST.getParameters().isEmpty());
+ }
+
public void testNoPathComponents() throws Exception {
ObjectId id = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
GitilesView view = GitilesView.path()
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/ViewFilterTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/ViewFilterTest.java
index fdb291c..3f0b727 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/ViewFilterTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/ViewFilterTest.java
@@ -135,6 +135,27 @@
assertEquals("heads/master", view.getPathPart());
}
+ public void testDescribe() throws Exception {
+ GitilesView view;
+
+ assertNull(getView("/repo/+describe"));
+ assertNull(getView("/repo/+describe/"));
+
+ view = getView("/repo/+describe/deadbeef");
+ assertEquals(Type.DESCRIBE, view.getType());
+ assertEquals("repo", view.getRepositoryName());
+ assertEquals(Revision.NULL, view.getRevision());
+ assertEquals(Revision.NULL, view.getOldRevision());
+ assertEquals("deadbeef", view.getPathPart());
+
+ view = getView("/repo/+describe/refs/heads/master~3^~2");
+ assertEquals(Type.DESCRIBE, view.getType());
+ assertEquals("repo", view.getRepositoryName());
+ assertEquals(Revision.NULL, view.getRevision());
+ assertEquals(Revision.NULL, view.getOldRevision());
+ assertEquals("refs/heads/master~3^~2", view.getPathPart());
+ }
+
public void testBranches() throws Exception {
RevCommit master = repo.branch("refs/heads/master").commit().create();
RevCommit stable = repo.branch("refs/heads/stable").commit().create();