Initial blame support
Initial blame support

file:a/gitphp.css -> file:b/gitphp.css
--- a/gitphp.css
+++ b/gitphp.css
@@ -110,12 +110,18 @@
 }
 table.code td {
 	padding: 0px 0px;
-	font-family:monospace; font-size:12px; white-space:pre;
 }
 table.code td.num {
 	text-align: right;
+	font-family:monospace; font-size:12px; white-space:pre;
 }
 table.code td.codeline {
 	padding-left: 5px;
+	font-family:monospace; font-size:12px; white-space:pre;
 }
+table.code td.author, table.code td.date {
+	white-space: nowrap;
+}
+table.code tr.light:hover { background-color:#ffffff; }
+table.code tr.dark:hover { background-color:#f6f6f0; }
 

--- a/include/defs.commands.php
+++ b/include/defs.commands.php
@@ -15,6 +15,7 @@
 define('GIT_SHOW_REF','show-ref');
 define('GIT_ARCHIVE','archive');
 define('GIT_GREP','grep');
+define('GIT_BLAME','blame');
 
 ?>
 

--- /dev/null
+++ b/include/display.git_blame.php
@@ -1,1 +1,58 @@
+<?php
+/*
+ * display.git_blame.php
+ * gitphp: A PHP git repository browser
+ * Component: Display - blame
+ *
+ * Copyright (C) 2010 Christopher Han <xiphux@gmail.com>
+ */
 
+require_once('gitutil.git_read_head.php');
+require_once('gitutil.git_parse_blame.php');
+require_once('gitutil.git_read_commit.php');
+require_once('gitutil.read_info_ref.php');
+require_once('gitutil.git_path_trees.php');
+
+function git_blame($projectroot, $project, $hash, $file, $hashbase)
+{
+	global $tpl;
+
+	$cachekey = sha1($project) . "|" . sha1($file) . "|" . $hashbase;
+
+	if (!$tpl->is_cached('blame.tpl', $cachekey)) {
+		$head = git_read_head($projectroot . $project);
+		if (!isset($hashbase))
+			$hashbase = $head;
+		if (!isset($hash) && isset($file))
+			$hash = git_get_hash_by_path($projectroot . $project, $hashbase,$file,"blob");
+		$tpl->assign("hash",$hash);
+		$tpl->assign("hashbase",$hashbase);
+		$tpl->assign("head", $head);
+		if ($co = git_read_commit($projectroot . $project, $hashbase)) {
+			$tpl->assign("fullnav",TRUE);
+			$refs = read_info_ref($projectroot . $project);
+			$tpl->assign("tree",$co['tree']);
+			$tpl->assign("title",$co['title']);
+			if (isset($file))
+				$tpl->assign("file",$file);
+			if ($hashbase == "HEAD") {
+				if (isset($refs[$head]))
+					$tpl->assign("hashbaseref",$refs[$head]);
+			} else {
+				if (isset($refs[$hashbase]))
+					$tpl->assign("hashbaseref",$refs[$hashbase]);
+			}
+		}
+		$paths = git_path_trees($projectroot . $project, $hashbase, $file);
+		$tpl->assign("paths",$paths);
+
+		$blamedata = git_parse_blame($projectroot . $project, $file, $hashbase);
+		$tpl->assign("blamedata",$blamedata);
+
+	}
+
+	$tpl->display('blame.tpl', $cachekey);
+}
+
+?>
+

--- /dev/null
+++ b/include/gitutil.git_parse_blame.php
@@ -1,1 +1,89 @@
+<?php
+/*
+ * gitutil.git_parse_blame.php
+ * gitphp: A PHP git repository browser
+ * Component: Git utility - parse blame info
+ *
+ * Copyright (C) 2010 Christopher Han <xiphux@gmail.com>
+ */
 
+require_once('gitutil.git_read_blame.php');
+require_once('util.date_str.php');
+
+function git_parse_blame($proj, $file, $rev = null)
+{
+	$lines = explode("\n", git_read_blame($proj, $file, $rev));
+
+	if (count($lines) < 1)
+		return null;
+	
+	$blamedata = array();
+	$commitcache = array();
+	
+	$commitgroup = null;
+	foreach ($lines as $i => $line) {
+		/*
+		 * Only parsing a handful of the blame info, see
+		 * the git blame man page for all the data
+		 */
+		if (preg_match("/^([0-9a-fA-F]{40}) ([0-9]+) ([0-9]+) ([0-9]+)$/",$line,$regs)) {
+			/* starting a new commit group */
+			if ($commitgroup)
+				$blamedata[] = $commitgroup;
+			$commitgroup = array();
+			$commitgroup['lines'] = array();
+			$commitgroup['commit'] = $regs[1];
+			if (isset($commitcache[$regs[1]])) {
+				$commitgroup['commitdata'] = $commitcache[$regs[1]];
+			} else {
+				$commitgroup['commitdata'] = array();
+				$commitcache[$regs[1]] = array();
+			}
+		} else if (preg_match("/^author (.*)$/",$line,$regs)) {
+			$commitgroup['commitdata']['author'] = $regs[1];
+			$commitcache[$commitgroup['commit']]['author'] = $regs[1];
+		} else if (preg_match("/^author-mail (.*)$/",$line,$regs)) {
+			$commitgroup['commitdata']['author-mail'] = $regs[1];
+			$commitcache[$commitgroup['commit']]['author-mail'] = $regs[1];
+		} else if (preg_match("/^author-time (.*)$/",$line,$regs)) {
+			$commitgroup['commitdata']['author-time'] = $regs[1];
+			$commitcache[$commitgroup['commit']]['author-time'] = $regs[1];
+		} else if (preg_match("/^author-tz (.*)$/",$line,$regs)) {
+			$commitgroup['commitdata']['author-tz'] = $regs[1];
+			$commitcache[$commitgroup['commit']]['author-tz'] = $regs[1];
+		} else if (preg_match("/^summary (.*)$/",$line,$regs)) {
+			$commitgroup['commitdata']['summary'] = $regs[1];
+			$commitcache[$commitgroup['commit']]['summary'] = $regs[1];
+		} else if (preg_match("/^\t(.*)$/",$line,$regs)) {
+			/* tab starts a file content line */
+			$commitgroup['lines'][] = $regs[1];
+		}
+	}
+	if ($commitgroup)
+		$blamedata[] = $commitgroup;
+
+	$len = count($blamedata);
+	for ($i = 0; $i < $len; $i++) {
+		if (isset($blamedata[$i]['commitdata']['author-time'])) {
+			$authortime = $blamedata[$i]['commitdata']['author-time'];
+			$authortz = "-0000";
+			if (isset($blamedata[$i]['commitdata']['author-tz']))
+				$authortz = $blamedata[$i]['commitdata']['author-tz'];
+			$date = date_str($authortime, $authortz);
+			$blamedata[$i]['commitdata']['authordate'] = $date['ymd-time'];
+		}
+		if (isset($blamedata[$i]['commitdata']['committer-time'])) {
+			$committertime = $blamedata[$i]['commitdata']['committer-time'];
+			$committertz = "-0000";
+			if (isset($blamedata[$i]['commitdata']['committer-tz']))
+				$committertz = $blamedata[$i]['commitdata']['committer-tz'];
+			$date = date_str($committertime, $committertz);
+			$blamedata[$i]['commitdata']['committerdate'] = $date['ymd-time'];
+		}
+	}
+
+	return $blamedata;
+}
+
+?>
+

--- /dev/null
+++ b/include/gitutil.git_read_blame.php
@@ -1,1 +1,22 @@
+<?php
+/*
+ * gitutil.git_read_blame.php
+ * gitphp: A PHP git repository browser
+ * Component: Git utility - get blame info
+ *
+ * Copyright (C) 2010 Christopher Han <xiphux@gmail.com>
+ */
 
+require_once('defs.commands.php');
+require_once('gitutil.git_exec.php');
+
+function git_read_blame($proj, $file, $rev = null)
+{
+	$cmd = GIT_BLAME . " -p";
+	if ($rev)
+		$cmd .= " " . $rev;
+	return git_exec($proj, $cmd . " " . $file);
+}
+
+?>
+

--- a/include/util.date_str.php
+++ b/include/util.date_str.php
@@ -17,6 +17,7 @@
 	$date['month'] = date("M",$epoch);
 	$date['rfc2822'] = date("r",$epoch);
 	$date['mday-time'] = date("d M H:i",$epoch);
+	$date['ymd-time'] = date("Y-m-d H:i",$epoch);
 	if (preg_match("/^([+\-][0-9][0-9])([0-9][0-9])$/",$tz,$regs)) {
 		$local = $epoch + ((((int)$regs[1]) + ($regs[2]/60)) * 3600);
 		$date['hour_local'] = date("H",$local);

file:a/index.php -> file:b/index.php
--- a/index.php
+++ b/index.php
@@ -214,6 +214,10 @@
 					require_once('include/display.git_blobdiff_plain.php');
 					git_blobdiff_plain($gitphp_conf['projectroot'],$project,$_GET['h'],$_GET['hb'],$_GET['hp'], (isset($_GET['f']) ? $_GET['f'] : NULL));
 					break;
+				case "blame":
+					require_once('include/display.git_blame.php');
+					git_blame($gitphp_conf['projectroot'],$project, (isset($_GET['h']) ? $_GET['h'] : NULL), (isset($_GET['f']) ? $_GET['f'] : NULL), (isset($_GET['hb']) ? $_GET['hb'] : NULL));
+					break;
 				case "snapshot":
 					require_once('include/display.git_snapshot.php');
 					git_snapshot($gitphp_conf['projectroot'],$project, (isset($_GET['h']) ? $_GET['h'] : NULL));

--- /dev/null
+++ b/templates/blame.tpl
@@ -1,1 +1,75 @@
+{*
+ * blame.tpl
+ * gitphp: A PHP git repository browser
+ * Component: Blame view template
+ *
+ * Copyright (C) 2010 Christopher Han <xiphux@gmail.com>
+ *}
 
+ {include file='header.tpl'}
+
+ {* If we managed to look up commit info, we have enough info to display the full header - othewise just use a simple header *}
+ <div class="page_nav">
+   {if $fullnav}
+     <a href="{$SCRIPT_NAME}?p={$project}&a=summary">summary</a> | <a href="{$SCRIPT_NAME}?p={$project}&a=shortlog">shortlog</a> | <a href="{$SCRIPT_NAME}?p={$project}&a=log">log</a> | <a href="{$SCRIPT_NAME}?p={$project}&a=commit&h={$hashbase}">commit</a> | <a href="{$SCRIPT_NAME}?p={$project}&a=commitdiff&h={$hashbase}">commitdiff</a> | <a href="{$SCRIPT_NAME}?p={$project}&a=tree&h={$tree}&hb={$hashbase}">tree</a><br />
+     {if $file}
+       <a href="{$SCRIPT_NAME}?p={$project}&a=blob_plain&h={$hash}&f={$file}">plain</a> | 
+       {if ($hashbase != "HEAD") && ($hashbase != $head)}
+         <a href="{$SCRIPT_NAME}?p={$project}&a=blame&hb=HEAD&f={$file}">HEAD</a>
+       {else}
+         HEAD
+       {/if}
+        | blame
+       <br />
+     {else}
+       <a href="{$SCRIPT_NAME}?p={$project}&a=blob_plain&h={$hash}">plain</a><br />
+     {/if}
+   {else}
+     <br /><br />
+   {/if}
+ </div>
+ <div>
+   {if $fullnav}
+     <a href="{$SCRIPT_NAME}?p={$project}&a=commit&h={$hashbase}" class="title">{$title}
+     {if $hashbaseref}
+       <span class="tag">{$hashbaseref}</span>
+     {/if}
+     </a>
+   {else}
+     <div class="title">{$hash}</div>
+   {/if}
+ </div>
+ <div class="page_path">
+   {* The path to the file, with directories broken into tree links *}
+   <b>
+     <a href="{$SCRIPT_NAME}?p={$project}&a=tree&hb={$hashbase}&h={$hashbase}">[{$project}]</a> / 
+     {foreach from=$paths item=path name=paths}
+       {if $smarty.foreach.paths.last}
+         <a href="{$SCRIPT_NAME}?p={$project}&a=blob_plain&h={$path.tree}&f={$path.full}">{$path.short}</a>
+       {else}
+         <a href="{$SCRIPT_NAME}?p={$project}&a=tree&hb={$hashbase}&h={$path.tree}&f={$path.full}">{$path.short}</a> / 
+       {/if}
+     {/foreach}
+   </b>
+ </div>
+ <div class="page_body">
+ 	<table class="code">
+	{counter name=linecount start=0 print=false}
+	{foreach from=$blamedata item=blameitem}
+		{cycle values="light,dark" assign=rowclass}
+		{foreach from=$blameitem.lines name=linegroup item=blameline}
+		{counter name=linecount assign=linenum}
+		<tr class="{$rowclass}">
+			<td class="num"><a id="l{$linenum}" href="#l{$linenum}" class="linenr">{$linenum}</a></td>
+			<td class="date">{$blameitem.commitdata.authordate}</td>
+			<td class="author">{if $smarty.foreach.linegroup.first}{$blameitem.commitdata.author}{/if}</td>
+			<td>{if $blameitem.commit}{if $smarty.foreach.linegroup.first}<a href="{$SCRIPT_NAME}?p={$project}&a=commit&h={$blameitem.commit}" title="{$blameitem.commitdata.summary}">commit</a>{/if}{/if}</td>
+			<td class="codeline">{$blameline}</td>
+		</tr>
+		{/foreach}
+	{/foreach}
+	</table>
+ </div>
+
+ {include file='footer.tpl'}
+

--- a/templates/blob.tpl
+++ b/templates/blob.tpl
@@ -19,6 +19,7 @@
        {else}
          HEAD
        {/if}
+        | <a href="{$SCRIPT_NAME}?p={$project}&a=blame&h={$hash}&f={$file}&hb={$hashbase}">blame</a>
        <br />
      {else}
        <a href="{$SCRIPT_NAME}?p={$project}&a=blob_plain&h={$hash}">plain</a><br />

comments