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();