From e3733c7a39cb0249922c7042d6b21a10c2e21e53 Mon Sep 17 00:00:00 2001 From: Alex Lewis Date: Wed, 20 Nov 2013 17:32:27 +0000 Subject: Add coloring modes to the blame page (issue-2, pull request #125) Blame output is now colored according to Commit (default), Author or Age. Both Commit and Author output uses random colors whereas Age uses a single color with varying tints applied to indicate the age. White indicates the eldest commit with the tint darkening as the commits get younger. Change-Id: I045458329af4765e91d5829ce3e8d28e21eeb66e --- releases.moxie | 2 + src/main/java/com/gitblit/utils/ColorFactory.java | 135 +++++++++++++++++++++ .../com/gitblit/wicket/GitBlitWebApp.properties | 1 + src/main/java/com/gitblit/wicket/WicketUtils.java | 10 ++ .../java/com/gitblit/wicket/pages/BlamePage.html | 18 +-- .../java/com/gitblit/wicket/pages/BlamePage.java | 134 ++++++++++++++++++-- src/main/resources/gitblit.css | 21 +++- 7 files changed, 301 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/gitblit/utils/ColorFactory.java diff --git a/releases.moxie b/releases.moxie index 47c63b9f..a6c39bd6 100644 --- a/releases.moxie +++ b/releases.moxie @@ -39,6 +39,7 @@ r20: { - Revised committer verification to require a matching displayname or account name AND the email address - Serve repositories on both /r and /git, displaying /r because it is shorter additions: + - Added color modes for the blame page (issue-2) - Added an optional MirrorExecutor which will periodically fetch ref updates from source repositories for mirrors (issue-5). Repositories must be manually cloned using native git and "--mirror". - Added branch graph image servlet based on EGit's branch graph renderer (issue-194) - Added option to render Markdown commit messages (issue-203) @@ -82,6 +83,7 @@ r20: { - Guenter Dressel - fpeters.fae - David Ostrovsky + - Alex Lewis } # diff --git a/src/main/java/com/gitblit/utils/ColorFactory.java b/src/main/java/com/gitblit/utils/ColorFactory.java new file mode 100644 index 00000000..b40735a0 --- /dev/null +++ b/src/main/java/com/gitblit/utils/ColorFactory.java @@ -0,0 +1,135 @@ +/* + * Copyright 2013 Alex Lewis. + * Copyright 2013 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.utils; + +import java.awt.Color; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +/** + * Factory for creating color maps. + * + * @author Alex Lewis + */ +public class ColorFactory { + + private static final double MAX_TINT_FACTOR = 1; + + private static final double MIN_TINT_FACTOR = 0.2; + + private static final double FIXED_TINT_FACTOR = 0.875; + + /** + * Builds a map of the supplied keys to a random color tinted according to + * the key's position in the set. + * + * Depending on the number of keys in the set a tint is calculated from 1.0 + * (I.e. white) to a minimum tint. The keys are sorted such that the + * "lowest" value will have a full tint applied to it (1.0) with an equally + * decreasing tint applied to each key thereafter. + * + * @param keys + * The keys to create a tinted color for. + * @param baseColor + * the base color (optional) + * @return The map of key to tinted color. + */ + public Map getGraduatedColorMap(Set keys, Color baseColor) { + Map colorMap = new HashMap(); + + if (baseColor == null) { + baseColor = getRandomColor(); + } + double tintStep = (MAX_TINT_FACTOR - MIN_TINT_FACTOR) / keys.size(); + + double currentTint = MAX_TINT_FACTOR; + for (T key : keys) { + Color color = tintColor(baseColor, currentTint); + + colorMap.put(key, getColorString(color)); + + currentTint -= tintStep; + } + + return colorMap; + } + + /** + * Builds a map of the supplied keys to random colors. + * + * Each color is selected randomly and tinted with a fixed tint. + * + * @param keys The keys to create the mapped colors. + * @return The map of key to random color. + */ + public Map getRandomColorMap(Set keys) { + Map colorMap = new HashMap(); + + for (T key : keys) { + Color color = tintColor(getRandomColor(), FIXED_TINT_FACTOR); + colorMap.put(key, getColorString(color)); + } + + return colorMap; + } + + private Color getRandomColor() { + Random random = new Random(); + + Color randomColor = new Color(random.nextInt(256), random.nextInt(256), + random.nextInt(256)); + + return randomColor; + } + + private Color tintColor(Color origColor, double tintFactor) { + int tintedRed = applyTint(origColor.getRed(), tintFactor); + int tintedGreen = applyTint(origColor.getGreen(), tintFactor); + int tintedBlue = applyTint(origColor.getBlue(), tintFactor); + + Color tintedColor = new Color(tintedRed, tintedGreen, tintedBlue); + + return tintedColor; + } + + /** + * Convert the color to an HTML compatible color string in hex format E.g. + * #FF0000 + * + * @param color The color to convert + * @return The string version of the color I.e. #RRGGBB + */ + private String getColorString(Color color) { + return "#" + Integer.toHexString(color.getRGB() & 0x00ffffff); + } + + /** + * Tint the supplied color with a tint factor (0 to 1 inclusive) to make the + * colour more pale I.e. closer to white. + * + * A Tint of 0 has no effect, a Tint of 1 turns the color white. + * + * @param color The original color + * @param tintFactor The factor - 0 to 1 inclusive + * @return The tinted color. + */ + private int applyTint(int color, double tintFactor) { + return (int) (color + ((255 - color) * tintFactor)); + } +} diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties index feaa9c6e..05553316 100644 --- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties +++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties @@ -8,6 +8,7 @@ gb.tags = tags gb.author = author gb.committer = committer gb.commit = commit +gb.age = age gb.tree = tree gb.parent = parent gb.url = URL diff --git a/src/main/java/com/gitblit/wicket/WicketUtils.java b/src/main/java/com/gitblit/wicket/WicketUtils.java index 8e119da8..f1f084c6 100644 --- a/src/main/java/com/gitblit/wicket/WicketUtils.java +++ b/src/main/java/com/gitblit/wicket/WicketUtils.java @@ -434,6 +434,16 @@ public class WicketUtils { parameterMap.put("pg", String.valueOf(pageNumber)); return new PageParameters(parameterMap); } + + public static PageParameters newBlameTypeParameter(String repositoryName, + String commitId, String path, String blameType) { + Map parameterMap = new HashMap(); + parameterMap.put("r", repositoryName); + parameterMap.put("h", commitId); + parameterMap.put("f", path); + parameterMap.put("blametype", blameType); + return new PageParameters(parameterMap); + } public static String getProjectName(PageParameters params) { return params.getString("p", ""); diff --git a/src/main/java/com/gitblit/wicket/pages/BlamePage.html b/src/main/java/com/gitblit/wicket/pages/BlamePage.html index 722cf3d1..ffd2a03b 100644 --- a/src/main/java/com/gitblit/wicket/pages/BlamePage.html +++ b/src/main/java/com/gitblit/wicket/pages/BlamePage.html @@ -21,17 +21,19 @@
[missing blob]
- +
- - - + - - - - + + + +
[commit][line][content] + + [blamebycommit] | [blamebyauthor] | [blamebyage] + +
diff --git a/src/main/java/com/gitblit/wicket/pages/BlamePage.java b/src/main/java/com/gitblit/wicket/pages/BlamePage.java index 52682639..ef023b75 100644 --- a/src/main/java/com/gitblit/wicket/pages/BlamePage.java +++ b/src/main/java/com/gitblit/wicket/pages/BlamePage.java @@ -15,12 +15,21 @@ */ 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; @@ -33,6 +42,7 @@ 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; @@ -46,11 +56,43 @@ 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, @@ -66,6 +108,26 @@ public class BlamePage extends RepositoryPage { 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)); @@ -93,23 +155,21 @@ public class BlamePage extends RepositoryPage { add(new Label("missingBlob").setVisible(false)); List lines = DiffUtils.blame(getRepository(), blobPath, objectId); + final Map colorMap = initializeColors(activeBlameType, lines); ListDataProvider blameDp = new ListDataProvider(lines); - DataView blameView = new DataView("annotation", blameDp) { + DataView blameView = new DataView("blameView", blameDp) { private static final long serialVersionUID = 1L; - private int count; private String lastCommitId = ""; private boolean showInitials = true; private String zeroId = ObjectId.zeroId().getName(); @Override public void populateItem(final Item item) { - AnnotatedLine entry = item.getModelObject(); - item.add(new Label("line", "" + entry.lineNumber)); - item.add(new Label("data", StringUtils.escapeForHtml(entry.data, true)) - .setEscapeModelStrings(false)); + final AnnotatedLine entry = item.getModelObject(); + + // commit id and author if (!lastCommitId.equals(entry.commitId)) { lastCommitId = entry.commitId; - count++; if (zeroId.equals(entry.commitId)) { // unknown commit item.add(new Label("commit", "")); @@ -122,6 +182,7 @@ public class BlamePage extends RepositoryPage { 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 { @@ -134,11 +195,26 @@ public class BlamePage extends RepositoryPage { item.add(new Label("commit").setVisible(false)); } } - if (count % 2 == 0) { - WicketUtils.setCssClass(item, "even"); - } else { - WicketUtils.setCssClass(item, "odd"); + + // 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)).setEscapeModelStrings(false); + data.add(new SimpleAttributeModifier("style", "background-color: " + color + ";")); + item.add(data); } }; add(blameView); @@ -171,4 +247,40 @@ public class BlamePage extends RepositoryPage { 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; + } } diff --git a/src/main/resources/gitblit.css b/src/main/resources/gitblit.css index b7853279..c07f8bf8 100644 --- a/src/main/resources/gitblit.css +++ b/src/main/resources/gitblit.css @@ -1318,6 +1318,7 @@ table.gitnotes td.message { } table.annotated { + width: 100%; border:1px solid #ddd; } @@ -1334,6 +1335,24 @@ table.annotated td { border: 0; } +table.annotated td.lineCommit { + padding-left: 5px; + padding-right: 5px; +} + +table.annotated td.lineNumber { + border-right: 1px solid #ddd; + border-left: 1px solid #ddd; + padding-left: 5px; + padding-right: 5px; + text-align: right; +} + +table.annotated td.lineContent { + padding-left: 5px; + font: monospace; +} + table.activity { width: 100%; margin-top: 10px; @@ -1379,7 +1398,7 @@ td.date { white-space: nowrap; } -span.sha1, span.sha1 a, span.sha1 a span, .commit_message, span.shortsha1 { +span.sha1, span.sha1 a, span.sha1 a span, .commit_message, span.shortsha1, td.sha1 { font-family: consolas, monospace; font-size: 13px; } -- cgit v1.2.3