blob: 768c0f302579942b8cf2419f75bfcc57296f02a8 [file] [log] [blame] [raw]
// 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 static org.eclipse.jgit.diff.DiffEntry.ChangeType.COPY;
import static org.eclipse.jgit.diff.DiffEntry.ChangeType.DELETE;
import static org.eclipse.jgit.diff.DiffEntry.ChangeType.RENAME;
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.http.server.ServletUtils;
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.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.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",
"archiveUrl", "archiveType"),
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 boolean contains(String key) {
return keys.contains(key);
}
}
private Linkifier linkifier;
private Repository repo;
private RevWalk walk;
private GitilesView view;
private Map<AnyObjectId, Set<Ref>> refsById;
private ArchiveFormat archiveFormat;
public CommitSoyData setLinkifier(Linkifier linkifier) {
this.linkifier = checkNotNull(linkifier, "linkifier");
return this;
}
public CommitSoyData setRevWalk(RevWalk walk) {
this.walk = checkNotNull(walk, "walk");
return this;
}
public CommitSoyData setArchiveFormat(ArchiveFormat archiveFormat) {
this.archiveFormat = checkNotNull(archiveFormat, "archiveFormat");
return this;
}
public Map<String, Object> toSoyData(HttpServletRequest req, RevCommit commit, KeySet ks,
GitDateFormatter df) throws IOException {
checkKeys(ks);
checkNotNull(req, "request");
repo = ServletUtils.getRepository(req);
view = ViewFilter.getView(req);
Map<String, Object> data = Maps.newHashMapWithExpectedSize(KeySet.DEFAULT.keys.size());
if (ks.contains("author")) {
data.put("author", toSoyData(commit.getAuthorIdent(), df));
}
if (ks.contains("committer")) {
data.put("committer", toSoyData(commit.getCommitterIdent(), df));
}
if (ks.contains("sha")) {
data.put("sha", ObjectId.toString(commit));
}
if (ks.contains("abbrevSha")) {
ObjectReader reader = repo.getObjectDatabase().newReader();
try {
data.put("abbrevSha", reader.abbreviate(commit).name());
} finally {
reader.release();
}
}
if (ks.contains("url")) {
data.put("url", GitilesView.revision()
.copyFrom(view)
.setRevision(commit)
.toUrl());
}
if (ks.contains("logUrl")) {
data.put("logUrl", urlFromView(view, commit, GitilesView.log()));
}
if (ks.contains("archiveUrl")) {
data.put("archiveUrl", urlFromView(view, commit,
GitilesView.archive().setExtension(archiveFormat.getDefaultSuffix())));
}
if (ks.contains("archiveType")) {
data.put("archiveType", archiveFormat.getShortName());
}
if (ks.contains("tree")) {
data.put("tree", ObjectId.toString(commit.getTree()));
}
if (ks.contains("treeUrl")) {
data.put("treeUrl", GitilesView.path().copyFrom(view).setPathPart("/").toUrl());
}
if (ks.contains("parents")) {
data.put("parents", toSoyData(view, commit.getParents()));
}
if (ks.contains("shortMessage")) {
data.put("shortMessage", commit.getShortMessage());
}
if (ks.contains("branches")) {
data.put("branches", getRefsById(commit, Constants.R_HEADS));
}
if (ks.contains("tags")) {
data.put("tags", getRefsById(commit, Constants.R_TAGS));
}
if (ks.contains("message")) {
if (linkifier != null) {
data.put("message", linkifier.linkify(req, commit.getFullMessage()));
} else {
data.put("message", commit.getFullMessage());
}
}
if (ks.contains("diffTree")) {
data.put("diffTree", computeDiffTree(commit));
}
checkState(ks.keys.size() == data.size(), "bad commit data keys: %s != %s", ks.keys,
data.keySet());
return ImmutableMap.copyOf(data);
}
public Map<String, Object> toSoyData(HttpServletRequest req, RevCommit commit,
GitDateFormatter df) throws IOException {
return toSoyData(req, commit, KeySet.DEFAULT, df);
}
private void checkKeys(KeySet ks) {
checkState(!ks.contains("diffTree") || walk != null, "RevWalk required for diffTree");
if (ks.contains("archiveUrl") || ks.contains("archiveType")) {
checkState(archiveFormat != null, "archive format required");
}
}
// TODO(dborowitz): Extract this.
static Map<String, String> toSoyData(PersonIdent ident, GitDateFormatter df) {
return ImmutableMap.of(
"name", ident.getName(),
"email", ident.getEmailAddress(),
"time", df.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).setPathPart("");
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)
.setPathPart("");
Revision oldRevision;
switch (commit.getParentCount()) {
case 0:
oldTree = new EmptyTreeIterator();
oldRevision = Revision.NULL;
break;
case 1:
oldTree = getTreeIterator(walk, commit.getParent(0));
oldRevision = Revision.peeled(view.getRevision().getName() + "^", commit.getParent(0));
break;
default:
// TODO(dborowitz): handle merges
return NullData.INSTANCE;
}
diffUrl.setOldRevision(oldRevision);
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);
ChangeType type = e.getChangeType();
if (type != DELETE) {
entry.put("path", e.getNewPath());
entry.put("url", GitilesView.path()
.copyFrom(view)
.setPathPart(e.getNewPath())
.toUrl());
} else {
entry.put("path", e.getOldPath());
entry.put("url", GitilesView.path()
.copyFrom(view)
.setRevision(oldRevision)
.setPathPart(e.getOldPath())
.toUrl());
}
entry.put("diffUrl", diffUrl.setAnchor("F" + result.size()).toUrl());
entry.put("changeType", e.getChangeType().toString());
if (type == COPY || type == 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) {
if (refsById == null) {
refsById = repo.getAllRefsByPeeledObjectId();
}
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;
}
private String urlFromView(GitilesView view, RevCommit commit, GitilesView.Builder builder) {
Revision rev = view.getRevision();
return builder.copyFrom(view)
.setRevision(rev.getId().equals(commit) ? rev.getName() : commit.name(), commit)
.setPathPart(null)
.toUrl();
}
}