Fix blobdiff with xdiff
Fix blobdiff with xdiff

--- a/config/gitphp.conf.defaults.php
+++ b/config/gitphp.conf.defaults.php
@@ -149,6 +149,20 @@
  * your projects.
  */
 $gitphp_conf['compat'] = false;
+
+/**
+ * largeskip
+ * When GitPHP is reading through the history for pages of the shortlog/log
+ * beyond the first, it needs to read from the tip but skip a number of commits
+ * for the previous pages.  The more commits it needs to skip, the longer it takes.
+ * Calling the git executable is faster when skipping a large number of commits,
+ * ie reading a log page significantly beyond the first.  This determines
+ * the threshold at which GitPHP will fall back to using the git exe for the log.
+ * Currently each log page shows 100 commits, so this would be calculated at
+ * page number * 100.  So for example at the default of 200, pages 0-2 would be
+ * loaded natively and pages 3+ would fall back on the git exe.
+ */
+$gitphp_conf['largeskip'] = 200;
 
 /*
  * compressformat

--- a/css/gitphpskin.css
+++ b/css/gitphpskin.css
@@ -325,6 +325,14 @@
 	color: #880000; 
 }
 
+span.commit_title {
+	font-weight: bold;
+}
+
+span.merge_title {
+	color: #777777;
+}
+
 span.newfile {
 	color: #008000;
 }

--- a/include/Util.class.php
+++ b/include/Util.class.php
@@ -69,7 +69,7 @@
 	 * @static
 	 * @return bool true if on 64 bit
 	 */
-	public function Is64Bit()
+	public static function Is64Bit()
 	{
 		return (strpos(php_uname('m'), '64') !== false);
 	}

--- a/include/git/Blob.class.php
+++ b/include/git/Blob.class.php
@@ -222,6 +222,26 @@
 	}
 
 	/**
+	 * IsBinary
+	 *
+	 * Tests if this blob is a binary file
+	 *
+	 * @access public
+	 * @return boolean true if binary file
+	 */
+	public function IsBinary()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		$data = $this->data;
+		if (strlen($this->data) > 8000)
+			$data = substr($data, 0, 8000);
+
+		return strpos($data, chr(0)) !== false;
+	}
+
+	/**
 	 * FileMime
 	 *
 	 * Get the file mimetype
@@ -277,7 +297,7 @@
 			}
 		}
 
-		$finfo = finfo_open(FILEINFO_MIME, $magicdb);
+		$finfo = @finfo_open(FILEINFO_MIME, $magicdb);
 		if ($finfo) {
 			$mime = finfo_buffer($finfo, $this->data, FILEINFO_MIME);
 			if ($mime && strpos($mime, '/')) {

--- a/include/git/Commit.class.php
+++ b/include/git/Commit.class.php
@@ -513,6 +513,22 @@
 	}
 
 	/**
+	 * IsMergeCommit
+	 *
+	 * Returns whether this is a merge commit
+	 *
+	 * @access pubilc
+	 * @return boolean true if merge commit
+	 */
+	public function IsMergeCommit()
+	{
+		if (!$this->dataRead)
+			$this->ReadData();
+
+		return count($this->parents) > 1;
+	}
+
+	/**
 	 * ReadData
 	 *
 	 * Read the data for the commit
@@ -559,8 +575,10 @@
 
 		}
 
+		$header = true;
+
 		foreach ($lines as $i => $line) {
-			if (preg_match('/^tree ([0-9a-fA-F]{40})$/', $line, $regs)) {
+			if ($header && preg_match('/^tree ([0-9a-fA-F]{40})$/', $line, $regs)) {
 				/* Tree */
 				try {
 					$tree = $this->GetProject()->GetTree($regs[1]);
@@ -570,24 +588,25 @@
 					}
 				} catch (Exception $e) {
 				}
