blob: 768c0f302579942b8cf2419f75bfcc57296f02a8 [file] [log] [blame]
Dave Borowitz9de65952012-08-13 16:09:45 -07001// Copyright 2012 Google Inc. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package com.google.gitiles;
16
17import static com.google.common.base.Preconditions.checkNotNull;
18import static com.google.common.base.Preconditions.checkState;
Dave Borowitzd6184a62013-01-10 11:53:54 -080019import static org.eclipse.jgit.diff.DiffEntry.ChangeType.COPY;
20import static org.eclipse.jgit.diff.DiffEntry.ChangeType.DELETE;
21import static org.eclipse.jgit.diff.DiffEntry.ChangeType.RENAME;
22
Dave Borowitz9de65952012-08-13 16:09:45 -070023import com.google.common.collect.ImmutableList;
24import com.google.common.collect.ImmutableMap;
25import com.google.common.collect.ImmutableSet;
26import com.google.common.collect.Lists;
27import com.google.common.collect.Maps;
28import com.google.template.soy.data.restricted.NullData;
29
30import org.eclipse.jgit.diff.DiffEntry;
31import org.eclipse.jgit.diff.DiffEntry.ChangeType;
32import org.eclipse.jgit.diff.DiffFormatter;
Dave Borowitz3b086a72013-07-02 15:03:03 -070033import org.eclipse.jgit.http.server.ServletUtils;
Dave Borowitz9de65952012-08-13 16:09:45 -070034import org.eclipse.jgit.lib.AnyObjectId;
35import org.eclipse.jgit.lib.Constants;
36import org.eclipse.jgit.lib.ObjectId;
37import org.eclipse.jgit.lib.ObjectReader;
38import org.eclipse.jgit.lib.PersonIdent;
39import org.eclipse.jgit.lib.Ref;
40import org.eclipse.jgit.lib.Repository;
41import org.eclipse.jgit.revwalk.RevCommit;
42import org.eclipse.jgit.revwalk.RevWalk;
43import org.eclipse.jgit.treewalk.AbstractTreeIterator;
44import org.eclipse.jgit.treewalk.CanonicalTreeParser;
45import org.eclipse.jgit.treewalk.EmptyTreeIterator;
46import org.eclipse.jgit.util.GitDateFormatter;
Dave Borowitz9de65952012-08-13 16:09:45 -070047import org.eclipse.jgit.util.RelativeDateFormatter;
48import org.eclipse.jgit.util.io.NullOutputStream;
49
50import java.io.IOException;
51import java.util.Collections;
52import java.util.Comparator;
53import java.util.List;
54import java.util.Map;
55import java.util.Set;
56
Dave Borowitz9de65952012-08-13 16:09:45 -070057import javax.servlet.http.HttpServletRequest;
58
59/** Soy data converter for git commits. */
60public class CommitSoyData {
61 /** Valid sets of keys to include in Soy data for commits. */
62 public static enum KeySet {
Dave Borowitzc222cce2013-06-19 10:47:06 -070063 DETAIL("author", "committer", "sha", "tree", "treeUrl", "parents", "message", "logUrl",
Dave Borowitzc8a15682013-07-02 14:33:08 -070064 "archiveUrl", "archiveType"),
Dave Borowitz9de65952012-08-13 16:09:45 -070065 DETAIL_DIFF_TREE(DETAIL, "diffTree"),
66 SHORTLOG("abbrevSha", "url", "shortMessage", "author", "branches", "tags"),
67 DEFAULT(DETAIL);
68
69 private final Set<String> keys;
70
71 private KeySet(String... keys) {
72 this.keys = ImmutableSet.copyOf(keys);
73 }
74
75 private KeySet(KeySet other, String... keys) {
76 this.keys = ImmutableSet.<String> builder().addAll(other.keys).add(keys).build();
77 }
Dave Borowitz3b086a72013-07-02 15:03:03 -070078
79 private boolean contains(String key) {
80 return keys.contains(key);
81 }
Dave Borowitz9de65952012-08-13 16:09:45 -070082 }
83
Dave Borowitz3b086a72013-07-02 15:03:03 -070084 private Linkifier linkifier;
85 private Repository repo;
86 private RevWalk walk;
87 private GitilesView view;
88 private Map<AnyObjectId, Set<Ref>> refsById;
Dave Borowitzc8a15682013-07-02 14:33:08 -070089 private ArchiveFormat archiveFormat;
Dave Borowitz9de65952012-08-13 16:09:45 -070090
Dave Borowitz3b086a72013-07-02 15:03:03 -070091 public CommitSoyData setLinkifier(Linkifier linkifier) {
92 this.linkifier = checkNotNull(linkifier, "linkifier");
93 return this;
Dave Borowitz9de65952012-08-13 16:09:45 -070094 }
95
Dave Borowitz3b086a72013-07-02 15:03:03 -070096 public CommitSoyData setRevWalk(RevWalk walk) {
97 this.walk = checkNotNull(walk, "walk");
98 return this;
Dave Borowitz9de65952012-08-13 16:09:45 -070099 }
100
Dave Borowitzc8a15682013-07-02 14:33:08 -0700101 public CommitSoyData setArchiveFormat(ArchiveFormat archiveFormat) {
102 this.archiveFormat = checkNotNull(archiveFormat, "archiveFormat");
103 return this;
104 }
105
Dave Borowitza03760a2014-01-29 16:17:28 -0800106 public Map<String, Object> toSoyData(HttpServletRequest req, RevCommit commit, KeySet ks,
107 GitDateFormatter df) throws IOException {
Dave Borowitz3b086a72013-07-02 15:03:03 -0700108 checkKeys(ks);
Dave Borowitz3809d4f2013-08-13 10:21:53 -0700109 checkNotNull(req, "request");
Dave Borowitz3b086a72013-07-02 15:03:03 -0700110 repo = ServletUtils.getRepository(req);
111 view = ViewFilter.getView(req);
112
Dave Borowitz9de65952012-08-13 16:09:45 -0700113 Map<String, Object> data = Maps.newHashMapWithExpectedSize(KeySet.DEFAULT.keys.size());
Dave Borowitz3b086a72013-07-02 15:03:03 -0700114 if (ks.contains("author")) {
Dave Borowitza03760a2014-01-29 16:17:28 -0800115 data.put("author", toSoyData(commit.getAuthorIdent(), df));
Dave Borowitz9de65952012-08-13 16:09:45 -0700116 }
Dave Borowitz3b086a72013-07-02 15:03:03 -0700117 if (ks.contains("committer")) {
Dave Borowitza03760a2014-01-29 16:17:28 -0800118 data.put("committer", toSoyData(commit.getCommitterIdent(), df));
Dave Borowitz9de65952012-08-13 16:09:45 -0700119 }
Dave Borowitz3b086a72013-07-02 15:03:03 -0700120 if (ks.contains("sha")) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700121 data.put("sha", ObjectId.toString(commit));
122 }
Dave Borowitz3b086a72013-07-02 15:03:03 -0700123 if (ks.contains("abbrevSha")) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700124 ObjectReader reader = repo.getObjectDatabase().newReader();
125 try {
126 data.put("abbrevSha", reader.abbreviate(commit).name());
127 } finally {
128 reader.release();
129 }
130 }
Dave Borowitz3b086a72013-07-02 15:03:03 -0700131 if (ks.contains("url")) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700132 data.put("url", GitilesView.revision()
133 .copyFrom(view)
134 .setRevision(commit)
135 .toUrl());
136 }
Dave Borowitz3b086a72013-07-02 15:03:03 -0700137 if (ks.contains("logUrl")) {
Dave Borowitzc222cce2013-06-19 10:47:06 -0700138 data.put("logUrl", urlFromView(view, commit, GitilesView.log()));
139 }
Dave Borowitzc8a15682013-07-02 14:33:08 -0700140 if (ks.contains("archiveUrl")) {
141 data.put("archiveUrl", urlFromView(view, commit,
142 GitilesView.archive().setExtension(archiveFormat.getDefaultSuffix())));
143 }
144 if (ks.contains("archiveType")) {
145 data.put("archiveType", archiveFormat.getShortName());
Dave Borowitz9de65952012-08-13 16:09:45 -0700146 }
Dave Borowitz3b086a72013-07-02 15:03:03 -0700147 if (ks.contains("tree")) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700148 data.put("tree", ObjectId.toString(commit.getTree()));
149 }
Dave Borowitz3b086a72013-07-02 15:03:03 -0700150 if (ks.contains("treeUrl")) {
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700151 data.put("treeUrl", GitilesView.path().copyFrom(view).setPathPart("/").toUrl());
Dave Borowitz9de65952012-08-13 16:09:45 -0700152 }
Dave Borowitz3b086a72013-07-02 15:03:03 -0700153 if (ks.contains("parents")) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700154 data.put("parents", toSoyData(view, commit.getParents()));
155 }
Dave Borowitz3b086a72013-07-02 15:03:03 -0700156 if (ks.contains("shortMessage")) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700157 data.put("shortMessage", commit.getShortMessage());
158 }
Dave Borowitz3b086a72013-07-02 15:03:03 -0700159 if (ks.contains("branches")) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700160 data.put("branches", getRefsById(commit, Constants.R_HEADS));
161 }
Dave Borowitz3b086a72013-07-02 15:03:03 -0700162 if (ks.contains("tags")) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700163 data.put("tags", getRefsById(commit, Constants.R_TAGS));
164 }
Dave Borowitz3b086a72013-07-02 15:03:03 -0700165 if (ks.contains("message")) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700166 if (linkifier != null) {
167 data.put("message", linkifier.linkify(req, commit.getFullMessage()));
168 } else {
169 data.put("message", commit.getFullMessage());
170 }
171 }
Dave Borowitz3b086a72013-07-02 15:03:03 -0700172 if (ks.contains("diffTree")) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700173 data.put("diffTree", computeDiffTree(commit));
174 }
Dave Borowitz3b086a72013-07-02 15:03:03 -0700175 checkState(ks.keys.size() == data.size(), "bad commit data keys: %s != %s", ks.keys,
Dave Borowitz9de65952012-08-13 16:09:45 -0700176 data.keySet());
177 return ImmutableMap.copyOf(data);
178 }
179
Dave Borowitza03760a2014-01-29 16:17:28 -0800180 public Map<String, Object> toSoyData(HttpServletRequest req, RevCommit commit,
181 GitDateFormatter df) throws IOException {
182 return toSoyData(req, commit, KeySet.DEFAULT, df);
Dave Borowitz3b086a72013-07-02 15:03:03 -0700183 }
184
185 private void checkKeys(KeySet ks) {
186 checkState(!ks.contains("diffTree") || walk != null, "RevWalk required for diffTree");
Dave Borowitzc8a15682013-07-02 14:33:08 -0700187 if (ks.contains("archiveUrl") || ks.contains("archiveType")) {
188 checkState(archiveFormat != null, "archive format required");
189 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700190 }
191
192 // TODO(dborowitz): Extract this.
Dave Borowitza03760a2014-01-29 16:17:28 -0800193 static Map<String, String> toSoyData(PersonIdent ident, GitDateFormatter df) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700194 return ImmutableMap.of(
195 "name", ident.getName(),
196 "email", ident.getEmailAddress(),
Dave Borowitza03760a2014-01-29 16:17:28 -0800197 "time", df.formatDate(ident),
Dave Borowitz9de65952012-08-13 16:09:45 -0700198 // TODO(dborowitz): Switch from relative to absolute at some threshold.
199 "relativeTime", RelativeDateFormatter.format(ident.getWhen()));
200 }
201
202 private List<Map<String, String>> toSoyData(GitilesView view, RevCommit[] parents) {
203 List<Map<String, String>> result = Lists.newArrayListWithCapacity(parents.length);
204 int i = 1;
205 // TODO(dborowitz): Render something slightly different when we're actively
206 // viewing a diff against one of the parents.
207 for (RevCommit parent : parents) {
208 String name = parent.name();
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700209 GitilesView.Builder diff = GitilesView.diff().copyFrom(view).setPathPart("");
Dave Borowitz9de65952012-08-13 16:09:45 -0700210 String parentName;
211 if (parents.length == 1) {
212 parentName = view.getRevision().getName() + "^";
213 } else {
214 parentName = view.getRevision().getName() + "^" + (i++);
215 }
216 result.add(ImmutableMap.of(
217 "sha", name,
218 "url", GitilesView.revision()
219 .copyFrom(view)
220 .setRevision(parentName, parent)
221 .toUrl(),
222 "diffUrl", diff.setOldRevision(parentName, parent).toUrl()));
223 }
224 return result;
225 }
226
227 private AbstractTreeIterator getTreeIterator(RevWalk walk, RevCommit commit) throws IOException {
228 CanonicalTreeParser p = new CanonicalTreeParser();
229 p.reset(walk.getObjectReader(), walk.parseTree(walk.parseCommit(commit).getTree()));
230 return p;
231 }
232
233 private Object computeDiffTree(RevCommit commit) throws IOException {
234 AbstractTreeIterator oldTree;
235 GitilesView.Builder diffUrl = GitilesView.diff().copyFrom(view)
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700236 .setPathPart("");
Dave Borowitzd6184a62013-01-10 11:53:54 -0800237 Revision oldRevision;
Dave Borowitz9de65952012-08-13 16:09:45 -0700238 switch (commit.getParentCount()) {
239 case 0:
240 oldTree = new EmptyTreeIterator();
Dave Borowitzd6184a62013-01-10 11:53:54 -0800241 oldRevision = Revision.NULL;
Dave Borowitz9de65952012-08-13 16:09:45 -0700242 break;
243 case 1:
244 oldTree = getTreeIterator(walk, commit.getParent(0));
Dave Borowitzd6184a62013-01-10 11:53:54 -0800245 oldRevision = Revision.peeled(view.getRevision().getName() + "^", commit.getParent(0));
Dave Borowitz9de65952012-08-13 16:09:45 -0700246 break;
247 default:
248 // TODO(dborowitz): handle merges
249 return NullData.INSTANCE;
250 }
Dave Borowitzd6184a62013-01-10 11:53:54 -0800251 diffUrl.setOldRevision(oldRevision);
Dave Borowitz9de65952012-08-13 16:09:45 -0700252 AbstractTreeIterator newTree = getTreeIterator(walk, commit);
253
254 DiffFormatter diff = new DiffFormatter(NullOutputStream.INSTANCE);
255 try {
256 diff.setRepository(repo);
257 diff.setDetectRenames(true);
258
259 List<Object> result = Lists.newArrayList();
260 for (DiffEntry e : diff.scan(oldTree, newTree)) {
261 Map<String, Object> entry = Maps.newHashMapWithExpectedSize(5);
Dave Borowitzd6184a62013-01-10 11:53:54 -0800262 ChangeType type = e.getChangeType();
263 if (type != DELETE) {
264 entry.put("path", e.getNewPath());
265 entry.put("url", GitilesView.path()
266 .copyFrom(view)
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700267 .setPathPart(e.getNewPath())
Dave Borowitzd6184a62013-01-10 11:53:54 -0800268 .toUrl());
269 } else {
270 entry.put("path", e.getOldPath());
271 entry.put("url", GitilesView.path()
272 .copyFrom(view)
273 .setRevision(oldRevision)
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700274 .setPathPart(e.getOldPath())
Dave Borowitzd6184a62013-01-10 11:53:54 -0800275 .toUrl());
276 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700277 entry.put("diffUrl", diffUrl.setAnchor("F" + result.size()).toUrl());
278 entry.put("changeType", e.getChangeType().toString());
Dave Borowitzd6184a62013-01-10 11:53:54 -0800279 if (type == COPY || type == RENAME) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700280 entry.put("oldPath", e.getOldPath());
281 }
282 result.add(entry);
283 }
284 return result;
285 } finally {
286 diff.release();
287 }
288 }
289
290 private static final Comparator<Map<String, String>> NAME_COMPARATOR =
291 new Comparator<Map<String, String>>() {
292 @Override
293 public int compare(Map<String, String> o1, Map<String, String> o2) {
294 return o1.get("name").compareTo(o2.get("name"));
295 }
296 };
297
298 private List<Map<String, String>> getRefsById(ObjectId id, String prefix) {
Dave Borowitz3b086a72013-07-02 15:03:03 -0700299 if (refsById == null) {
300 refsById = repo.getAllRefsByPeeledObjectId();
301 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700302 Set<Ref> refs = refsById.get(id);
303 if (refs == null) {
304 return ImmutableList.of();
305 }
306 List<Map<String, String>> result = Lists.newArrayListWithCapacity(refs.size());
307 for (Ref ref : refs) {
308 if (ref.getName().startsWith(prefix)) {
309 result.add(ImmutableMap.of(
310 "name", ref.getName().substring(prefix.length()),
311 "url", GitilesView.revision()
312 .copyFrom(view)
313 .setRevision(Revision.unpeeled(ref.getName(), ref.getObjectId()))
314 .toUrl()));
315 }
316 }
317 Collections.sort(result, NAME_COMPARATOR);
318 return result;
319 }
Dave Borowitzc222cce2013-06-19 10:47:06 -0700320
321 private String urlFromView(GitilesView view, RevCommit commit, GitilesView.Builder builder) {
322 Revision rev = view.getRevision();
323 return builder.copyFrom(view)
324 .setRevision(rev.getId().equals(commit) ? rev.getName() : commit.name(), commit)
325 .setPathPart(null)
326 .toUrl();
327 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700328}