| // Copyright 2012 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 com.google.common.base.Preconditions.checkNotNull; |
| |
| import com.google.common.base.Strings; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.ListMultimap; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Sets; |
| import com.google.common.primitives.Longs; |
| import com.google.gitiles.CommitData.Field; |
| import com.google.gitiles.DateFormatter.Format; |
| import com.google.gitiles.GitilesRequestFailureException.FailureReason; |
| import com.google.gson.reflect.TypeToken; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.io.Writer; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.Set; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| import org.eclipse.jgit.diff.DiffConfig; |
| import org.eclipse.jgit.errors.IncorrectObjectTypeException; |
| import org.eclipse.jgit.errors.MissingObjectException; |
| import org.eclipse.jgit.http.server.ServletUtils; |
| import org.eclipse.jgit.lib.AbbreviatedObjectId; |
| import org.eclipse.jgit.lib.Constants; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.ObjectReader; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.revwalk.FollowFilter; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevObject; |
| import org.eclipse.jgit.revwalk.RevSort; |
| import org.eclipse.jgit.revwalk.RevTag; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.eclipse.jgit.revwalk.filter.AndRevFilter; |
| import org.eclipse.jgit.revwalk.filter.RevFilter; |
| import org.eclipse.jgit.treewalk.filter.AndTreeFilter; |
| import org.eclipse.jgit.treewalk.filter.PathFilterGroup; |
| import org.eclipse.jgit.treewalk.filter.TreeFilter; |
| import org.eclipse.jgit.util.StringUtils; |
| |
| /** Serves an HTML page with a shortlog for commits and paths. */ |
| public class LogServlet extends BaseServlet { |
| private static final long serialVersionUID = 1L; |
| |
| static final String LIMIT_PARAM = "n"; |
| static final String START_PARAM = "s"; |
| |
| private static final String FOLLOW_PARAM = "follow"; |
| private static final String NAME_STATUS_PARAM = "name-status"; |
| private static final String PRETTY_PARAM = "pretty"; |
| private static final String TOPO_ORDER_PARAM = "topo-order"; |
| private static final String REVERSE_PARAM = "reverse"; |
| private static final String FIRST_PARENT_PARAM = "first-parent"; |
| |
| private static final int DEFAULT_LIMIT = 100; |
| private static final int MAX_LIMIT = 10000; |
| |
| private final Linkifier linkifier; |
| |
| public LogServlet(GitilesAccess.Factory accessFactory, Renderer renderer, Linkifier linkifier) { |
| super(renderer, accessFactory); |
| this.linkifier = checkNotNull(linkifier, "linkifier"); |
| } |
| |
| @Override |
| protected void doGetHtml(HttpServletRequest req, HttpServletResponse res) throws IOException { |
| Repository repo = ServletUtils.getRepository(req); |
| GitilesView view = getView(req, repo); |
| |
| Paginator paginator = null; |
| try { |
| GitilesAccess access = getAccess(req); |
| paginator = newPaginator(repo, view, access); |
| if (paginator == null) { |
| throw new GitilesRequestFailureException(FailureReason.OBJECT_NOT_FOUND); |
| } |
| DateFormatter df = new DateFormatter(access, Format.DEFAULT); |
| |
| // Allow the user to select a logView variant with the "pretty" param. |
| String pretty = Iterables.getFirst(view.getParameters().get(PRETTY_PARAM), "default"); |
| Map<String, Object> data = Maps.newHashMapWithExpectedSize(2); |
| |
| if (!view.getRevision().nameIsId()) { |
| List<Map<String, Object>> tags = Lists.newArrayListWithExpectedSize(1); |
| for (RevObject o : RevisionServlet.listObjects(paginator.getWalk(), view.getRevision())) { |
| if (o instanceof RevTag) { |
| tags.add(new TagSoyData(linkifier, req).toSoyData(paginator.getWalk(), (RevTag) o, df)); |
| } |
| } |
| if (!tags.isEmpty()) { |
| data.put("tags", tags); |
| } |
| } |
| |
| String title = "Log - "; |
| if (!Revision.isNull(view.getOldRevision())) { |
| title += view.getRevisionRange(); |
| } else { |
| title += view.getRevision().getName(); |
| } |
| |
| data.put("title", title); |
| |
| try (OutputStream out = startRenderStreamingHtml(req, res, "gitiles.logDetail", data)) { |
| Writer w = newWriter(out, res); |
| new LogSoyData(req, access, pretty) |
| .renderStreaming(paginator, null, renderer, w, df, LogSoyData.FooterBehavior.NEXT); |
| w.flush(); |
| } |
| } finally { |
| if (paginator != null) { |
| paginator.getWalk().close(); |
| } |
| } |
| } |
| |
| @Override |
| protected void doGetJson(HttpServletRequest req, HttpServletResponse res) throws IOException { |
| Repository repo = ServletUtils.getRepository(req); |
| GitilesView view = getView(req, repo); |
| |
| Set<Field> fs = Sets.newEnumSet(CommitJsonData.DEFAULT_FIELDS, Field.class); |
| String nameStatus = Iterables.getFirst(view.getParameters().get(NAME_STATUS_PARAM), null); |
| if ("1".equals(nameStatus) || "".equals(nameStatus)) { |
| fs.add(Field.DIFF_TREE); |
| } |
| |
| Paginator paginator = null; |
| try { |
| GitilesAccess access = getAccess(req); |
| paginator = newPaginator(repo, view, access); |
| if (paginator == null) { |
| throw new GitilesRequestFailureException(FailureReason.OBJECT_NOT_FOUND); |
| } |
| DateFormatter df = new DateFormatter(access, Format.DEFAULT); |
| CommitJsonData.Log result = new CommitJsonData.Log(); |
| List<CommitJsonData.Commit> entries = Lists.newArrayListWithCapacity(paginator.getLimit()); |
| for (RevCommit c : paginator) { |
| entries.add(new CommitJsonData().toJsonData(req, paginator.getWalk(), c, fs, df)); |
| } |
| result.log = entries; |
| if (paginator.getPreviousStart() != null) { |
| result.previous = paginator.getPreviousStart().name(); |
| } |
| if (paginator.getNextStart() != null) { |
| result.next = paginator.getNextStart().name(); |
| } |
| renderJson(req, res, result, new TypeToken<CommitJsonData.Log>() {}.getType()); |
| } finally { |
| if (paginator != null) { |
| paginator.getWalk().close(); |
| } |
| } |
| } |
| |
| private static GitilesView getView(HttpServletRequest req, Repository repo) throws IOException { |
| GitilesView view = ViewFilter.getView(req); |
| if (!Revision.isNull(view.getRevision())) { |
| return view; |
| } |
| Ref headRef = repo.exactRef(Constants.HEAD); |
| if (headRef == null) { |
| return null; |
| } |
| try (RevWalk walk = new RevWalk(repo)) { |
| return GitilesView.log() |
| .copyFrom(view) |
| .setRevision(Revision.peel(Constants.HEAD, walk.parseAny(headRef.getObjectId()), walk)) |
| .build(); |
| } |
| } |
| |
| private static class InvalidStartValueException extends IllegalArgumentException { |
| private static final long serialVersionUID = 1L; |
| |
| InvalidStartValueException() { |
| super(); |
| } |
| } |
| |
| private static Optional<ObjectId> getStart( |
| ListMultimap<String, String> params, ObjectReader reader) |
| throws IOException, InvalidStartValueException { |
| List<String> values = params.get(START_PARAM); |
| switch (values.size()) { |
| case 0: |
| return Optional.empty(); |
| case 1: |
| String id = values.get(0); |
| if (!AbbreviatedObjectId.isId(id)) { |
| throw new InvalidStartValueException(); |
| } |
| Collection<ObjectId> ids = reader.resolve(AbbreviatedObjectId.fromString(id)); |
| if (ids.size() != 1) { |
| throw new InvalidStartValueException(); |
| } |
| return Optional.of(Iterables.getOnlyElement(ids)); |
| default: |
| throw new InvalidStartValueException(); |
| } |
| } |
| |
| private static RevWalk newWalk(Repository repo, GitilesView view, GitilesAccess access) |
| throws MissingObjectException, IOException { |
| RevWalk walk = new RevWalk(repo); |
| if (isTrue(view, FIRST_PARENT_PARAM)) { |
| walk.setFirstParent(true); |
| } |
| if (isTrue(view, TOPO_ORDER_PARAM)) { |
| walk.sort(RevSort.TOPO, true); |
| } |
| if (isTrue(view, REVERSE_PARAM)) { |
| walk.sort(RevSort.REVERSE, true); |
| } |
| try { |
| walk.markStart(walk.parseCommit(view.getRevision().getId())); |
| if (!Revision.isNull(view.getOldRevision())) { |
| walk.markUninteresting(walk.parseCommit(view.getOldRevision().getId())); |
| } |
| } catch (IncorrectObjectTypeException iote) { |
| return null; |
| } |
| setTreeFilter(walk, view, access); |
| setRevFilter(walk, view); |
| return walk; |
| } |
| |
| private static void setRevFilter(RevWalk walk, GitilesView view) { |
| List<RevFilter> filters = new ArrayList<>(3); |
| if (isTrue(view, "no-merges")) { |
| filters.add(RevFilter.NO_MERGES); |
| } |
| |
| String author = Iterables.getFirst(view.getParameters().get("author"), null); |
| if (author != null) { |
| filters.add(IdentRevFilter.author(author)); |
| } |
| |
| String committer = Iterables.getFirst(view.getParameters().get("committer"), null); |
| if (committer != null) { |
| filters.add(IdentRevFilter.committer(committer)); |
| } |
| |
| if (filters.size() > 1) { |
| walk.setRevFilter(AndRevFilter.create(filters)); |
| } else if (filters.size() == 1) { |
| walk.setRevFilter(filters.get(0)); |
| } |
| } |
| |
| private static void setTreeFilter(RevWalk walk, GitilesView view, GitilesAccess access) |
| throws IOException { |
| if (Strings.isNullOrEmpty(view.getPathPart())) { |
| return; |
| } |
| walk.setRewriteParents(false); |
| String path = view.getPathPart(); |
| |
| List<String> followParams = view.getParameters().get(FOLLOW_PARAM); |
| boolean follow = |
| !followParams.isEmpty() |
| ? isTrue(followParams.get(0)) |
| : access.getConfig().getBoolean("log", null, "follow", true); |
| if (follow) { |
| walk.setTreeFilter(FollowFilter.create(path, access.getConfig().get(DiffConfig.KEY))); |
| } else { |
| walk.setTreeFilter( |
| AndTreeFilter.create(PathFilterGroup.createFromStrings(path), TreeFilter.ANY_DIFF)); |
| } |
| } |
| |
| private static boolean isTrue(GitilesView view, String param) { |
| return isTrue(Iterables.getFirst(view.getParameters().get(param), null)); |
| } |
| |
| private static boolean isTrue(String v) { |
| if (v == null) { |
| return false; |
| } else if (v.isEmpty()) { |
| return true; |
| } |
| return Boolean.TRUE.equals(StringUtils.toBooleanOrNull(v)); |
| } |
| |
| private static Paginator newPaginator(Repository repo, GitilesView view, GitilesAccess access) |
| throws IOException { |
| if (view == null) { |
| return null; |
| } |
| |
| try (RevWalk walk = newWalk(repo, view, access)) { |
| if (walk == null) { |
| return null; |
| } |
| |
| try { |
| Optional<ObjectId> start = getStart(view.getParameters(), walk.getObjectReader()); |
| return new Paginator(walk, getLimit(view), start.orElse(null)); |
| } catch (InvalidStartValueException e) { |
| return null; |
| } |
| } |
| } |
| |
| private static int getLimit(GitilesView view) { |
| List<String> values = view.getParameters().get(LIMIT_PARAM); |
| if (values.isEmpty()) { |
| return DEFAULT_LIMIT; |
| } |
| Long limit = Longs.tryParse(values.get(0)); |
| if (limit == null) { |
| return DEFAULT_LIMIT; |
| } |
| return (int) Math.min(limit, MAX_LIMIT); |
| } |
| } |