-			} else if (preg_match('/^parent ([0-9a-fA-F]{40})$/', $line, $regs)) {
+			} else if ($header && preg_match('/^parent ([0-9a-fA-F]{40})$/', $line, $regs)) {
 				/* Parent */
 				try {
 					$this->parents[] = $this->GetProject()->GetCommit($regs[1]);
 				} catch (Exception $e) {
 				}
-			} else if (preg_match('/^author (.*) ([0-9]+) (.*)$/', $line, $regs)) {
+			} else if ($header && preg_match('/^author (.*) ([0-9]+) (.*)$/', $line, $regs)) {
 				/* author data */
 				$this->author = $regs[1];
 				$this->authorEpoch = $regs[2];
 				$this->authorTimezone = $regs[3];
-			} else if (preg_match('/^committer (.*) ([0-9]+) (.*)$/', $line, $regs)) {
+			} else if ($header && preg_match('/^committer (.*) ([0-9]+) (.*)$/', $line, $regs)) {
 				/* committer data */
 				$this->committer = $regs[1];
 				$this->committerEpoch = $regs[2];
 				$this->committerTimezone = $regs[3];
 			} else {
 				/* commit comment */
+				$header = false;
 				$trimmed = trim($line);
 				if (empty($this->title) && (strlen($trimmed) > 0))
 					$this->title = $trimmed;

--- a/include/git/FileDiff.class.php
+++ b/include/git/FileDiff.class.php
@@ -573,58 +573,66 @@
 			return;
 		}
 
