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: I045458329af4765e91d5829ce3e8d28e21eeb66etags/v1.4.0
- Revised committer verification to require a matching displayname or account name AND the email address | - 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 | - Serve repositories on both /r and /git, displaying /r because it is shorter | ||||
additions: | 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 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 branch graph image servlet based on EGit's branch graph renderer (issue-194) | ||||
- Added option to render Markdown commit messages (issue-203) | - Added option to render Markdown commit messages (issue-203) | ||||
- Guenter Dressel | - Guenter Dressel | ||||
- fpeters.fae | - fpeters.fae | ||||
- David Ostrovsky | - David Ostrovsky | ||||
- Alex Lewis | |||||
} | } | ||||
# | # |
/* | |||||
* 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)); | |||||
} | |||||
} |
gb.author = author | gb.author = author | ||||
gb.committer = committer | gb.committer = committer | ||||
gb.commit = commit | gb.commit = commit | ||||
gb.age = age | |||||
gb.tree = tree | gb.tree = tree | ||||
gb.parent = parent | gb.parent = parent | ||||
gb.url = URL | gb.url = URL |
parameterMap.put("pg", String.valueOf(pageNumber)); | parameterMap.put("pg", String.valueOf(pageNumber)); | ||||
return new PageParameters(parameterMap); | 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) { | public static String getProjectName(PageParameters params) { | ||||
return params.getString("p", ""); | return params.getString("p", ""); |
<div wicket:id="missingBlob">[missing blob]</div> | <div wicket:id="missingBlob">[missing blob]</div> | ||||
<!-- blame content --> | <!-- blame content --> | ||||
<table class="annotated" style="margin-bottom:5px;"> | |||||
<table class="annotated" style="margin-bottom:0px;"> | |||||
<tbody> | <tbody> | ||||
<tr> | <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> | ||||
<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> | </tr> | ||||
</tbody> | </tbody> | ||||
</table> | </table> |
*/ | */ | ||||
package com.gitblit.wicket.pages; | package com.gitblit.wicket.pages; | ||||
import java.awt.Color; | |||||
import java.text.DateFormat; | import java.text.DateFormat; | ||||
import java.text.MessageFormat; | import java.text.MessageFormat; | ||||
import java.text.SimpleDateFormat; | import java.text.SimpleDateFormat; | ||||
import java.util.Comparator; | |||||
import java.util.Date; | |||||
import java.util.HashSet; | |||||
import java.util.List; | 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.PageParameters; | ||||
import org.apache.wicket.behavior.SimpleAttributeModifier; | |||||
import org.apache.wicket.markup.html.basic.Label; | import org.apache.wicket.markup.html.basic.Label; | ||||
import org.apache.wicket.markup.html.link.BookmarkablePageLink; | import org.apache.wicket.markup.html.link.BookmarkablePageLink; | ||||
import org.apache.wicket.markup.repeater.Item; | import org.apache.wicket.markup.repeater.Item; | ||||
import com.gitblit.Keys; | import com.gitblit.Keys; | ||||
import com.gitblit.models.AnnotatedLine; | import com.gitblit.models.AnnotatedLine; | ||||
import com.gitblit.models.PathModel; | import com.gitblit.models.PathModel; | ||||
import com.gitblit.utils.ColorFactory; | |||||
import com.gitblit.utils.DiffUtils; | import com.gitblit.utils.DiffUtils; | ||||
import com.gitblit.utils.JGitUtils; | import com.gitblit.utils.JGitUtils; | ||||
import com.gitblit.utils.StringUtils; | import com.gitblit.utils.StringUtils; | ||||
@CacheControl(LastModified.BOOT) | @CacheControl(LastModified.BOOT) | ||||
public class BlamePage extends RepositoryPage { | 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) { | public BlamePage(PageParameters params) { | ||||
super(params); | super(params); | ||||
final String blobPath = WicketUtils.getPath(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(); | RevCommit commit = getCommit(); | ||||
add(new BookmarkablePageLink<Void>("blobLink", BlobPage.class, | add(new BookmarkablePageLink<Void>("blobLink", BlobPage.class, | ||||
add(new BookmarkablePageLink<Void>("historyLink", HistoryPage.class, | add(new BookmarkablePageLink<Void>("historyLink", HistoryPage.class, | ||||
WicketUtils.newPathParameter(repositoryName, objectId, blobPath))); | 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 CommitHeaderPanel("commitHeader", repositoryName, commit)); | ||||
add(new PathBreadcrumbsPanel("breadcrumbs", repositoryName, blobPath, objectId)); | add(new PathBreadcrumbsPanel("breadcrumbs", repositoryName, blobPath, objectId)); | ||||
add(new Label("missingBlob").setVisible(false)); | add(new Label("missingBlob").setVisible(false)); | ||||
List<AnnotatedLine> lines = DiffUtils.blame(getRepository(), blobPath, objectId); | List<AnnotatedLine> lines = DiffUtils.blame(getRepository(), blobPath, objectId); | ||||
final Map<?, String> colorMap = initializeColors(activeBlameType, lines); | |||||
ListDataProvider<AnnotatedLine> blameDp = new ListDataProvider<AnnotatedLine>(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 static final long serialVersionUID = 1L; | ||||
private int count; | |||||
private String lastCommitId = ""; | private String lastCommitId = ""; | ||||
private boolean showInitials = true; | private boolean showInitials = true; | ||||
private String zeroId = ObjectId.zeroId().getName(); | private String zeroId = ObjectId.zeroId().getName(); | ||||
@Override | @Override | ||||
public void populateItem(final Item<AnnotatedLine> item) { | 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)) { | if (!lastCommitId.equals(entry.commitId)) { | ||||
lastCommitId = entry.commitId; | lastCommitId = entry.commitId; | ||||
count++; | |||||
if (zeroId.equals(entry.commitId)) { | if (zeroId.equals(entry.commitId)) { | ||||
// unknown commit | // unknown commit | ||||
item.add(new Label("commit", "<?>")); | item.add(new Label("commit", "<?>")); | ||||
WicketUtils.setHtmlTooltip(commitLink, | WicketUtils.setHtmlTooltip(commitLink, | ||||
MessageFormat.format("{0}, {1}", entry.author, df.format(entry.when))); | MessageFormat.format("{0}, {1}", entry.author, df.format(entry.when))); | ||||
item.add(commitLink); | item.add(commitLink); | ||||
WicketUtils.setCssStyle(item, "border-top: 1px solid #ddd;"); | |||||
showInitials = true; | showInitials = true; | ||||
} | } | ||||
} else { | } else { | ||||
item.add(new Label("commit").setVisible(false)); | 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); | add(blameView); | ||||
sb.append("</div>"); | sb.append("</div>"); | ||||
return sb.toString(); | 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; | |||||
} | |||||
} | } |
} | } | ||||
table.annotated { | table.annotated { | ||||
width: 100%; | |||||
border:1px solid #ddd; | border:1px solid #ddd; | ||||
} | } | ||||
border: 0; | 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 { | table.activity { | ||||
width: 100%; | width: 100%; | ||||
margin-top: 10px; | margin-top: 10px; | ||||
white-space: nowrap; | 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-family: consolas, monospace; | ||||
font-size: 13px; | font-size: 13px; | ||||
} | } |