Initial file search functionality
Initial file search functionality

--- a/config/gitphp.conf.php.example
+++ b/config/gitphp.conf.php.example
@@ -154,6 +154,13 @@
 $gitphp_conf['search'] = TRUE;
 
 /*
+ * filesearch
+ * Set this to false to disable searching within files
+ * (it can be resource intensive)
+ */
+$gitphp_conf['filesearch'] = TRUE;
+
+/*
  * git_projects
  * Two-dimensional array list of projects
  * First array index is the name of the category the projects

--- a/include/defs.commands.php
+++ b/include/defs.commands.php
@@ -14,6 +14,7 @@
 define('GIT_REV_PARSE','rev-parse');
 define('GIT_SHOW_REF','show-ref');
 define('GIT_TAR_TREE','tar-tree');
+define('GIT_GREP','grep');
 
 ?>
 

--- a/include/display.git_search.php
+++ b/include/display.git_search.php
@@ -38,7 +38,6 @@
 	$co = git_read_commit($projectroot . $project, $hash);
 
 	$revlist = explode("\n",trim(git_rev_list($projectroot . $project, $hash, 101, ($page * 100), FALSE, FALSE, $searchtype, $search)));
-
 	if (count($revlist) < 1 || (strlen($revlist[0]) < 1)) {
 		$tpl->clear_all_assign();
 		$tpl->assign("message","No matches for '" . $search . "'.");

--- /dev/null
+++ b/include/display.git_search_files.php
@@ -1,1 +1,123 @@
+<?php
+/*
+ *  display.git_search_files.php
+ *  gitphp: A PHP git repository browser
+ *  Component: Display - search in files
+ *
+ *  Copyright (C) 2009 Christopher Han <xiphux@gmail.com>
+ */
 
+require_once('defs.constants.php');
+require_once('util.highlight.php');
+require_once('util.file_type.php');
+require_once('gitutil.git_filesearch.php');
+require_once('gitutil.git_read_commit.php');
+
+function git_search_files($projectroot, $project, $hash, $search, $page = 0)
+{
+	global $tpl,$gitphp_conf;
+
+	if (!($gitphp_conf['search'] && $gitphp_conf['filesearch'])) {
+		$tpl->clear_all_assign();
+		$tpl->assign("message","File search has been disabled");
+		$tpl->display("message.tpl");
+		return;
+	}
+
+	if (!isset($search) || (strlen($search) < 2)) {
+		$tpl->clear_all_assign();
+		$tpl->assign("error",TRUE);
+		$tpl->assign("message","You must enter search text of at least 2 characters");
+		$tpl->display("message.tpl");
+		return;
+	}
+	if (!isset($hash)) {
+		//$hash = git_read_head($projectroot . $project);
+		$hash = "HEAD";
+	}
+
+	$co = git_read_commit($projectroot . $project, $hash);
+
+	$filesearch = git_filesearch($projectroot . $project, $hash, $search, false, ($page * 100), 101);
+
+	if (count($filesearch) < 1) {
+		$tpl->clear_all_assign();
+		$tpl->assign("message","No matches for '" . $search . "'.");
+		$tpl->display("message.tpl");
+		return;
+	}
+
+	$tpl->clear_all_assign();
+	$tpl->assign("project",$project);
+	$tpl->assign("hash",$hash);
+	$tpl->assign("treehash",$co['tree']);
+	$tpl->display("search_nav.tpl");
+
+	$tpl->assign("search",$search);
+	$tpl->assign("searchtype","file");
+	if ($page > 0) {
+		$tpl->assign("firstlink",TRUE);
+		$tpl->assign("prevlink",TRUE);
+		if ($page > 1)
+			$tpl->assign("prevpage",$page-1);
+	}
+	if (count($filesearch) > 100) {
+		$tpl->assign("nextlink",TRUE);
+		$tpl->assign("nextpage",$page+1);
+	}
+	$tpl->display("search_pagenav.tpl");
+
+	$tpl->assign("title",$co['title']);
+	$tpl->display("search_header.tpl");
+
+	$alternate = FALSE;
+	$i = 0;
+	foreach ($filesearch as $file => $data) {
+		$tpl->clear_all_assign();
+		if ($alternate)
+			$tpl->assign("class","dark");
+		else
+			$tpl->assign("class","light");
+		$alternate = !$alternate;
+		$tpl->assign("project",$project);
+		$tpl->assign("hashbase",$hash);
+		$tpl->assign("file",$file);
+		$hlt = highlight($file, $search, "searchmatch");
+		if ($hlt)
+			$tpl->assign("filename",$hlt);
+		else
+			$tpl->assign("filename",$file);
+		$tpl->assign("hash",$data['hash']);
+		$type = file_type($data['mode']);
+		if ($type == "directory")
+			$tpl->assign("tree",TRUE);
+		if (isset($data['lines'])) {
+			$matches = array();
+			foreach ($data['lines'] as $line) {
+				$hlt = highlight($line,$search,"searchmatch",floor(GITPHP_TRIM_LENGTH*1.5),true);
+				if ($hlt)
+					$matches[] = $hlt;
+			}
+			if (count($matches) > 0)
+				$tpl->assign("matches",$matches);
+		}
+		$tpl->display("search_fileitem.tpl");
+		$i++;
+		if ($i >= 100)
+			break;
+	}
+
+	$tpl->clear_all_assign();
+	$tpl->assign("project",$project);
+	$tpl->assign("hash",$hash);
+	$tpl->assign("search",$search);
+	$tpl->assign("searchtype","file");
+	if (count($filesearch) > 100) {
+		$tpl->assign("nextlink",TRUE);
+		$tpl->assign("nextpage",$page+1);
+	}
+	$tpl->display("search_footer.tpl");
+}
+
+?>
+

