From e3733c7a39cb0249922c7042d6b21a10c2e21e53 Mon Sep 17 00:00:00 2001
From: Alex Lewis <alex.lewis001@gmail.com>
Date: Mon, 02 Dec 2013 14:59:40 -0500
Subject: [PATCH] Add coloring modes to the blame page (issue-2, pull request #125)

---
 src/main/java/com/gitblit/wicket/pages/BlamePage.html     |   18 +-
 src/main/resources/gitblit.css                            |   21 +++
 releases.moxie                                            |    2 
 src/main/java/com/gitblit/wicket/WicketUtils.java         |   10 +
 src/main/java/com/gitblit/wicket/GitBlitWebApp.properties |    1 
 src/main/java/com/gitblit/utils/ColorFactory.java         |  135 ++++++++++++++++++++++
 src/main/java/com/gitblit/wicket/pages/BlamePage.java     |  134 ++++++++++++++++++++-
 7 files changed, 301 insertions(+), 20 deletions(-)

diff --git a/releases.moxie b/releases.moxie
index 47c63b9..a6c39bd 100644
--- a/releases.moxie
+++ b/releases.moxie
@@ -39,6 +39,7 @@
 	- 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 @@
 	- 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 0000000..b40735a
--- /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 <T> Map<T, String> getGraduatedColorMap(Set<T> keys, Color baseColor) {
+		Map<T, String> colorMap = new HashMap<T, String>();
+
+		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 <T> Map<T, String> getRandomColorMap(Set<T> keys) {
+		Map<T, String> colorMap = new HashMap<T, String>();
+
+		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 feaa9c6..0555331 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
@@ -8,6 +8,7 @@
 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 8e119da..f1f084c 100644
--- a/src/main/java/com/gitblit/wicket/WicketUtils.java
+++ b/src/main/java/com/gitblit/wicket/WicketUtils.java
@@ -434,6 +434,16 @@
 		parameterMap.put("pg", String.valueOf(pageNumber));
 		return new PageParameters(parameterMap);
 	}
+	
+	public static PageParameters newBlameTypeParameter(String repositoryName,
+			String commitId, String path, String blameType) {
+		Map<String, String> parameterMap = new HashMap<String, String>();
+		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 722cf3d..ffd2a03 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 @@
 	<div wicket:id="missingBlob">[missing blob]</div>
 		
 	<!--  blame content -->
-	<table class="annotated" style="margin-bottom:5px;">
+	<table class="annotated" style="margin-bottom:0px;">
 		<tbody>
 			<tr>
-				<th><wicket:message key="gb.commit">[commit]</wicket:message></th>
-				<th><wicket:message key="gb.line">[line]</wicket:message></th>
-				<th><wicket:message key="gb.content">[content]</wicket:message></th>
+				<td colspan="3" class="rightAlign" style="background-color:#fbfbfb;">
+					<span class="link" style="padding-right:10px;">
+						<a wicket:id="blameByCommitLink"><wicket:message key="gb.commit">[blamebycommit]</wicket:message></a> | <a wicket:id="blameByAuthorLink"><wicket:message key="gb.author">[blamebyauthor]</wicket:message></a> | <a wicket:id="blameByAgeLink"><wicket:message key="gb.age">[blamebyage]</wicket:message></a>
+					</span>
+				</td>
 			</tr>
-			<tr wicket:id="annotation">
-				<td><span class="sha1" wicket:id="commit"></span></td>
-				<td><span class="sha1" wicket:id="line"></span></td>
-				<td><span class="sha1" wicket:id="data"></span></td>
+			<tr wicket:id="blameView">
+				<td class="lineCommit"><span class="sha1" wicket:id="commit"></span></td>
+				<td class="lineNumber"><span class="sha1" wicket:id="line"></span></td>
+				<td class="lineContent sha1" wicket:id="data"></td>
 			</tr>
 		</tbody>
 	</table>
diff --git a/src/main/java/com/gitblit/wicket/pages/BlamePage.java b/src/main/java/com/gitblit/wicket/pages/BlamePage.java
index 5268263..ef023b7 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 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,10 +56,42 @@
 @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();
 
@@ -65,6 +107,26 @@
 				WicketUtils.newPathParameter(repositoryName, Constants.HEAD, blobPath)));
 		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));
 
@@ -93,23 +155,21 @@
 		add(new Label("missingBlob").setVisible(false));
 
 		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) {
+		DataView<AnnotatedLine> blameView = new DataView<AnnotatedLine>("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<AnnotatedLine> 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 @@
 						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 @@
 						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 @@
 		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;
+	}
 }
diff --git a/src/main/resources/gitblit.css b/src/main/resources/gitblit.css
index b785327..c07f8bf 100644
--- a/src/main/resources/gitblit.css
+++ b/src/main/resources/gitblit.css
@@ -1318,6 +1318,7 @@
 }
 
 table.annotated {
+	width: 100%;
 	border:1px solid #ddd;
 }
 
@@ -1332,6 +1333,24 @@
 table.annotated td {
 	padding: 0px;
 	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 {
@@ -1379,7 +1398,7 @@
 	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;
 }

--
Gitblit v1.9.1