/* * 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("blobLink", BlobPage.class, WicketUtils.newPathParameter(repositoryName, objectId, blobPath))); add(new BookmarkablePageLink("commitLink", CommitPage.class, WicketUtils.newObjectParameter(repositoryName, objectId))); add(new BookmarkablePageLink("commitDiffLink", CommitDiffPage.class, WicketUtils.newObjectParameter(repositoryName, objectId))); // blame page links add(new BookmarkablePageLink("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 blameByPageLink = new BookmarkablePageLink(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 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 lines = DiffUtils.blame(getRepository(), blobPath, objectId); final Map colorMap = initializeColors(activeBlameType, lines); ListDataProvider blameDp = new ListDataProvider(lines); DataView blameView = new DataView("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 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 getRepoNavPageClass() { return TreePage.class; } protected String missingBlob(String blobPath, RevCommit commit) { StringBuilder sb = new StringBuilder(); sb.append("
"); String pattern = getString("gb.doesNotExistInTree").replace("{0}", "{0}").replace("{1}", "{1}"); sb.append(MessageFormat.format(pattern, blobPath, commit.getTree().getId().getName())); sb.append("
"); return sb.toString(); } private Map initializeColors(BlameType blameType, List lines) { ColorFactory colorFactory = new ColorFactory(); Map colorMap; if (BlameType.AGE == blameType) { Set keys = new TreeSet(new Comparator() { @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 keys = new HashSet(); for (AnnotatedLine line : lines) { if (blameType == BlameType.AUTHOR) { keys.add(line.author); } else { keys.add(line.commitId); } } colorMap = colorFactory.getRandomColorMap(keys); } return colorMap; } }