--- /dev/null
+++ b/include/gitutil.git_filesearch.php
@@ -1,1 +1,84 @@
+<?php
+/*
+ *  gitutil.git_filesearch.php
+ *  gitphp: A PHP git repository browser
+ *  Component: Git utility - search files
+ *
+ *  Copyright (C) 2009 Christopher Han <xiphux@gmail.com>
+ */
 
+require_once('gitutil.git_grep.php');
+require_once('gitutil.git_ls_tree.php');
+
+function git_filesearch($project, $hash, $search, $case = false, $skip = 0, $count = 100)
+{
+	$matches = array();
+	
+	/*
+	 * Search file contents
+	 */
+	$grepout = git_grep($project, $hash, $search, $case, false, true);
+	$lines = explode("\n",$grepout);
+	foreach ($lines as $j => $line) {
+		if ($case)
+			$ret = ereg("^([^:]+):([^:]+):(.*" . quotemeta($search) . ".*)",$line,$regs);
+		else
+			$ret = eregi("^([^:]+):([^:]+):(.*" . quotemeta($search) . ".*)",$line,$regs);
+		if ($ret) {
+			$fname = trim($regs[2]);
+			if (!isset($matches[$fname])) {
+				$matches[$fname] = array();
+				$matches[$fname]['lines'] = array();
+			}
+			$matches[$fname]['lines'][] = $regs[3];
+		}
+	}
+
+	/*
+	 * Search filenames
+	 */
+	 $lsout = git_ls_tree($project, $hash, false, true);
+	 $entries = explode("\n",$lsout);
+	 foreach ($entries as $j => $line) {
+		$ret = ereg("^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)",$line,$regs);
+		$fname = trim($regs[4]);
+		if (isset($matches[$fname])) {
+			$matches[$fname]['hash'] = $regs[3];
+			$matches[$fname]['mode'] = $regs[1];
+		} else {
+			if ($case)
+				$ret = ereg("^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.*" . quotemeta($search) . ".*)",$line,$regs);
+			else
+				$ret = eregi("^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.*" . quotemeta($search) . ".*)",$line,$regs);
+			if ($ret) {
+				$fname = trim($regs[4]);
+				$matches[$fname] = array();
+				$matches[$fname]['hash'] = $regs[3];
+				$matches[$fname]['mode'] = $regs[1];
+			}
+		}
+	 }
+	
+	if ($skip > 0) {
+		foreach ($matches as $i => $val) {
+			unset($matches[$i]);
+			$skip--;
+			if ($skip <= 0)
+				break;
+		}
+	}
+
+	if (count($matches) > $count) {
+		$index = 1;
+		foreach ($matches as $i => $val) {
+			if ($index > $count)
+				unset($matches[$i]);
+			$index++;
+		}
+	}
+
+	return $matches;
+}
+
+?>
+

--- /dev/null
+++ b/include/gitutil.git_grep.php
@@ -1,1 +1,27 @@
+<?php
+/*
+ *  gitutil.git_grep.php
+ *  gitphp: A PHP git repository browser
+ *  Component: Git utility - grep
+ *
+ *  Copyright (C) 2009 Christopher Han <xiphux@gmail.com>
+ */
 
+require_once('defs.commands.php');
+require_once('gitutil.git_exec.php');
+
+function git_grep($project, $hash, $search, $case = false, $binary = false, $fullname = true)
+{
+	$cmd = GIT_GREP;
+	if (!$binary)
+		$cmd .= " -I";
+	if ($fullname)
+		$cmd .= " --full-name";
+	if (!$case)
+		$cmd .= " --ignore-case";
+	$cmd .= " -e " . $search;
+	return git_exec($project, $cmd . " " . $hash);
+}
+
+?>
+

--- a/include/gitutil.git_ls_tree.php
+++ b/include/gitutil.git_ls_tree.php
@@ -10,11 +10,13 @@
  require_once('defs.commands.php');
  require_once('gitutil.git_exec.php');
 
-function git_ls_tree($proj,$hash,$nullterm = FALSE)
+function git_ls_tree($proj,$hash,$nullterm = FALSE, $recurse = FALSE)
 {
 	$cmd = GIT_LS_TREE;
 	if ($nullterm)
 		$cmd .= " -z";
+	if ($recurse)
+		$cmd .= " -r -t --full-tree";
 	return git_exec($proj, $cmd . " " . $hash);
 }
 

