Merge remote-tracking branch 'master' into improve-pullreq-performance

This commit is contained in:
Tomofumi Tanaka
2013-10-31 00:56:18 +09:00
8 changed files with 303 additions and 135 deletions

View File

@@ -0,0 +1,93 @@
package util;
import org.eclipse.jgit.api.errors.PatchApplyException;
import org.eclipse.jgit.diff.RawText;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.patch.FileHeader;
import org.eclipse.jgit.patch.HunkHeader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
/**
* This class helps to apply patch. Most of these code came from {@link org.eclipse.jgit.api.ApplyCommand}.
*/
public class PatchUtil {
public static String apply(String source, String patch, FileHeader fh)
throws IOException, PatchApplyException {
RawText rt = new RawText(source.getBytes("UTF-8"));
List<String> oldLines = new ArrayList<String>(rt.size());
for (int i = 0; i < rt.size(); i++)
oldLines.add(rt.getString(i));
List<String> newLines = new ArrayList<String>(oldLines);
for (HunkHeader hh : fh.getHunks()) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(patch.getBytes("UTF-8"), hh.getStartOffset(), hh.getEndOffset() - hh.getStartOffset());
RawText hrt = new RawText(out.toByteArray());
List<String> hunkLines = new ArrayList<String>(hrt.size());
for (int i = 0; i < hrt.size(); i++)
hunkLines.add(hrt.getString(i));
int pos = 0;
for (int j = 1; j < hunkLines.size(); j++) {
String hunkLine = hunkLines.get(j);
switch (hunkLine.charAt(0)) {
case ' ':
if (!newLines.get(hh.getNewStartLine() - 1 + pos).equals(
hunkLine.substring(1))) {
throw new PatchApplyException(MessageFormat.format(
JGitText.get().patchApplyException, hh));
}
pos++;
break;
case '-':
if (!newLines.get(hh.getNewStartLine() - 1 + pos).equals(
hunkLine.substring(1))) {
throw new PatchApplyException(MessageFormat.format(
JGitText.get().patchApplyException, hh));
}
newLines.remove(hh.getNewStartLine() - 1 + pos);
break;
case '+':
newLines.add(hh.getNewStartLine() - 1 + pos,
hunkLine.substring(1));
pos++;
break;
}
}
}
if (!isNoNewlineAtEndOfFile(fh))
newLines.add(""); //$NON-NLS-1$
if (!rt.isMissingNewlineAtEnd())
oldLines.add(""); //$NON-NLS-1$
if (!isChanged(oldLines, newLines))
return null; // don't touch the file
StringBuilder sb = new StringBuilder();
for (String l : newLines) {
// don't bother handling line endings - if it was windows, the \r is
// still there!
sb.append(l).append('\n');
}
sb.deleteCharAt(sb.length() - 1);
return sb.toString();
}
private static boolean isChanged(List<String> ol, List<String> nl) {
if (ol.size() != nl.size())
return true;
for (int i = 0; i < ol.size(); i++)
if (!ol.get(i).equals(nl.get(i)))
return true;
return false;
}
private static boolean isNoNewlineAtEndOfFile(FileHeader fh) {
HunkHeader lastHunk = fh.getHunks().get(fh.getHunks().size() - 1);
RawText lhrt = new RawText(lastHunk.getBuffer());
return lhrt.getString(lhrt.size() - 1).equals(
"\\ No newline at end of file"); //$NON-NLS-1$
}
}

View File

