/* | |
* 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.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(); | |
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()); | |
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; | |
} | |
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; | |
} | |
} |