--- a/include/util.highlight.php
+++ b/include/util.highlight.php
@@ -7,7 +7,7 @@
  *  Copyright (C) 2008 Christopher Han <xiphux@gmail.com>
  */
 
-function highlight($haystack, $needle, $highlightclass, $trimlen = NULL)
+function highlight($haystack, $needle, $highlightclass, $trimlen = NULL, $escape = false)
 {
 	if (eregi("(.*)(" . quotemeta($needle) . ")(.*)",$haystack,$regs)) {
 		if (isset($trimlen) && ($trimlen > 0)) {
@@ -31,6 +31,11 @@
 				}
 			}
 		}
+		if ($escape) {
+			$regs[1] = htmlspecialchars($regs[1]);
+			$regs[2] = htmlspecialchars($regs[2]);
+			$regs[3] = htmlspecialchars($regs[3]);
+		}
 		$ret = $regs[1] . "<span";
 		if ($highlightclass)
 			$ret .= " class=\"" . $highlightclass . "\"";

file:a/index.php -> file:b/index.php
--- a/index.php
+++ b/index.php
@@ -119,8 +119,13 @@
 					git_history($gitphp_conf['projectroot'],$_GET['p'], (isset($_GET['h']) ? $_GET['h'] : NULL),$_GET['f']);
 					break;
 				case "search":
-					require_once('include/display.git_search.php');
-					git_search($gitphp_conf['projectroot'],$_GET['p'],(isset($_GET['h']) ? $_GET['h'] : NULL),(isset($_GET['s']) ? $_GET['s'] : NULL),(isset($_GET['st']) ? $_GET['st'] : "commit"),(isset($_GET['pg']) ? $_GET['pg'] : 0));
+					if (isset($_GET['st']) && ($_GET['st'] == 'file')) {
+						require_once('include/display.git_search_files.php');
+						git_search_files($gitphp_conf['projectroot'],$_GET['p'],(isset($_GET['h']) ? $_GET['h'] : NULL),(isset($_GET['s']) ? $_GET['s'] : NULL),(isset($_GET['pg']) ? $_GET['pg'] : 0));
+					} else {
+						require_once('include/display.git_search.php');
+						git_search($gitphp_conf['projectroot'],$_GET['p'],(isset($_GET['h']) ? $_GET['h'] : NULL),(isset($_GET['s']) ? $_GET['s'] : NULL),(isset($_GET['st']) ? $_GET['st'] : "commit"),(isset($_GET['pg']) ? $_GET['pg'] : 0));
+					}
 					break;
 				default:
 					echo "Unknown action";
@@ -163,6 +168,8 @@
 		$tpl->assign("hash",$_GET['h']);
 	if ($gitphp_conf['search'])
 		$tpl->assign("enablesearch",TRUE);
+	if ($gitphp_conf['filesearch'])
+		$tpl->assign("filesearch",TRUE);
 	 $tpl->display("header.tpl");
  }
 

--- a/templates/header.tpl
+++ b/templates/header.tpl
@@ -40,6 +40,9 @@
 <option {if $searchtype == 'commit'}selected="selected"{/if} value="commit">commit</option>
 <option {if $searchtype == 'author'}selected="selected"{/if} value="author">author</option>
 <option {if $searchtype == 'committer'}selected="selected"{/if} value="committer">committer</option>
+{if $filesearch}
+<option {if $searchtype == 'file'}selected="selected"{/if} value="file">file</option>
+{/if}
 </select> search: <input type="text" name="s" {if $search}value="{$search}"{/if} />
 </div>
 </form>

--- /dev/null
+++ b/templates/search_fileitem.tpl
@@ -1,1 +1,21 @@
+{*
+ *  search_fileitem.tpl
+ *  gitphp: A PHP git repository browser
+ *  Component: Search file item template
+ *
+ *  Copyright (C) 2009 Christopher Han <xiphux@gmail.com>
+ *}
+<tr class="{$class}">
+<td>
+{if $tree}
+<a href="{$SCRIPT_NAME}?p={$project}&a=tree&h={$hash}&hb={$hashbase}&f={$file}" class="list"><b>{$filename}</b></a>
+{else}
+<a href="{$SCRIPT_NAME}?p={$project}&a=blob&h={$hash}&hb={$hashbase}&f={$file}" class="list"><b>{$filename}</b></a>
+{foreach from=$matches item=line name=match}
+{if $smarty.foreach.match.first}<br />{/if}<span class="respectwhitespace">{$line}</span><br />
+{/foreach}
+{/if}
+</td>
+<td class="link">{if $tree}<a href="{$SCRIPT_NAME}?p={$project}&a=tree&h={$hash}&hb={$hashbase}&f={$file}">tree</a>{else}<a href="{$SCRIPT_NAME}?p={$project}&a=blob&h={$hash}&hb={$hashbase}&f={$file}">blob</a> | <a href="{$SCRIPT_NAME}?p={$project}&a=history&h={$hashbase}&f={$file}">history</a>{/if}</td>
+</tr>
 

comments