@@ -8,7 +8,8 @@ import java.io.File
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.apache.commons.io._ import org.apache.commons.io._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.lib.PersonIdent import org.eclipse.jgit.lib.{FileMode, Constants, PersonIdent}
import org.eclipse.jgit.dircache.DirCache
class CreateRepositoryController extends CreateRepositoryControllerBase class CreateRepositoryController extends CreateRepositoryControllerBase
with RepositoryService with AccountService with WikiService with LabelsService with ActivityService with RepositoryService with AccountService with WikiService with LabelsService with ActivityService
@@ -73,13 +74,11 @@ trait CreateRepositoryControllerBase extends ControllerBase {
JGitUtil.initRepository(gitdir) JGitUtil.initRepository(gitdir)
if(form.createReadme){ if(form.createReadme){
FileUtil.withTmpDir(getInitRepositoryDir(form.owner, form.name)){ tmpdir => using(Git.open(gitdir)){ git =>
// Clone the repository val builder = DirCache.newInCore.builder()
Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
// Create README.md val content = if(form.description.nonEmpty){
FileUtils.writeStringToFile(new File(tmpdir, "README.md"),
if(form.description.nonEmpty){
form.name + "\n" + form.name + "\n" +
"===============\n" + "===============\n" +
"\n" + "\n" +
@@ -87,14 +86,14 @@ trait CreateRepositoryControllerBase extends ControllerBase {
} else { } else {
form.name + "\n" + form.name + "\n" +
"===============\n" "===============\n"
}, "UTF-8") }
val git = Git.open(tmpdir) builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
git.add.addFilepattern("README.md").call inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
git.commit builder.finish()
.setCommitter(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
.setMessage("Initial commit").call JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
git.push.call loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
} }
} }

View File

@@ -66,7 +66,7 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
val Array(from, to) = params("commitId").split("\\.\\.\\.") val Array(from, to) = params("commitId").split("\\.\\.\\.")
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
wiki.html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true), repository, wiki.html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true).filter(_.newPath == pageName + ".md"), repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info")) hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info"))
} }
}) })
@@ -96,7 +96,7 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
val Array(from, to) = params("commitId").split("\\.\\.\\.") val Array(from, to) = params("commitId").split("\\.\\.\\.")
if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, None)){ if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, None)){
redirect(s"/${repository.owner}/${repository.name}/wiki/}") redirect(s"/${repository.owner}/${repository.name}/wiki/")
} else { } else {
flash += "info" -> "This patch was not able to be reversed." flash += "info" -> "This patch was not able to be reversed."
redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/${from}...${to}") redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/${from}...${to}")
@@ -195,7 +195,7 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
private def conflictForEdit: Constraint = new Constraint(){ private def conflictForEdit: Constraint = new Constraint(){
override def validate(name: String, value: String): Option[String] = { override def validate(name: String, value: String): Option[String] = {
optionIf(targetWikiPage.map(_.id != params("id")).getOrElse(true)){ optionIf(targetWikiPage.map(_.id != params("id")).getOrElse(false)){
Some("Someone has edited the wiki since you started. Please reload this page and re-apply your changes.") Some("Someone has edited the wiki since you started. Please reload this page and re-apply your changes.")
} }
} }

View File

@@ -1,14 +1,20 @@
package service package service
import java.io.File
import java.util.Date import java.util.Date
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import util.{StringUtil, Directory, JGitUtil, LockUtil} import util.{PatchUtil, Directory, JGitUtil, LockUtil}
import util.ControlUtil._ import _root_.util.ControlUtil._
import org.eclipse.jgit.treewalk.CanonicalTreeParser import org.eclipse.jgit.treewalk.{TreeWalk, CanonicalTreeParser}
import org.eclipse.jgit.diff.DiffFormatter import org.eclipse.jgit.lib._
import org.eclipse.jgit.api.errors.PatchApplyException import org.eclipse.jgit.dircache.{DirCache, DirCacheEntry}
import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter}
import java.io.ByteArrayInputStream
import org.eclipse.jgit.patch._
import org.eclipse.jgit.api.errors.PatchFormatException
import scala.collection.JavaConverters._
object WikiService { object WikiService {
@@ -42,13 +48,8 @@ trait WikiService {
LockUtil.lock(s"${owner}/${repository}/wiki"){ LockUtil.lock(s"${owner}/${repository}/wiki"){
defining(Directory.getWikiRepositoryDir(owner, repository)){ dir => defining(Directory.getWikiRepositoryDir(owner, repository)){ dir =>
if(!dir.exists){ if(!dir.exists){
try {
JGitUtil.initRepository(dir) JGitUtil.initRepository(dir)
saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None) saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None)
} finally {
// once delete cloned repository because initial cloned repository does not have 'branch.master.merge'
FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner, repository))
}
} }
} }
} }
@@ -99,12 +100,13 @@ trait WikiService {
*/ */
def revertWikiPage(owner: String, repository: String, from: String, to: String, def revertWikiPage(owner: String, repository: String, from: String, to: String,
committer: model.Account, pageName: Option[String]): Boolean = { committer: model.Account, pageName: Option[String]): Boolean = {
LockUtil.lock(s"${owner}/${repository}/wiki"){
defining(Directory.getWikiWorkDir(owner, repository)){ workDir =>
// clone working copy
cloneOrPullWorkingCopy(workDir, owner, repository)
using(Git.open(workDir)){ git => case class RevertInfo(operation: String, filePath: String, source: String)
try {
LockUtil.lock(s"${owner}/${repository}/wiki"){
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
val reader = git.getRepository.newObjectReader val reader = git.getRepository.newObjectReader
val oldTreeIter = new CanonicalTreeParser val oldTreeIter = new CanonicalTreeParser
oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}")) oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}"))
@@ -112,7 +114,6 @@ trait WikiService {
val newTreeIter = new CanonicalTreeParser val newTreeIter = new CanonicalTreeParser
newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}")) newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}"))
import scala.collection.JavaConverters._
val diffs = git.diff.setNewTree(oldTreeIter).setOldTree(newTreeIter).call.asScala.filter { diff => val diffs = git.diff.setNewTree(oldTreeIter).setOldTree(newTreeIter).call.asScala.filter { diff =>
pageName match { pageName match {
case Some(x) => diff.getNewPath == x + ".md" case Some(x) => diff.getNewPath == x + ".md"
@@ -127,57 +128,123 @@ trait WikiService {
new String(out.toByteArray, "UTF-8") new String(out.toByteArray, "UTF-8")
} }
try { val p = new Patch()
git.apply.setPatch(new java.io.ByteArrayInputStream(patch.getBytes("UTF-8"))).call p.parse(new ByteArrayInputStream(patch.getBytes("UTF-8")))
git.add.addFilepattern(".").call if(!p.getErrors.isEmpty){
git.commit.setCommitter(committer.fullName, committer.mailAddress).setMessage(pageName match { throw new PatchFormatException(p.getErrors())
case Some(x) => s"Revert ${from} ... ${to} on ${x}"
case None => s"Revert ${from} ... ${to}"
}).call
git.push.call
true
} catch {
case ex: PatchApplyException => false
} }
val revertInfo = (p.getFiles.asScala.map { fh =>
fh.getChangeType match {
case DiffEntry.ChangeType.MODIFY => {
val source = getWikiPage(owner, repository, fh.getNewPath.replaceFirst("\\.md$", "")).map(_.content).getOrElse("")
val applied = PatchUtil.apply(source, patch, fh)
if(applied != null){
Seq(RevertInfo("ADD", fh.getNewPath, applied))
} else Nil
}
case DiffEntry.ChangeType.ADD => {
val applied = PatchUtil.apply("", patch, fh)
if(applied != null){
Seq(RevertInfo("ADD", fh.getNewPath, applied))
} else Nil
}
case DiffEntry.ChangeType.DELETE => {
Seq(RevertInfo("DELETE", fh.getNewPath, ""))
}
case DiffEntry.ChangeType.RENAME => {
val applied = PatchUtil.apply("", patch, fh)
if(applied != null){
Seq(RevertInfo("DELETE", fh.getOldPath, ""), RevertInfo("ADD", fh.getNewPath, applied))
} else {
Seq(RevertInfo("DELETE", fh.getOldPath, ""))
}
}
case _ => Nil
}
}).flatten
if(revertInfo.nonEmpty){
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
using(new RevWalk(git.getRepository)){ revWalk =>
using(new TreeWalk(git.getRepository)){ treeWalk =>
val index = treeWalk.addTree(revWalk.parseTree(headId))
treeWalk.setRecursive(true)
while(treeWalk.next){
val path = treeWalk.getPathString
val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
if(revertInfo.find(x => x.filePath == path).isEmpty){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
} }
} }
} }
} }
revertInfo.filter(_.operation == "ADD").foreach { x =>
builder.add(JGitUtil.createDirCacheEntry(x.filePath, FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, x.source.getBytes("UTF-8"))))
}
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
pageName match {
case Some(x) => s"Revert ${from} ... ${to} on ${x}"
case None => s"Revert ${from} ... ${to}"
})
}
}
}
true
} catch {
case e: Exception => {
e.printStackTrace()
false
}
}
}
/** /**
* Save the wiki page. * Save the wiki page.
*/ */
def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String, def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String,
content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = { content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = {
LockUtil.lock(s"${owner}/${repository}/wiki"){ LockUtil.lock(s"${owner}/${repository}/wiki"){
defining(Directory.getWikiWorkDir(owner, repository)){ workDir => using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
// clone working copy val builder = DirCache.newInCore.builder()
cloneOrPullWorkingCopy(workDir, owner, repository) val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
var created = true
var updated = false
var removed = false
// write as file if(headId != null){
using(Git.open(workDir)){ git => using(new RevWalk(git.getRepository)){ revWalk =>
defining(new File(workDir, newPageName + ".md")){ file => using(new TreeWalk(git.getRepository)){ treeWalk =>
// new page val index = treeWalk.addTree(revWalk.parseTree(headId))
val created = !file.exists treeWalk.setRecursive(true)
while(treeWalk.next){
// created or updated val path = treeWalk.getPathString
val added = executeIf(!file.exists || FileUtils.readFileToString(file, "UTF-8") != content){ val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
FileUtils.writeStringToFile(file, content, "UTF-8") if(path == currentPageName + ".md" && currentPageName != newPageName){
git.add.addFilepattern(file.getName).call removed = true
} else if(path != newPageName + ".md"){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
} else {
created = false
updated = JGitUtil.getContent(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false)
}
}
}
}
} }
// delete file optionIf(created || updated || removed){
val deleted = executeIf(currentPageName != "" && currentPageName != newPageName){ builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
git.rm.addFilepattern(currentPageName + ".md").call builder.finish()
} val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
if(message.trim.length == 0) {
// commit and push if(removed){
optionIf(added || deleted){
defining(git.commit.setCommitter(committer.fullName, committer.mailAddress)
.setMessage(if(message.trim.length == 0){
if(deleted){
s"Rename ${currentPageName} to ${newPageName}" s"Rename ${currentPageName} to ${newPageName}"
} else if(created){ } else if(created){
s"Created ${newPageName}" s"Created ${newPageName}"
@@ -186,12 +253,9 @@ trait WikiService {
} }
} else { } else {
message message
}).call){ commit => })
git.push.call
Some(commit.getName) Some(newHeadId)
}
}
}
} }
} }
} }
@@ -203,34 +267,33 @@ trait WikiService {
def deleteWikiPage(owner: String, repository: String, pageName: String, def deleteWikiPage(owner: String, repository: String, pageName: String,
committer: String, mailAddress: String, message: String): Unit = { committer: String, mailAddress: String, message: String): Unit = {
LockUtil.lock(s"${owner}/${repository}/wiki"){ LockUtil.lock(s"${owner}/${repository}/wiki"){
defining(Directory.getWikiWorkDir(owner, repository)){ workDir => using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
// clone working copy val builder = DirCache.newInCore.builder()
cloneOrPullWorkingCopy(workDir, owner, repository) val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
var removed = false
// delete file using(new RevWalk(git.getRepository)){ revWalk =>
new File(workDir, pageName + ".md").delete using(new TreeWalk(git.getRepository)){ treeWalk =>
val index = treeWalk.addTree(revWalk.parseTree(headId))
using(Git.open(workDir)){ git => treeWalk.setRecursive(true)
git.rm.addFilepattern(pageName + ".md").call while(treeWalk.next){
val path = treeWalk.getPathString
// commit and push val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
git.commit.setCommitter(committer, mailAddress).setMessage(message).call if(path != pageName + ".md"){
git.push.call builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
} } else {
removed = true
} }
} }
} }
private def cloneOrPullWorkingCopy(workDir: File, owner: String, repository: String): Unit = { if(removed){
if(!workDir.exists){ builder.finish()
Git.cloneRepository JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message)
.setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString) }
.setDirectory(workDir) }
.call }
.getRepository
.close
} else using(Git.open(workDir)){ git =>
git.pull.call
} }
} }

