blob: 2fcca0ae15cf416484abba3432750a240da03fd3 [file] [log] [blame]
/*
* Copyright 2011 gitblit.com.
*
* 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.gitblit.wicket.pages;
import java.awt.Color;
import java.text.DateFormat;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.apache.wicket.Component;
import org.apache.wicket.PageParameters;
import org.apache.wicket.behavior.SimpleAttributeModifier;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.link.ExternalLink;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import com.gitblit.Keys;
import com.gitblit.models.AnnotatedLine;
import com.gitblit.models.PathModel;
import com.gitblit.utils.ColorFactory;
import com.gitblit.utils.DiffUtils;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.CacheControl;
import com.gitblit.wicket.CacheControl.LastModified;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.CommitHeaderPanel;
import com.gitblit.wicket.panels.LinkPanel;
import com.gitblit.wicket.panels.PathBreadcrumbsPanel;
@CacheControl(LastModified.BOOT)
public class BlamePage extends RepositoryPage {
/**
* The different types of Blame visualizations.
*/
private enum BlameType {
COMMIT,
AUTHOR,
AGE;
private BlameType() {
}
public static BlameType get(String name) {
for (BlameType blameType : BlameType.values()) {
if (blameType.name().equalsIgnoreCase(name)) {
return blameType;
}
}
throw new IllegalArgumentException("Unknown Blame Type [" + name
+ "]");
}
@Override
public String toString() {
return name().toLowerCase();
}
}
public BlamePage(PageParameters params) {
super(params);
final String blobPath = WicketUtils.getPath(params);
final String blameTypeParam = params.getString("blametype", BlameType.COMMIT.toString());
final BlameType activeBlameType = BlameType.get(blameTypeParam);
RevCommit commit = getCommit();
PathModel pathModel = null;
List<PathModel> paths = JGitUtils.getFilesInPath(getRepository(), StringUtils.getRootPath(blobPath), commit);
for (PathModel path : paths) {
if (path.path.equals(blobPath)) {
pathModel = path;
break;
}
}
if (pathModel == null) {
final String notFound = MessageFormat.format("Blame page failed to find {0} in {1} @ {2}",
blobPath, repositoryName, objectId);
logger.error(notFound);
add(new Label("annotation").setVisible(false));
add(new Label("missingBlob", missingBlob(blobPath, commit)).setEscapeModelStrings(false));
return;
}
if (pathModel.isFilestoreItem()) {
String rawUrl = JGitUtils.getLfsRepositoryUrl(getContextUrl(), repositoryName, pathModel.getFilestoreOid());
add(new ExternalLink("blobLink", rawUrl));
} else {
add(new BookmarkablePageLink<Void>("blobLink", BlobPage.class,
WicketUtils.newPathParameter(repositoryName, objectId, blobPath)));
}
add(new BookmarkablePageLink<Void>("commitLink", CommitPage.class,
WicketUtils.newObjectParameter(repositoryName, objectId)));
add(new BookmarkablePageLink<Void>("commitDiffLink", CommitDiffPage.class,
WicketUtils.newObjectParameter(repositoryName, objectId)));
// blame page links
add(new BookmarkablePageLink<Void>("historyLink", HistoryPage.class,
WicketUtils.newPathParameter(repositoryName, objectId, blobPath)));
// "Blame by" links
for (BlameType type : BlameType.values()) {
String typeString = type.toString();
PageParameters blameTypePageParam =
WicketUtils.newBlameTypeParameter(repositoryName, commit.getName(),
WicketUtils.getPath(params), typeString);
String blameByLinkText = "blameBy"
+ Character.toUpperCase(typeString.charAt(0)) + typeString.substring(1)
+ "Link";
BookmarkablePageLink<Void> blameByPageLink =
new BookmarkablePageLink<Void>(blameByLinkText, BlamePage.class, blameTypePageParam);
if (activeBlameType == type) {
blameByPageLink.add(new SimpleAttributeModifier("style", "font-weight:bold;"));
}
add(blameByPageLink);
}
add(new CommitHeaderPanel("commitHeader", repositoryName, commit));
add(new PathBreadcrumbsPanel("breadcrumbs", repositoryName, blobPath, objectId));
String format = app().settings().getString(Keys.web.datetimestampLongFormat,
"EEEE, MMMM d, yyyy HH:mm Z");
final DateFormat df = new SimpleDateFormat(format);
df.setTimeZone(getTimeZone());
add(new Label("missingBlob").setVisible(false));
final int tabLength = app().settings().getInteger(Keys.web.tabLength, 4);
List<AnnotatedLine> lines = DiffUtils.blame(getRepository(), blobPath, objectId);
final Map<?, String> colorMap = initializeColors(activeBlameType, lines);
ListDataProvider<AnnotatedLine> blameDp = new ListDataProvider<AnnotatedLine>(lines);
DataView<AnnotatedLine> blameView = new DataView<AnnotatedLine>("annotation", blameDp) {
private static final long serialVersionUID = 1L;
private String lastCommitId = "";
private boolean showInitials = true;
private String zeroId = ObjectId.zeroId().getName();
@Override
public void populateItem(final Item<AnnotatedLine> item) {
final AnnotatedLine entry = item.getModelObject();
// commit id and author
if (!lastCommitId.equals(entry.commitId)) {
lastCommitId = entry.commitId;
if (zeroId.equals(entry.commitId)) {
// unknown commit
item.add(new Label("commit", "<?>"));
showInitials = false;
} else {
// show the link for first line
LinkPanel commitLink = new LinkPanel("commit", null,
getShortObjectId(entry.commitId), CommitPage.class,
newCommitParameter(entry.commitId));
WicketUtils.setHtmlTooltip(commitLink,
MessageFormat.format("{0}, {1}", entry.author, df.format(entry.when)));
item.add(commitLink);
WicketUtils.setCssStyle(item, "border-top: 1px solid #ddd;");
showInitials = true;
}
} else {
if (showInitials) {
showInitials = false;
// show author initials
item.add(new Label("commit", getInitials(entry.author)));
} else {
// hide the commit link until the next block
item.add(new Label("commit").setVisible(false));
}
}
// line number
item.add(new Label("line", "" + entry.lineNumber));
// line content
String color;
switch (activeBlameType) {
case AGE:
color = colorMap.get(entry.when);
break;
case AUTHOR:
color = colorMap.get(entry.author);
break;
default:
color = colorMap.get(entry.commitId);
break;
}
Component data = new Label("data", StringUtils.escapeForHtml(entry.data, true, tabLength)).setEscapeModelStrings(false);
data.add(new SimpleAttributeModifier("style", "background-color: " + color + ";"));
item.add(data);
}
};
add(blameView);
}
private String getInitials(String author) {
StringBuilder sb = new StringBuilder();
String[] chunks = author.split(" ");
for (String chunk : chunks) {
sb.append(chunk.charAt(0));
}
return sb.toString().toUpperCase();
}
@Override
protected String getPageName() {
return getString("gb.blame");
}
@Override
protected boolean isCommitPage() {
return true;
}
@Override
protected Class<? extends BasePage> getRepoNavPageClass() {
return TreePage.class;
}
protected String missingBlob(String blobPath, RevCommit commit) {
StringBuilder sb = new StringBuilder();
sb.append("<div class=\"alert alert-error\">");
String pattern = getString("gb.doesNotExistInTree").replace("{0}", "<b>{0}</b>").replace("{1}", "<b>{1}</b>");
sb.append(MessageFormat.format(pattern, blobPath, commit.getTree().getId().getName()));
sb.append("</div>");
return sb.toString();
}
private Map<?, String> initializeColors(BlameType blameType, List<AnnotatedLine> lines) {
ColorFactory colorFactory = new ColorFactory();
Map<?, String> colorMap;
if (BlameType.AGE == blameType) {
Set<Date> keys = new TreeSet<Date>(new Comparator<Date>() {
@Override
public int compare(Date o1, Date o2) {
// younger code has a brighter, older code lightens to white
return o1.compareTo(o2);
}
});
for (AnnotatedLine line : lines) {
keys.add(line.when);
}
// TODO consider making this a setting
colorMap = colorFactory.getGraduatedColorMap(keys, Color.decode("#FFA63A"));
} else {
Set<String> keys = new HashSet<String>();
for (AnnotatedLine line : lines) {
if (blameType == BlameType.AUTHOR) {
keys.add(line.author);
} else {
keys.add(line.commitId);
}
}
colorMap = colorFactory.getRandomColorMap(keys);
}
return colorMap;
}
}