Java tutorial
// 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 static com.google.common.base.Preconditions.checkState; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.template.soy.data.restricted.NullData; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffEntry.ChangeType; import org.eclipse.jgit.diff.DiffFormatter; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.AbstractTreeIterator; import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.EmptyTreeIterator; import org.eclipse.jgit.util.GitDateFormatter; import org.eclipse.jgit.util.GitDateFormatter.Format; import org.eclipse.jgit.util.RelativeDateFormatter; import org.eclipse.jgit.util.io.NullOutputStream; import java.io.IOException; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; import javax.servlet.http.HttpServletRequest; /** Soy data converter for git commits. */ public class CommitSoyData { /** Valid sets of keys to include in Soy data for commits. */ public static enum KeySet { DETAIL("author", "committer", "sha", "tree", "treeUrl", "parents", "message", "logUrl"), DETAIL_DIFF_TREE( DETAIL, "diffTree"), SHORTLOG("abbrevSha", "url", "shortMessage", "author", "branches", "tags"), DEFAULT(DETAIL); private final Set<String> keys; private KeySet(String... keys) { this.keys = ImmutableSet.copyOf(keys); } private KeySet(KeySet other, String... keys) { this.keys = ImmutableSet.<String>builder().addAll(other.keys).add(keys).build(); } } private final Linkifier linkifier; private final HttpServletRequest req; private final Repository repo; private final RevWalk walk; private final GitilesView view; private final Map<AnyObjectId, Set<Ref>> refsById; private final GitDateFormatter dateFormatter; // TODO(dborowitz): This constructor is getting a bit ridiculous. public CommitSoyData(@Nullable Linkifier linkifier, HttpServletRequest req, Repository repo, RevWalk walk, GitilesView view) { this(linkifier, req, repo, walk, view, null); } public CommitSoyData(@Nullable Linkifier linkifier, HttpServletRequest req, Repository repo, RevWalk walk, GitilesView view, @Nullable Map<AnyObjectId, Set<Ref>> refsById) { this.linkifier = linkifier; this.req = req; this.repo = repo; this.walk = walk; this.view = view; this.refsById = refsById; this.dateFormatter = new GitDateFormatter(Format.DEFAULT); } public Map<String, Object> toSoyData(RevCommit commit, KeySet keys) throws IOException { Map<String, Object> data = Maps.newHashMapWithExpectedSize(KeySet.DEFAULT.keys.size()); if (keys.keys.contains("author")) { data.put("author", toSoyData(commit.getAuthorIdent(), dateFormatter)); } if (keys.keys.contains("committer")) { data.put("committer", toSoyData(commit.getCommitterIdent(), dateFormatter)); } if (keys.keys.contains("sha")) { data.put("sha", ObjectId.toString(commit)); } if (keys.keys.contains("abbrevSha")) { ObjectReader reader = repo.getObjectDatabase().newReader(); try { data.put("abbrevSha", reader.abbreviate(commit).name()); } finally { reader.release(); } } if (keys.keys.contains("url")) { data.put("url", GitilesView.revision().copyFrom(view).setRevision(commit).toUrl()); } if (keys.keys.contains("logUrl")) { Revision rev = view.getRevision(); GitilesView.Builder logView = GitilesView.log().copyFrom(view) .setRevision(rev.getId().equals(commit) ? rev.getName() : commit.name(), commit) .setOldRevision(Revision.NULL).setTreePath(null); data.put("logUrl", logView.toUrl()); } if (keys.keys.contains("tree")) { data.put("tree", ObjectId.toString(commit.getTree())); } if (keys.keys.contains("treeUrl")) { data.put("treeUrl", GitilesView.path().copyFrom(view).setTreePath("/").toUrl()); } if (keys.keys.contains("parents")) { data.put("parents", toSoyData(view, commit.getParents())); } if (keys.keys.contains("shortMessage")) { data.put("shortMessage", commit.getShortMessage()); } if (keys.keys.contains("branches")) { data.put("branches", getRefsById(commit, Constants.R_HEADS)); } if (keys.keys.contains("tags")) { data.put("tags", getRefsById(commit, Constants.R_TAGS)); } if (keys.keys.contains("message")) { if (linkifier != null) { data.put("message", linkifier.linkify(req, commit.getFullMessage())); } else { data.put("message", commit.getFullMessage()); } } if (keys.keys.contains("diffTree")) { data.put("diffTree", computeDiffTree(commit)); } checkState(keys.keys.size() == data.size(), "bad commit data keys: %s != %s", keys.keys, data.keySet()); return ImmutableMap.copyOf(data); } public Map<String, Object> toSoyData(RevCommit commit) throws IOException { return toSoyData(commit, KeySet.DEFAULT); } // TODO(dborowitz): Extract this. static Map<String, String> toSoyData(PersonIdent ident, GitDateFormatter dateFormatter) { return ImmutableMap.of("name", ident.getName(), "email", ident.getEmailAddress(), "time", dateFormatter.formatDate(ident), // TODO(dborowitz): Switch from relative to absolute at some threshold. "relativeTime", RelativeDateFormatter.format(ident.getWhen())); } private List<Map<String, String>> toSoyData(GitilesView view, RevCommit[] parents) { List<Map<String, String>> result = Lists.newArrayListWithCapacity(parents.length); int i = 1; // TODO(dborowitz): Render something slightly different when we're actively // viewing a diff against one of the parents. for (RevCommit parent : parents) { String name = parent.name(); GitilesView.Builder diff = GitilesView.diff().copyFrom(view).setTreePath(""); String parentName; if (parents.length == 1) { parentName = view.getRevision().getName() + "^"; } else { parentName = view.getRevision().getName() + "^" + (i++); } result.add(ImmutableMap.of("sha", name, "url", GitilesView.revision().copyFrom(view).setRevision(parentName, parent).toUrl(), "diffUrl", diff.setOldRevision(parentName, parent).toUrl())); } return result; } private AbstractTreeIterator getTreeIterator(RevWalk walk, RevCommit commit) throws IOException { CanonicalTreeParser p = new CanonicalTreeParser(); p.reset(walk.getObjectReader(), walk.parseTree(walk.parseCommit(commit).getTree())); return p; } private Object computeDiffTree(RevCommit commit) throws IOException { AbstractTreeIterator oldTree; GitilesView.Builder diffUrl = GitilesView.diff().copyFrom(view).setTreePath(""); switch (commit.getParentCount()) { case 0: oldTree = new EmptyTreeIterator(); diffUrl.setOldRevision(Revision.NULL); break; case 1: oldTree = getTreeIterator(walk, commit.getParent(0)); diffUrl.setOldRevision(view.getRevision().getName() + "^", commit.getParent(0)); break; default: // TODO(dborowitz): handle merges return NullData.INSTANCE; } AbstractTreeIterator newTree = getTreeIterator(walk, commit); DiffFormatter diff = new DiffFormatter(NullOutputStream.INSTANCE); try { diff.setRepository(repo); diff.setDetectRenames(true); List<Object> result = Lists.newArrayList(); for (DiffEntry e : diff.scan(oldTree, newTree)) { Map<String, Object> entry = Maps.newHashMapWithExpectedSize(5); entry.put("path", e.getNewPath()); entry.put("url", GitilesView.path().copyFrom(view).setTreePath(e.getNewPath()).toUrl()); entry.put("diffUrl", diffUrl.setAnchor("F" + result.size()).toUrl()); entry.put("changeType", e.getChangeType().toString()); if (e.getChangeType() == ChangeType.COPY || e.getChangeType() == ChangeType.RENAME) { entry.put("oldPath", e.getOldPath()); } result.add(entry); } return result; } finally { diff.release(); } } private static final Comparator<Map<String, String>> NAME_COMPARATOR = new Comparator<Map<String, String>>() { @Override public int compare(Map<String, String> o1, Map<String, String> o2) { return o1.get("name").compareTo(o2.get("name")); } }; private List<Map<String, String>> getRefsById(ObjectId id, String prefix) { checkNotNull(refsById, "must pass in ID to ref map to look up refs by ID"); Set<Ref> refs = refsById.get(id); if (refs == null) { return ImmutableList.of(); } List<Map<String, String>> result = Lists.newArrayListWithCapacity(refs.size()); for (Ref ref : refs) { if (ref.getName().startsWith(prefix)) { result.add(ImmutableMap.of("name", ref.getName().substring(prefix.length()), "url", GitilesView.revision().copyFrom(view) .setRevision(Revision.unpeeled(ref.getName(), ref.getObjectId())).toUrl())); } } Collections.sort(result, NAME_COMPARATOR); return result; } }