-		$tmpdir = GitPHP_TmpDir::GetInstance();
-
-		$pid = 0;
-		if (function_exists('posix_getpid'))
-			$pid = posix_getpid();
-		else
-			$pid = rand();
-
-		$fromTmpFile = null;
-		$toTmpFile = null;
-
-		$fromName = null;
-		$toName = null;
-
-		if ((empty($this->status)) || ($this->status == 'D') || ($this->status == 'M')) {
-			$fromBlob = $this->GetFromBlob();
-			$fromTmpFile = 'gitphp_' . $pid . '_from';
-			$tmpdir->AddFile($fromTmpFile, $fromBlob->GetData());
-
-			$fromName = 'a/';
-			if (!empty($file)) {
-				$fromName .= $file;
-			} else if (!empty($this->fromFile)) {
-				$fromName .= $this->fromFile;
-			} else {
-				$fromName .= $this->fromHash;
-			}
-		}
-
-		if ((empty($this->status)) || ($this->status == 'A') || ($this->status == 'M')) {
-			$toBlob = $this->GetToBlob();
-			$toTmpFile = 'gitphp_' . $pid . '_to';
-			$tmpdir->AddFile($toTmpFile, $toBlob->GetData());
-
-			$toName = 'b/';
-			if (!empty($file)) {
-				$toName .= $file;
-			} else if (!empty($this->toFile)) {
-				$toName .= $this->toFile;
-			} else {
-				$toName .= $this->toHash;
-			}
-		}
-
-		$this->diffData = GitPHP_DiffExe::Diff((empty($fromTmpFile) ? null : escapeshellarg($tmpdir->GetDir() . $fromTmpFile)), $fromName, (empty($toTmpFile) ? null : escapeshellarg($tmpdir->GetDir() . $toTmpFile)), $toName);
-
-		if (!empty($fromTmpFile)) {
-			$tmpdir->RemoveFile($fromTmpFile);
-		}
-
-		if (!empty($toTmpFile)) {
-			$tmpdir->RemoveFile($toTmpFile);
+		if (function_exists('xdiff_string_diff')) {
+
+			$this->diffData = $this->GetXDiff(3, true, $file);
+
+		} else {
+
+			$tmpdir = GitPHP_TmpDir::GetInstance();
+
+			$pid = 0;
+			if (function_exists('posix_getpid'))
+				$pid = posix_getpid();
+			else
+				$pid = rand();
+
+			$fromTmpFile = null;
+			$toTmpFile = null;
+
+			$fromName = null;
+			$toName = null;
+
+			if ((empty($this->status)) || ($this->status == 'D') || ($this->status == 'M')) {
+				$fromBlob = $this->GetFromBlob();
+				$fromTmpFile = 'gitphp_' . $pid . '_from';
+				$tmpdir->AddFile($fromTmpFile, $fromBlob->GetData());
+
+				$fromName = 'a/';
+				if (!empty($file)) {
+					$fromName .= $file;
+				} else if (!empty($this->fromFile)) {
+					$fromName .= $this->fromFile;
+				} else {
+					$fromName .= $this->fromHash;
+				}
+			}
+
+			if ((empty($this->status)) || ($this->status == 'A') || ($this->status == 'M')) {
+				$toBlob = $this->GetToBlob();
+				$toTmpFile = 'gitphp_' . $pid . '_to';
+				$tmpdir->AddFile($toTmpFile, $toBlob->GetData());
+
+				$toName = 'b/';
+				if (!empty($file)) {
+					$toName .= $file;
+				} else if (!empty($this->toFile)) {
+					$toName .= $this->toFile;
+				} else {
+					$toName .= $this->toHash;
+				}
+			}
+
+			$this->diffData = GitPHP_DiffExe::Diff((empty($fromTmpFile) ? null : escapeshellarg($tmpdir->GetDir() . $fromTmpFile)), $fromName, (empty($toTmpFile) ? null : escapeshellarg($tmpdir->GetDir() . $toTmpFile)), $toName);
+
+			if (!empty($fromTmpFile)) {
+				$tmpdir->RemoveFile($fromTmpFile);
+			}
+
+			if (!empty($toTmpFile)) {
+				$tmpdir->RemoveFile($toTmpFile);
+			}
+
 		}
 
 		if ($explode)
@@ -659,9 +667,14 @@
 		$fromBlob = $this->GetFromBlob();
 		$blob = $fromBlob->GetData(true);
 
-		$diffLines = explode("\n", $exe->Execute(GIT_DIFF,
-			array("-U0", $this->fromHash,
-				$this->toHash)));
+		$diffLines = '';
+		if (function_exists('xdiff_string_diff')) {
+			$diffLines = explode("\n", $this->GetXDiff(0, false));
+		} else {
+			$diffLines = explode("\n", $exe->Execute(GIT_DIFF,
+				array("-U0", $this->fromHash,
+					$this->toHash)));
+		}
 
 		unset($exe);
 
@@ -746,6 +759,65 @@
 	}
 
 	/**
+	 * GetXDiff
+	 *
+	 * Get diff using xdiff
+	 *
+	 * @access private
+	 * @param int $context number of context lines
+	 * @param boolean $header true to include standard diff header
+	 * @param string $file override the file name
+	 * @return string diff content
+	 */
+	private function GetXDiff($context = 3, $header = true, $file = null)
+	{
+		if (!function_exists('xdiff_string_diff'))
+			return '';
+
+		$fromData = '';
+		$toData = '';
+		$isBinary = false;
+		$fromName = '/dev/null';
+		$toName = '/dev/null';
+		if (empty($this->status) || ($this->status == 'M') || ($this->status == 'D')) {
+			$fromBlob = $this->GetFromBlob();
+			$isBinary = $isBinary || $fromBlob->IsBinary();
+			$fromData = $fromBlob->GetData(false);
+			$fromName = 'a/';
+			if (!empty($file)) {
+				$fromName .= $file;
+			} else if (!empty($this->fromFile)) {
+				$fromName .= $this->fromFile;
+			} else {
+				$fromName .= $this->fromHash;
+			}
+		}
+		if (empty($this->status) || ($this->status == 'M') || ($this->status == 'A')) {
+			$toBlob = $this->GetToBlob();
+			$isBinary = $isBinary || $toBlob->IsBinary();
+			$toData = $toBlob->GetData(false);
+			$toName = 'b/';
+			if (!empty($file)) {
+				$toName .= $file;
+			} else if (!empty($this->toFile)) {
+				$toName .= $this->toFile;
+			} else {
+				$toName .= $this->toHash;
+			}
+		}
+		$output = '';
+		if ($isBinary) {
+			$output = sprintf(__('Binary files %1$s and %2$s differ'), $fromName, $toName) . "\n";
+		} else {
+			if ($header) {
+				$output = '--- ' . $fromName . "\n" . '+++ ' . $toName . "\n";
+			}
+			$output .= xdiff_string_diff($fromData, $toData, $context);
+		}
+		return $output;
+	}
+
+	/**
 	 * GetCommit
 	 *
 	 * Gets the commit for this filediff

--- a/include/git/Project.class.php
+++ b/include/git/Project.class.php
@@ -1309,7 +1309,7 @@
 	 */
 	public function GetLog($hash, $count = 50, $skip = 0)
 	{
-		if (GitPHP_Config::GetInstance()->GetValue('compat', false) || ($skip > 200)) {
+		if (GitPHP_Config::GetInstance()->GetValue('compat', false) || ($skip > GitPHP_Config::GetInstance()->GetValue('largeskip', 200)) ) {
 			return $this->GetLogGit($hash, $count, $skip);
 		} else {
 			return $this->GetLogRaw($hash, $count, $skip);

--- a/include/git/Tag.class.php
+++ b/include/git/Tag.class.php
@@ -168,6 +168,14 @@
 			$this->ReadData();
 			if ($this->commitReferenced)
 				$this->DereferenceCommit();
+		}
+
+		if (!$this->commit) {
+			if ($this->object instanceof GitPHP_Commit) {
+				$this->commit = $this->object;
+			} else if ($this->object instanceof GitPHP_Tag) {
+				$this->commit = $this->object->GetCommit();
+			}
 		}
 
 		return $this->commit;

--- a/templates/log.tpl
+++ b/templates/log.tpl
@@ -44,7 +44,8 @@
    </div>
    <div class="title_text">
      <div class="log_link">
-       <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=commit&amp;h={$rev->GetHash()}">{t}commit{/t}</a> | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=commitdiff&amp;h={$rev->GetHash()}">{t}commitdiff{/t}</a> | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=tree&amp;h={$rev->GetHash()}&amp;hb={$rev->GetHash()}">{t}tree{/t}</a>
+       {assign var=revtree value=$rev->GetTree()}
+       <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=commit&amp;h={$rev->GetHash()}">{t}commit{/t}</a> | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=commitdiff&amp;h={$rev->GetHash()}">{t}commitdiff{/t}</a> | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=tree&amp;h={$revtree->GetHash()}&amp;hb={$rev->GetHash()}">{t}tree{/t}</a>
        <br />
        {if $mark}
          {if $mark->GetHash() == $rev->GetHash()}

--- a/templates/shortloglist.tpl
+++ b/templates/shortloglist.tpl
@@ -15,11 +15,14 @@
        <td title="{if $rev->GetAge() > 60*60*24*7*2}{$rev->GetAge()|agestring}{else}{$rev->GetCommitterEpoch()|date_format:"%Y-%m-%d"}{/if}"><em>{if $rev->GetAge() > 60*60*24*7*2}{$rev->GetCommitterEpoch()|date_format:"%Y-%m-%d"}{else}{$rev->GetAge()|agestring}{/if}</em></td>
        <td><em>{$rev->GetAuthorName()}</em></td>
        <td>
-         <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=commit&amp;h={$rev->GetHash()}" class="list commitTip" {if strlen($rev->GetTitle()) > 50}title="{$rev->GetTitle()|htmlspecialchars}"{/if}><strong>{$rev->GetTitle(50)|escape}</strong></a>
+         <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=commit&amp;h={$rev->GetHash()}" class="list commitTip" {if strlen($rev->GetTitle()) > 50}title="{$rev->GetTitle()|htmlspecialchars}"{/if}>
+         {if $rev->IsMergeCommit()}<span class="merge_title">{else}<span class="commit_title">{/if}{$rev->GetTitle(50)|escape}</span>
+         </a>
 	 {include file='refbadges.tpl' commit=$rev}
        </td>
        <td class="link">
-         <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=commit&amp;h={$rev->GetHash()}">{t}commit{/t}</a> | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=commitdiff&amp;h={$rev->GetHash()}">{t}commitdiff{/t}</a> | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=tree&amp;h={$rev->GetHash()}&amp;hb={$rev->GetHash()}">{t}tree{/t}</a> | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=snapshot&amp;h={$rev->GetHash()}" class="snapshotTip">{t}snapshot{/t}</a>
+         {assign var=revtree value=$rev->GetTree()}
+         <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=commit&amp;h={$rev->GetHash()}">{t}commit{/t}</a> | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=commitdiff&amp;h={$rev->GetHash()}">{t}commitdiff{/t}</a> | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=tree&amp;h={$revtree->GetHash()}&amp;hb={$rev->GetHash()}">{t}tree{/t}</a> | <a href="{$SCRIPT_NAME}?p={$project->GetProject()|urlencode}&amp;a=snapshot&amp;h={$rev->GetHash()}" class="snapshotTip">{t}snapshot{/t}</a>
 	 {if $source == 'shortlog'}
 	  | 
 	  {if $mark}

comments