| // 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.nio.charset.StandardCharsets.UTF_8; | |
| 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.HashMap; | |
| import java.util.HashSet; | |
| 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.RefDatabase; | |
| 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<String, Map<AnyObjectId, List<Ref>>> refsByIdByPrefix; | |
| 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 (refsByIdByPrefix == null) { | |
| refsByIdByPrefix = new HashMap<>(); | |
| } | |
| Map<AnyObjectId, List<Ref>> refsById = refsByIdByPrefix.get(prefix); | |
| if (refsById == null) { | |
| // Index only refs under this namespace. getAllRefsByPeeledObjectId peels every ref, which | |
| // is catastrophic on repos with many refs (e.g. Gerrit's refs/changes/*). | |
| Map<AnyObjectId, Set<Ref>> unsortedRefsById = new HashMap<>(); | |
| RefDatabase refDb = repo.getRefDatabase(); | |
| for (Ref ref : refDb.getRefsByPrefix(prefix)) { | |
| ObjectId target; | |
| try { | |
| target = refDb.peel(ref).getPeeledObjectId(); | |
| } catch (IOException e) { | |
| // Treat an unpeelable ref (e.g. missing target) as unpeeled rather than failing the | |
| // whole page, like Repository#peel. | |
| target = null; | |
| } | |
| if (target == null) { | |
| target = ref.getObjectId(); | |
| } | |
| if (target != null) { | |
| unsortedRefsById.computeIfAbsent(target, k -> new HashSet<>()).add(ref); | |
| } | |
| } | |
| refsById = new HashMap<>(); | |
| for (Map.Entry<AnyObjectId, Set<Ref>> e : unsortedRefsById.entrySet()) { | |
| refsById.put( | |
| e.getKey(), e.getValue().stream().sorted(comparing(Ref::getName)).collect(toList())); | |
| } | |
| refsByIdByPrefix.put(prefix, refsById); | |
| } | |
| return refsById.getOrDefault(id, ImmutableList.of()); | |
| } | |
| 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; | |
| } |