James Moger
2016-01-25 252dc07d7f85cc344b5919bb7c6166ef84b2102e
commit | author | age
716745 1 /*
JM 2  * Copyright 2011 gitblit.com.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.gitblit.wicket.pages;
17
e3733c 18 import java.awt.Color;
008322 19 import java.text.DateFormat;
JM 20 import java.text.MessageFormat;
21 import java.text.SimpleDateFormat;
e3733c 22 import java.util.Comparator;
AL 23 import java.util.Date;
24 import java.util.HashSet;
716745 25 import java.util.List;
e3733c 26 import java.util.Map;
AL 27 import java.util.Set;
28 import java.util.TreeSet;
716745 29
e3733c 30 import org.apache.wicket.Component;
716745 31 import org.apache.wicket.PageParameters;
e3733c 32 import org.apache.wicket.behavior.SimpleAttributeModifier;
716745 33 import org.apache.wicket.markup.html.basic.Label;
JM 34 import org.apache.wicket.markup.html.link.BookmarkablePageLink;
46f33f 35 import org.apache.wicket.markup.html.link.ExternalLink;
716745 36 import org.apache.wicket.markup.repeater.Item;
JM 37 import org.apache.wicket.markup.repeater.data.DataView;
38 import org.apache.wicket.markup.repeater.data.ListDataProvider;
0078c2 39 import org.eclipse.jgit.lib.ObjectId;
716745 40 import org.eclipse.jgit.revwalk.RevCommit;
JM 41
008322 42 import com.gitblit.Keys;
JM 43 import com.gitblit.models.AnnotatedLine;
ed9d67 44 import com.gitblit.models.PathModel;
e3733c 45 import com.gitblit.utils.ColorFactory;
008322 46 import com.gitblit.utils.DiffUtils;
ed9d67 47 import com.gitblit.utils.JGitUtils;
008322 48 import com.gitblit.utils.StringUtils;
a7db57 49 import com.gitblit.wicket.CacheControl;
JM 50 import com.gitblit.wicket.CacheControl.LastModified;
716745 51 import com.gitblit.wicket.WicketUtils;
JM 52 import com.gitblit.wicket.panels.CommitHeaderPanel;
53 import com.gitblit.wicket.panels.LinkPanel;
54 import com.gitblit.wicket.panels.PathBreadcrumbsPanel;
55
a7db57 56 @CacheControl(LastModified.BOOT)
716745 57 public class BlamePage extends RepositoryPage {
JM 58
e3733c 59     /**
AL 60      * The different types of Blame visualizations.
61      */
62     private enum BlameType {
63         COMMIT,
64
65         AUTHOR,
66
67         AGE;
68
69         private BlameType() {
70         }
71
72         public static BlameType get(String name) {
73             for (BlameType blameType : BlameType.values()) {
74                 if (blameType.name().equalsIgnoreCase(name)) {
75                     return blameType;
76                 }
77             }
78             throw new IllegalArgumentException("Unknown Blame Type [" + name
79                     + "]");
80         }
81
82         @Override
83         public String toString() {
84             return name().toLowerCase();
85         }
86     }
87
716745 88     public BlamePage(PageParameters params) {
JM 89         super(params);
90
91         final String blobPath = WicketUtils.getPath(params);
e3733c 92
AL 93         final String blameTypeParam = params.getString("blametype", BlameType.COMMIT.toString());
94         final BlameType activeBlameType = BlameType.get(blameTypeParam);
716745 95
JM 96         RevCommit commit = getCommit();
46f33f 97         
PM 98         PathModel pathModel = null;
99         
100         List<PathModel> paths = JGitUtils.getFilesInPath(getRepository(), StringUtils.getRootPath(blobPath), commit);
101         for (PathModel path : paths) {
102             if (path.path.equals(blobPath)) {
103                 pathModel = path;
104                 break;
105             }
106         }
107         
108         if (pathModel == null) {
109             final String notFound = MessageFormat.format("Blame page failed to find {0} in {1} @ {2}",
110                     blobPath, repositoryName, objectId);
111             logger.error(notFound);
112             add(new Label("annotation").setVisible(false));
113             add(new Label("missingBlob", missingBlob(blobPath, commit)).setEscapeModelStrings(false));
114             return;
115         }
116         
117         if (pathModel.isFilestoreItem()) {
118             String rawUrl = JGitUtils.getLfsRepositoryUrl(getContextUrl(), repositoryName, pathModel.getFilestoreOid());
119             add(new ExternalLink("blobLink", rawUrl));
120         } else {
121             add(new BookmarkablePageLink<Void>("blobLink", BlobPage.class,
122                     WicketUtils.newPathParameter(repositoryName, objectId, blobPath)));    
123         }
124         
716745 125         add(new BookmarkablePageLink<Void>("commitLink", CommitPage.class,
JM 126                 WicketUtils.newObjectParameter(repositoryName, objectId)));
127         add(new BookmarkablePageLink<Void>("commitDiffLink", CommitDiffPage.class,
128                 WicketUtils.newObjectParameter(repositoryName, objectId)));
129
130         // blame page links
131         add(new BookmarkablePageLink<Void>("historyLink", HistoryPage.class,
132                 WicketUtils.newPathParameter(repositoryName, objectId, blobPath)));
e3733c 133
AL 134         // "Blame by" links
135         for (BlameType type : BlameType.values()) {
136             String typeString = type.toString();
137             PageParameters blameTypePageParam =
138                     WicketUtils.newBlameTypeParameter(repositoryName, commit.getName(),
139                             WicketUtils.getPath(params), typeString);
140
141             String blameByLinkText = "blameBy"
142                     + Character.toUpperCase(typeString.charAt(0)) + typeString.substring(1)
143                     + "Link";
144             BookmarkablePageLink<Void> blameByPageLink =
145                     new BookmarkablePageLink<Void>(blameByLinkText, BlamePage.class, blameTypePageParam);
146
147             if (activeBlameType == type) {
148                 blameByPageLink.add(new SimpleAttributeModifier("style", "font-weight:bold;"));
149             }
150
151             add(blameByPageLink);
152         }
716745 153
JM 154         add(new CommitHeaderPanel("commitHeader", repositoryName, commit));
155
156         add(new PathBreadcrumbsPanel("breadcrumbs", repositoryName, blobPath, objectId));
157
99d0d4 158         String format = app().settings().getString(Keys.web.datetimestampLongFormat,
a4ed6d 159                 "EEEE, MMMM d, yyyy HH:mm Z");
008322 160         final DateFormat df = new SimpleDateFormat(format);
JM 161         df.setTimeZone(getTimeZone());
699e71 162
46f33f 163         
699e71 164
46f33f 165         
699e71 166
ed9d67 167         add(new Label("missingBlob").setVisible(false));
699e71 168
310a80 169         final int tabLength = app().settings().getInteger(Keys.web.tabLength, 4);
008322 170         List<AnnotatedLine> lines = DiffUtils.blame(getRepository(), blobPath, objectId);
e3733c 171         final Map<?, String> colorMap = initializeColors(activeBlameType, lines);
008322 172         ListDataProvider<AnnotatedLine> blameDp = new ListDataProvider<AnnotatedLine>(lines);
15e560 173         DataView<AnnotatedLine> blameView = new DataView<AnnotatedLine>("annotation", blameDp) {
716745 174             private static final long serialVersionUID = 1L;
008322 175             private String lastCommitId = "";
JM 176             private boolean showInitials = true;
0078c2 177             private String zeroId = ObjectId.zeroId().getName();
716745 178
699e71 179             @Override
008322 180             public void populateItem(final Item<AnnotatedLine> item) {
e3733c 181                 final AnnotatedLine entry = item.getModelObject();
AL 182
183                 // commit id and author
008322 184                 if (!lastCommitId.equals(entry.commitId)) {
JM 185                     lastCommitId = entry.commitId;
0078c2 186                     if (zeroId.equals(entry.commitId)) {
JM 187                         // unknown commit
188                         item.add(new Label("commit", "<?>"));
189                         showInitials = false;
190                     } else {
191                         // show the link for first line
192                         LinkPanel commitLink = new LinkPanel("commit", null,
193                                 getShortObjectId(entry.commitId), CommitPage.class,
194                                 newCommitParameter(entry.commitId));
195                         WicketUtils.setHtmlTooltip(commitLink,
196                                 MessageFormat.format("{0}, {1}", entry.author, df.format(entry.when)));
197                         item.add(commitLink);
e3733c 198                         WicketUtils.setCssStyle(item, "border-top: 1px solid #ddd;");
0078c2 199                         showInitials = true;
JM 200                     }
008322 201                 } else {
JM 202                     if (showInitials) {
203                         showInitials = false;
204                         // show author initials
205                         item.add(new Label("commit", getInitials(entry.author)));
206                     } else {
207                         // hide the commit link until the next block
208                         item.add(new Label("commit").setVisible(false));
209                     }
210                 }
e3733c 211
AL 212                 // line number
213                 item.add(new Label("line", "" + entry.lineNumber));
214
215                 // line content
216                 String color;
217                 switch (activeBlameType) {
218                 case AGE:
219                     color = colorMap.get(entry.when);
220                     break;
221                 case AUTHOR:
222                     color = colorMap.get(entry.author);
223                     break;
224                 default:
225                     color = colorMap.get(entry.commitId);
226                     break;
008322 227                 }
310a80 228                 Component data = new Label("data", StringUtils.escapeForHtml(entry.data, true, tabLength)).setEscapeModelStrings(false);
e3733c 229                 data.add(new SimpleAttributeModifier("style", "background-color: " + color + ";"));
AL 230                 item.add(data);
716745 231             }
JM 232         };
233         add(blameView);
234     }
235
008322 236     private String getInitials(String author) {
JM 237         StringBuilder sb = new StringBuilder();
238         String[] chunks = author.split(" ");
239         for (String chunk : chunks) {
240             sb.append(chunk.charAt(0));
241         }
242         return sb.toString().toUpperCase();
243     }
244
716745 245     @Override
JM 246     protected String getPageName() {
247         return getString("gb.blame");
248     }
699e71 249
6ef8d7 250     @Override
5d5e55 251     protected boolean isCommitPage() {
JM 252         return true;
253     }
254
255     @Override
6ef8d7 256     protected Class<? extends BasePage> getRepoNavPageClass() {
JM 257         return TreePage.class;
258     }
699e71 259
ed9d67 260     protected String missingBlob(String blobPath, RevCommit commit) {
JM 261         StringBuilder sb = new StringBuilder();
262         sb.append("<div class=\"alert alert-error\">");
263         String pattern = getString("gb.doesNotExistInTree").replace("{0}", "<b>{0}</b>").replace("{1}", "<b>{1}</b>");
264         sb.append(MessageFormat.format(pattern, blobPath, commit.getTree().getId().getName()));
265         sb.append("</div>");
266         return sb.toString();
267     }
e3733c 268
AL 269     private Map<?, String> initializeColors(BlameType blameType, List<AnnotatedLine> lines) {
270         ColorFactory colorFactory = new ColorFactory();
271         Map<?, String> colorMap;
272
273         if (BlameType.AGE == blameType) {
274             Set<Date> keys = new TreeSet<Date>(new Comparator<Date>() {
275                 @Override
276                 public int compare(Date o1, Date o2) {
277                     // younger code has a brighter, older code lightens to white
278                     return o1.compareTo(o2);
279                 }
280             });
281
282             for (AnnotatedLine line : lines) {
283                 keys.add(line.when);
284             }
285
286             // TODO consider making this a setting
287             colorMap = colorFactory.getGraduatedColorMap(keys, Color.decode("#FFA63A"));
288         } else {
289             Set<String> keys = new HashSet<String>();
290
291             for (AnnotatedLine line : lines) {
292                 if (blameType == BlameType.AUTHOR) {
293                     keys.add(line.author);
294                 } else {
295                     keys.add(line.commitId);
296                 }
297             }
298
299             colorMap = colorFactory.getRandomColorMap(keys);
300         }
301
302         return colorMap;
303     }
716745 304 }