View File

@@ -56,25 +56,10 @@ object Directory {
def getDownloadWorkDir(owner: String, repository: String, sessionId: String): File = def getDownloadWorkDir(owner: String, repository: String, sessionId: String): File =
new File(getTemporaryDir(owner, repository), s"download/${sessionId}") new File(getTemporaryDir(owner, repository), s"download/${sessionId}")
/**
* Temporary directory which is used in the repository creation.
*
* GitBucket generates initial repository contents in this directory and push them.
* This directory is removed after the repository creation.
*/
def getInitRepositoryDir(owner: String, repository: String): File =
new File(getTemporaryDir(owner, repository), "init")
/** /**
* Substance directory of the wiki repository. * Substance directory of the wiki repository.
*/ */
def getWikiRepositoryDir(owner: String, repository: String): File = def getWikiRepositoryDir(owner: String, repository: String): File =
new File(s"${RepositoryHome}/${owner}/${repository}.wiki.git") new File(s"${RepositoryHome}/${owner}/${repository}.wiki.git")
/**
* Wiki working directory which is cloned from the wiki repository.
*/
def getWikiWorkDir(owner: String, repository: String): File =
new File(getTemporaryDir(owner, repository), "wiki")
} }

View File

@@ -15,6 +15,7 @@ import org.eclipse.jgit.errors.MissingObjectException
import java.util.Date import java.util.Date
import org.eclipse.jgit.api.errors.NoHeadException import org.eclipse.jgit.api.errors.NoHeadException
import service.RepositoryService import service.RepositoryService
import org.eclipse.jgit.dircache.DirCacheEntry
/** /**
* Provides complex JGit operations. * Provides complex JGit operations.
@@ -464,4 +465,32 @@ object JGitUtil {
}.find(_._1 != null) }.find(_._1 != null)
} }
def createDirCacheEntry(path: String, mode: FileMode, objectId: ObjectId): DirCacheEntry = {
val entry = new DirCacheEntry(path)
entry.setFileMode(mode)
entry.setObjectId(objectId)
entry
}
def createNewCommit(git: Git, inserter: ObjectInserter, headId: AnyObjectId, treeId: AnyObjectId,
fullName: String, mailAddress: String, message: String): String = {
val newCommit = new CommitBuilder()
newCommit.setCommitter(new PersonIdent(fullName, mailAddress))
newCommit.setAuthor(new PersonIdent(fullName, mailAddress))
newCommit.setMessage(message)
if(headId != null){
newCommit.setParentIds(List(headId).asJava)
}
newCommit.setTreeId(treeId)
val newHeadId = inserter.insert(newCommit)
inserter.flush()
val refUpdate = git.getRepository.updateRef(Constants.HEAD)
refUpdate.setNewObjectId(newHeadId)
refUpdate.update()
newHeadId.getName
}
} }

View File

@@ -57,7 +57,6 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
value value
} }
import scala.util.matching.Regex
import scala.util.matching.Regex._ import scala.util.matching.Regex._
implicit class RegexReplaceString(s: String) { implicit class RegexReplaceString(s: String) {
def replaceAll(pattern: String, replacer: (Match) => String): String = { def replaceAll(pattern: String, replacer: (Match) => String): String = {

View File

@@ -35,7 +35,7 @@
@commits.map { commit => @commits.map { commit =>
<tr> <tr>
<td width="0%"><input type="checkbox" name="commitId" value="@commit.id"></td> <td width="0%"><input type="checkbox" name="commitId" value="@commit.id"></td>
<td>@avatar(commit.committer, 20)&nbsp;<a href="@url(commit.committer)">@commit.committer</a></td> <td>@avatar(commit, 20)&nbsp;@user(commit.committer, commit.mailAddress)</td>
<td width="80%"> <td width="80%">
<span class="muted">@datetime(commit.time):</span>&nbsp;@commit.shortMessage <span class="muted">@datetime(commit.time):</span>&nbsp;@commit.shortMessage
</td> </td>