blob: 203051b72488403351c88ee56833a21d4c637d15 [file] [log] [blame]
// Copyright (C) 2014 The Android Open Source Project
//
// 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 java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.http.server.ServletUtils;
import org.eclipse.jgit.lib.AbbreviatedObjectId;
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.notes.NoteMap;
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.io.NullOutputStream;
/** Format-independent data about a single commit. */
class CommitData {
enum Field {
ABBREV_SHA,
ARCHIVE_TYPE,
ARCHIVE_URL,
AUTHOR,
BRANCHES,
COMMITTER,
DIFF_TREE,
LOG_URL,
MESSAGE,
NOTES,
PARENTS,
PARENT_BLAME_URL,
SHA,
SHORT_MESSAGE,
TAGS,
TREE,
TREE_URL,
URL;
static ImmutableSet<Field> setOf(Iterable<Field> base, Field... fields) {
return Sets.immutableEnumSet(Iterables.concat(base, Arrays.asList(fields)));
}
}
static class DiffList {
Revision revision;
Revision oldRevision;
List<DiffEntry> entries;
}
static class Builder {
private ArchiveFormat archiveFormat;
private Map<AnyObjectId, Set<Ref>> refsById;
private static final int MAX_NOTE_SIZE = 524288;
Builder setArchiveFormat(@Nullable ArchiveFormat archiveFormat) {
this.archiveFormat = archiveFormat;
return this;
}
CommitData build(HttpServletRequest req, RevWalk walk, RevCommit c, Set<Field> fs)
throws IOException {
checkFields(fs);
checkNotNull(req, "request");
checkNotNull(walk, "walk");
Repository repo = ServletUtils.getRepository(req);
GitilesView view = ViewFilter.getView(req);
CommitData result = new CommitData();
if (fs.contains(Field.AUTHOR)) {
walk.parseBody(c);
result.author = c.getAuthorIdent();
}
if (fs.contains(Field.COMMITTER)) {
walk.parseBody(c);
result.committer = c.getCommitterIdent();
}
if (fs.contains(Field.SHA)) {
result.sha = c.copy();
}
if (fs.contains(Field.ABBREV_SHA)) {
try (ObjectReader reader = repo.getObjectDatabase().newReader()) {
result.abbrev = reader.abbreviate(c);
}
}
if (fs.contains(Field.URL)) {
result.url = GitilesView.revision().copyFrom(view).setRevision(c).toUrl();
}
if (fs.contains(Field.LOG_URL)) {
result.logUrl = urlFromView(view, c, GitilesView.log());
}
if (fs.contains(Field.ARCHIVE_URL)) {
result.archiveUrl =
urlFromView(
view, c, GitilesView.archive().setExtension(archiveFormat.getDefaultSuffix()));
}
if (fs.contains(Field.ARCHIVE_TYPE)) {
result.archiveType = archiveFormat;
}
if (fs.contains(Field.TREE)) {
result.tree = c.getTree().copy();
}
if (fs.contains(Field.TREE_URL)) {
// Tree always implies the root tree.
result.treeUrl = GitilesView.path().copyFrom(view).setPathPart("/").toUrl();
}
if (fs.contains(Field.PARENTS)) {
result.parents = Arrays.asList(c.getParents());
}
if (fs.contains(Field.BRANCHES)) {
result.branches = getRefsById(repo, c, Constants.R_HEADS);
}
if (fs.contains(Field.TAGS)) {
result.tags = getRefsById(repo, c, Constants.R_TAGS);
}
if (fs.contains(Field.NOTES)) {
Ref notesRef = repo.getRefDatabase().exactRef(Constants.R_NOTES_COMMITS);
if (notesRef != null) {
try {
byte[] data =
NoteMap.read(walk.getObjectReader(), walk.parseCommit(notesRef.getObjectId()))
.getCachedBytes(c, MAX_NOTE_SIZE);
result.notes = new String(data, "utf-8");
} catch (Exception e) {
result.notes = "";
}
}
}
if (fs.contains(Field.MESSAGE)) {
walk.parseBody(c);
result.message = c.getFullMessage();
}
if (fs.contains(Field.SHORT_MESSAGE)) {
walk.parseBody(c);
String msg = c.getShortMessage();
if (msg.length() > 80) {
String ft = result.message;
if (ft == null) {
ft = c.getFullMessage();
}
int lf = ft.indexOf('\n');
if (lf > 0) {
msg = ft.substring(0, lf);
}
}
result.shortMessage = msg;
}
if (fs.contains(Field.DIFF_TREE)) {
result.diffEntries = computeDiffEntries(repo, view, walk, c);
}
return result;
}
private void checkFields(Set<Field> fs) {
if (fs.contains(Field.ARCHIVE_URL) || fs.contains(Field.ARCHIVE_TYPE)) {
checkState(archiveFormat != null, "archive format required");
}
}
private static String urlFromView(
GitilesView view, RevCommit commit, GitilesView.Builder builder) {
Revision rev = view.getRevision();
return builder
.copyFrom(view)
.setOldRevision(Revision.NULL)
.setRevision(rev.getId().equals(commit) ? rev.getName() : commit.name(), commit)
.setPathPart(view.getPathPart())
.toUrl();
}
private List<Ref> getRefsById(Repository repo, ObjectId id, String prefix) throws IOException {
if (refsById == null) {
refsById = repo.getAllRefsByPeeledObjectId();
}
Set<Ref> refs = refsById.get(id);
if (refs == null) {
return ImmutableList.of();
}
return refs.stream()
.filter(r -> r.getName().startsWith(prefix))
.sorted(comparing(Ref::getName))
.collect(toList());
}
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 DiffList computeDiffEntries(
Repository repo, GitilesView view, RevWalk walk, RevCommit commit) throws IOException {
DiffList result = new DiffList();
result.revision =
view.getRevision().matches(commit)
? view.getRevision()
: Revision.peeled(commit.name(), commit);
AbstractTreeIterator oldTree;
switch (commit.getParentCount()) {
case 0:
result.oldRevision = Revision.NULL;
oldTree = new EmptyTreeIterator();
break;
case 1:
result.oldRevision =
Revision.peeled(result.revision.getName() + "^", commit.getParent(0));
oldTree = getTreeIterator(walk, commit.getParent(0));
break;
default:
// TODO(dborowitz): handle merges
return result;
}
AbstractTreeIterator newTree = getTreeIterator(walk, commit);
try (DiffFormatter diff = new DiffFormatter(NullOutputStream.INSTANCE)) {
diff.setRepository(repo);
diff.setDetectRenames(true);
result.entries = diff.scan(oldTree, newTree);
return result;
}
}
}
ObjectId sha;
PersonIdent author;
PersonIdent committer;
AbbreviatedObjectId abbrev;
ObjectId tree;
List<RevCommit> parents;
String shortMessage;
String message;
String notes;
List<Ref> branches;
List<Ref> tags;
DiffList diffEntries;
String url;
String logUrl;
String treeUrl;
String archiveUrl;
ArchiveFormat archiveType;
}