--- /dev/null
+<?php
+/* CommitLoader.class.php - phpgitweb
+ * Copyright (C) 2011-2012 Philipp Kreil (pk910)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class CommitLoader {
+ private $project;
+
+ public function __construct($project) {
+ $this->project = $project;
+ }
+
+ public function load($id) {
+ if(!Validation::validate_hash($id)) {
+ trigger_error("Invalid hash parameter", E_USER_ERROR);
+ return false;
+ }
+ $commit = GitCommand::get_commit($this->project['path'], $id);
+ return $commit;
+ }
+
+
+}
return;
if(!array_key_exists(strtolower($name), $this->content))
$this->content[strtolower($name)] = array();
+ if(!is_array($this->content[strtolower($name)]))
+ $this->content[strtolower($name)] = array($this->content[strtolower($name)]);
$this->content[strtolower($name)][] = $value;
}
case "year":
$rep = date("Y");
break;
- case "title":
- $rep = GitConfig::GITWEB_TITLE;
- break;
case "rendertime":
$rep = "%rendertime%"; //gets replaced later
break;
private static $git = null;
private static $counter = 0;
- private static function command_header($git_path = null) {
+ private static function command_header() {
if(!self::$git) {
//find git executable
if(GitConfig::GIT_EXEC)
}
}
$command = self::$git;
- if($git_path)
- $command .= " --git-dir=".$git_path;
+
return $command;
}
- private static function execute($command) {
+ private static function git_execute($params, $git_path = null) {
$result = array();
+
+ if($git_path) {
+ $args = array("--git-dir=".$git_path);
+ $args = array_merge($args, $params);
+ } else
+ $args = $params;
+
+ $command = self::command_header();
+ foreach($args as $arg) {
+ $command .= ' '.escapeshellarg($arg);
+ }
+
exec($command, $result);
self::$counter++;
return implode("\n", $result);
}
public static function core_version() {
- $command = self::command_header();
- if(!$command)
- return null;
- $command .= " --version";
- $version = self::execute($command);
+ $args = array("--version");
+ $version = self::git_execute($args);
preg_match("/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/i", $version, $result);
return $result[0];
}
public static function last_activity($git_path, $ref = null) {
- $command = self::command_header($git_path);
- if(!$command)
- return null;
- $command .= " for-each-ref --format=%\(committer\) --sort=-committerdate --count=1";
+ $args = array("for-each-ref", "--format=%(committer)", "--sort=-committerdate", "--count=1");
if($ref)
- $command .= " ".$ref;
- $age = self::execute($command);
+ $args[] = $ref;
+ $age = self::git_execute($args, $git_path);
preg_match("/[0-9]{9,}/i", $age, $result);
return $result[0];
}
$commit = array();
$rev_lines = explode("\n", str_replace("\r", "", $commit_data));
$commit['id'] = $rev_lines[0];
+ $commit['parent'] = array();
+ if(!preg_match("/^[a-f0-9]{40}$/i", $commit['id']))
+ return null;
foreach($rev_lines as $rev_line) {
if(substr($rev_line, 0, 4) == " ") {
if(array_key_exists('text', $commit))
else if($opt[0] == "parent")
$commit['parent'][] = $opt[1];
else if($opt[0] == "author") {
- preg_match('/(.*) <([^>]*)> ([0-9]*) [+-0-9]*/i', $opt[1], $matches);
+ preg_match('/(.*) <([^>]*)> ([0-9]*) ([+\-0-9]{5})/i', $opt[1], $matches);
$commit['author'] = $matches[1];
$commit['author_mail'] = $matches[2];
$commit['author_time'] = $matches[3];
+ $commit['author_timezone'] = $matches[4];
} else if($opt[0] == "committer") {
- preg_match('/(.*) <([^>]*)> ([0-9]*) [+-0-9]*/i', $opt[1], $matches);
+ preg_match('/(.*) <([^>]*)> ([0-9]*) ([+\-0-9]{5})/i', $opt[1], $matches);
$commit['committer'] = $matches[1];
$commit['committer_mail'] = $matches[2];
$commit['committer_time'] = $matches[3];
+ $commit['committer_timezone'] = $matches[4];
}
}
}
}
public static function get_commits($git_path, $head, $maxcount, $skip, $file = null) {
- $command = self::command_header($git_path);
- if(!$command)
- return null;
- $command .= " rev-list --header --max-count=".$maxcount." --skip=".$skip." ".($head ? $head : "--all")." --";
+ $args = array("rev-list", "--header", "--max-count=".$maxcount, "--skip=".$skip, ($head ? $head : "--all"), "--");
if($file)
- $command .= " ".$file;
- $commit_list = self::execute($command);
+ $args[] = $file;
+ $commit_list = self::git_execute($args, $git_path);
$commits = array();
foreach(explode("\000", $commit_list) as $commit) {
if($commit)
return $commits;
}
+ public static function get_commit($git_path, $commit_id) {
+ $args = array("rev-list", "--header", "--max-count=1", $commit_id, "--");
+ $commit_data = self::git_execute($args, $git_path);
+ $commit = self::parse_commit($commit_data);
+ return $commit;
+ }
+
+ public static function get_hash($git_path, $ref) {
+ $args = array("rev-parse", "--verify", "-q", $ref);
+ $result = self::git_execute($args, $git_path);
+ if(prag_match("#([a-f0-9]{40})#i", $result, $match))
+ return $match[1];
+ return null;
+ }
+
}
define("PHPGITWEB_VERSION", "0.0.1");
require_once("lib/ContentProvider.class.php");
require_once("lib/ProjectLoader.class.php");
+require_once("lib/CommitLoader.class.php");
require_once("lib/GitCommand.class.php");
require_once("lib/Tools.class.php");
+require_once("lib/Validation.class.php");
require_once("lib/graph.class.php");
class PHPGitWeb {
$this->page->set('git_version', GitCommand::core_version());
$this->project_loader = new ProjectLoader();
+ $this->page->append('title', GitConfig::GITWEB_TITLE);
}
public function get_project_loader() {
$this->project = $this->project_loader->getProject($project);
if(!$this->project)
- $this->page->append('content', new ContentProvider('main', 'project_error'));
+ $this->page->append('content', new ContentProvider('main', 'err404_project'));
else {
ContentProvider::overall_set('project', $this->project['name']);
ContentProvider::overall_set('project_head', "HEAD");
$this->append_header_nav($this->project['name'], '?p='.$this->project['name'].'&a=summary', true);
+ $this->append_title(" - ".$this->project['name'], false);
$this->project_header = new ContentProvider('main', 'project_header');
$this->project_header->set('sub_nav', "");
$this->page->append('content', $this->project_header);
$this->page->append('header_nav', $name);
}
+ public function append_title($name, $prepend_slash = true) {
+ if($prepend_slash)
+ $this->page->append('title', '/');
+ $this->page->append('title', $name);
+ }
+
+ public function append_sub_nav($html) {
+ if($this->project_header)
+ $this->project_header->append('sub_nav', $html);
+ }
+
public function load_extension($extension) {
switch($extension) {
case "graph":
$project['name'] = $name;
$dir_seperator = (substr(GitConfig::PROJECT_ROOT, -1) == '/' ? '' : '/');
+ if(!Validation::validate_path($name))
+ return NULL;
if(is_dir(GitConfig::PROJECT_ROOT.$dir_seperator.$name))
$project['path'] = GitConfig::PROJECT_ROOT.$dir_seperator.$name;
else if(is_dir(GitConfig::PROJECT_ROOT.$dir_seperator.$name.".git"))
return $ctext;
}
+ public static function parseTimeZone($timezone) {
+ if(!preg_match("/^([+-])([0-9]{2})([0-9]{2})$/i", $timezone, $treffer))
+ return 0;
+ $offset = ($treffer[1] == '-' ? -1 : 1) * (($treffer[2] * 3600) + ($treffer[3] * 60));
+ return $offset;
+ }
+
}
?>
\ No newline at end of file
--- /dev/null
+<?php
+/* Validation.class.php - phpgitweb
+ * Copyright (C) 2011-2012 Philipp Kreil (pk910)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class Validation {
+
+ public static function validate_path($path) {
+ /* Path validation #1
+ * no '.' or '..' as elements of path, i.e. no '.' nor '..'
+ * at the beginning, at the end, and between slashes.
+ * also this catches doubled slashes
+ */
+ if(preg_match('#(^|/)(|\.|\.\.)(/|$)#', $path))
+ return false;
+
+ /* Path validation #2
+ * no null characters
+ */
+ if(preg_match('#\0#', $path))
+ return false;
+
+ return true;
+ }
+
+ public static function validate_hash($hash) {
+ /* Hash validation #1
+ * regular hashes [a-f0-9] are always ok
+ */
+ if(preg_match('#^[a-f0-9]{1,40}$#i', $hash))
+ return true;
+
+ /* Hash validation #2
+ * must be a valid path
+ */
+ if(!self::validate_path($hash))
+ return false;
+
+ /* Hash validation #3
+ * restrictions on ref name according to git-check-ref-format
+ */
+ if(preg_match('#(\.|\.\.|[\000-\040\177 ~^:?*\[\]]|/$)#', $hash))
+ return false;
+
+ return true;
+ }
+
+}
+
+?>
\ No newline at end of file
--- /dev/null
+<?php
+/* commit.class.php - phpgitweb
+ * Copyright (C) 2011-2012 Philipp Kreil (pk910)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+require_once('pages/shortlog.class.php');
+
+class page_commit {
+ private $page, $phpgitweb;
+ private $commitid;
+
+ public function main($phpgitweb, $project) {
+ $this->phpgitweb = $phpgitweb;
+ if(!$project)
+ return new ContentProvider('main', 'err400');
+ $project['refs'] = $phpgitweb->get_project_loader()->getProjectRefs($project);
+ $phpgitweb->append_header_nav("commit", null, true);
+ $phpgitweb->append_title("commit");
+
+ $commit_loader = new CommitLoader($project);
+
+ if(array_key_exists('h', $_GET))
+ $commitid = $_GET['h'];
+ else
+ $commitid = "HEAD";
+
+ $commit = $commit_loader->load($commitid);
+ if(!$commit)
+ return new ContentProvider('main', 'err404_object');
+
+ ContentProvider::overall_set('project_head', $commit['id']);
+
+
+ $this->page = new ContentProvider('commit', 'main');
+ $this->page->set('hash', $commit['id']);
+ $this->page->set('author', htmlentities($commit['author']));
+ $this->page->set('author_mail', htmlentities($commit['author_mail']));
+ $this->page->set('author_date', gmdate('r', $commit['author_time']));
+ $author_local_time = $commit['author_time'] + Tools::parseTimeZone($commit['author_timezone']);
+ if(gmdate('H', $author_local_time) < 6)
+ $this->page->set('author_local_date', '<span class="atnight">'.gmdate('H:i', $author_local_time).'</span>');
+ else
+ $this->page->set('author_local_date', gmdate('H:i', $author_local_time));
+ $this->page->set('author_timezone', $commit['author_timezone']);
+ $this->page->set('committer', htmlentities($commit['committer']));
+ $this->page->set('committer_mail', htmlentities($commit['committer_mail']));
+ $this->page->set('committer_date', gmdate('r', $commit['committer_time']));
+ $committer_local_time = $commit['committer_time'] + Tools::parseTimeZone($commit['committer_timezone']);
+ if(gmdate('H', $committer_local_time) < 6)
+ $this->page->set('committer_local_date', '<span class="atnight">'.gmdate('H:i', $committer_local_time).'</span>');
+ else
+ $this->page->set('committer_local_date', gmdate('H:i', $committer_local_time));
+ $this->page->set('committer_timezone', $commit['committer_timezone']);
+ $this->page->set('message', htmlentities(Tools::chop_text($commit['text'], 50, 5)));
+ $this->page->set('full_message', htmlentities($commit['text']));
+ $this->page->set('tree_hash', $commit['tree']);
+ foreach($commit['parent'] as $parent) {
+ $this->page->append('parents', new ContentProvider('commit', 'parent', array('hash' => $parent, 'head' => $commit['id'])));
+ }
+
+ $refs = new ContentProvider('commit', 'commit_refs');
+ $found = false;
+ foreach($project['refs'] as $ref => $rhash) {
+ if(strtolower($rhash) == strtolower($commit['id'])) {
+ $refexp = explode('/', $ref, 3);
+ $reftype = $refexp[1];
+ if($reftype == 'heads')
+ $reftype = 'head';
+ else if($reftype == 'remotes')
+ $reftype = 'remote';
+ else if($reftype == 'tags')
+ $reftype = 'tag';
+ $refs->append('refs', new ContentProvider('commit', 'commit_ref_'.$reftype, array("name" => $refexp[2], "ref_link" => $ref)));
+ $found = true;
+ }
+ }
+ $this->page->set('refs', ($found ? $refs : ""));
+
+ return $this->page;
+ }
+
+}
+
+?>
\ No newline at end of file
$age = Tools::age_calculate($project['last_change']);
- $entry->set('ageclass', $age['age_class']);
+ $entry->set('age_class', $age['age_class']);
$entry->set('age', $age['age_str']);
return $entry;
}
private $project;
private $graph_data;
private $first_commit;
+ private $have_more = false;
public function generate_shortlog($project, $head, $max, $skip, $file = null, $pages = true, $next_page = 0) {
$this->project = $project;
if($commit_counter < $skip)
continue;
if($commit_counter > $max+$skip) {
+ $this->have_more = true;
if($pages) {
$content->append('entries', new ContentProvider('shortlog', 'shortlog_page', array("page" => $next_page)));
} else
return $this->first_commit;
}
+ public function get_have_more() {
+ return $this->have_more;
+ }
+
private function shortlog_entry($class, $commit) {
$entry = new ContentProvider('shortlog', 'shortlog_entry');
$entry->set('class', $class);
if(GitConfig::GITGRAPH_ENABLE)
$entry->set('graph_data', $this->graph_data->get_graph($commit['id']));
- $entry->set('refs', $this->shortlog_commit_refs($commit['id']));
+ $entry->set('refs', $this->shortlog_commit_refs($this->project, $commit['id']));
return $entry;
}
- private function shortlog_commit_refs($hash) {
- if(!is_array($this->project['refs']))
+ public function shortlog_commit_refs($project, $hash) {
+ if(!is_array($project['refs']))
return "";
$refs = new ContentProvider('shortlog', 'shortlog_refs');
$found = false;
- foreach($this->project['refs'] as $ref => $rhash) {
+ foreach($project['refs'] as $ref => $rhash) {
if(strtolower($rhash) == strtolower($hash)) {
$refexp = explode('/', $ref, 3);
$reftype = $refexp[1];
$this->phpgitweb = $phpgitweb;
$this->project = $project;
if(!$this->project)
- return;
+ return new ContentProvider('main', 'err400');
$project['refs'] = $phpgitweb->get_project_loader()->getProjectRefs($project);
$phpgitweb->append_header_nav("shortlog", null, true);
+ $phpgitweb->append_title("shortlog");
$this->page = new ContentProvider('shortlog', 'main');
//pages
if(array_key_exists('pg', $_GET)) {
+ $pg = $_GET['pg'];
+ if($pg < 0)
+ $pg = 0;
$skip = $_GET['pg'] * 100;
$next_page = $_GET['pg'] + 1;
} else {
+ $pg = 0;
$skip = 0;
$next_page = 1;
}
+ $subnav = new ContentProvider('shortlog', 'shortlog_subnav');
+ $phpgitweb->append_sub_nav($subnav);
+
+ if($pg) {
+ $subnav->set('first', new ContentProvider('shortlog', 'shortlog_subnav_first_link'));
+ $subnav->set('prev', new ContentProvider('shortlog', 'shortlog_subnav_prev_link', array('page' => ($pg - 1))));
+ } else {
+ $subnav->set('first', new ContentProvider('shortlog', 'shortlog_subnav_first'));
+ $subnav->set('prev', new ContentProvider('shortlog', 'shortlog_subnav_prev'));
+ }
+
$shortlog = new shortlog();
$this->page->set('shortlog', $shortlog->generate_shortlog($project, null, 100, $skip, null, true, $next_page));
+ if($shortlog->get_have_more())
+ $subnav->set('next', new ContentProvider('shortlog', 'shortlog_subnav_next_link', array('page' => ($pg + 1))));
+ else
+ $subnav->set('next', new ContentProvider('shortlog', 'shortlog_subnav_next'));
+
return $this->page;
}
public function main($phpgitweb, $project) {
$this->phpgitweb = $phpgitweb;
if(!$project)
- return;
+ return new ContentProvider('main', 'err400');
$project['refs'] = $phpgitweb->get_project_loader()->getProjectRefs($project);
$phpgitweb->append_header_nav("summary", null, true);
+ $phpgitweb->append_title("summary");
$this->page = new ContentProvider('summary', 'main');
$this->page->set('shortlog', $shortlog->generate_shortlog($project, null, 16, 0, null, false));
$first_commit = $shortlog->get_first_commit();
- $this->page->set('last_change', date('r', $first_commit['committer_time']));
+ $this->page->set('last_change', gmdate('r', $first_commit['committer_time']));
return $this->page;
}
--- /dev/null
+# [main]
+<div class="header">
+<a class="title" href="?p=%project%&a=commitdiff&h=%hash%">%message% %refs%</a>
+</div>
+<div class="title_text">
+<table class="object_header">
+ <tr>
+ <td>author</td>
+ <td>
+ <a title="Search for commits authored by %author%" class="list" href="?p=%project%&a=search&h=%hash%&s=%author%&st=author">%author%</a>
+ <a title="Search for commits authored by %author_mail%" class="list" href="?p=%project%&a=search&h=%hash%&s=%author_mail%&st=author"><%author_mail%></a>
+ </td>
+ <td rowspan="2"></td>
+ </tr>
+ <tr>
+ <td></td>
+ <td> %author_date% (%author_local_date% %author_timezone%)</td>
+ </tr>
+ <tr>
+ <td>committer</td>
+ <td>
+ <a title="Search for commits committed by %committer%" class="list" href="?p=%project%&a=search&h=%hash%&s=pk910&st=committer">%committer%</a>
+ <a title="Search for commits committed by %committer_mail%" class="list" href="?p=%project%&a=search&h=%hash%&s=%committer_mail%&st=committer"><%committer_mail%></a>
+ </td>
+ <td rowspan="2"></td>
+ </tr>
+ <tr>
+ <td></td>
+ <td> %committer_date% (%committer_local_date% %committer_timezone%)</td>
+ </tr>
+ <tr>
+ <td>commit</td>
+ <td class="sha1">%hash%</td>
+ </tr>
+ <tr>
+ <td>tree</td>
+ <td class="sha1"><a class="list" href="?p=%project%&a=tree&h=%tree_hash%&hb=%hash%">%tree_hash%</a></td>
+ <td class="link">
+ <a href="?p=%project%&a=tree&h=%tree_hash%&hb=%hash%">tree</a> |
+ <a title="in format: tar.gz" href="?p=%project%&a=snapshot&h=%hash%&sf=tgz">snapshot</a>
+ </td>
+ </tr>
+%parents%
+</table>
+</div>
+<div class="page_body">
+%full_message%<br/>
+</div>
+
+<div class="list_head">
+</div>
+<table class="diff_tree">
+%tree%
+</table>
+
+
+# [parent]
+ <tr>
+ <td>parent</td>
+ <td class="sha1"><a class="list" href="?p=%project%&a=commit&h=%hash%">%hash%</a></td>
+ <td class="link">
+ <a href="?p=%project%&a=commit&h=%hash%">commit</a> |
+ <a href="?p=%project%&a=commitdiff&h=%head%;hp=%hash%">diff</a>
+ </td>
+ </tr>
+
+# [commit_refs]
+<span class="refs"> %refs%</span>
+
+# [commit_ref_head]
+<span class="head" title="heads/%name%">%name%</span>
+
+# [commit_ref_remote]
+<span class="remote" title="remotes/%name%">%name%</span>
+
+# [commit_ref_tag]
+<span class="tag" title="tags/%name%">%name%</span>
+
+# [tree]
+<tr class="%class%">
+ <td><a class="list" href="?p=%project%&a=blob&h=%hash%&f=%file%">src/mod-helpserv.c</a></td>
+ <td></td>
+ <td class="link">
+ <a href="?p=%project%&a=blobdiff&h=%hash%&f=src/mod-helpserv.c&hp=%parent%">diff</a> |
+ <a href="?p=%project%&a=blob&h=%hash%&f=%file%">blob</a> |
+ <a href="?p=%project%&a=history&h=%hash%&f=%file%">history</a></td>
+</tr>
</pre>
</div>
-# [project_error]
+# [err400]
<div class="page_body">
+<br /><br />
+404 - Project needed
+<br /><br />
+</div>
+
+# [err404_project]
+<div class="page_body">
+<br /><br />
404 - Project not found
<br /><br />
</div>
+# [err404_object]
+<div class="page_body">
+<br /><br />
+404 - Unknown commit object
+<br /><br />
+</div>
+
# [project_header]
<form method="get" enctype="application/x-www-form-urlencoded">
<div class="search">
# [main]
+<form method="get" enctype="application/x-www-form-urlencoded">
+<p class="projsearch">Search:
+<input type="text" name="s" />
+</p>
+</form>
<table class="project_list">
<tr>
<th>%header_project%</th>
<span class="remote" title="remotes/%name%"><a href="?p=%project%&a=shortlog&h=%ref_link%">%name%</a></span>
# [shortlog_ref_tag]
-<span class="tag" title="tags/%name%"><a href="?p=%project%&a=shortlog&h=%ref_link%">%name%</a></span>
\ No newline at end of file
+<span class="tag" title="tags/%name%"><a href="?p=%project%&a=shortlog&h=%ref_link%">%name%</a></span>
+
+# [shortlog_subnav]
+%first% ⋅ %prev% ⋅ %next%
+
+# [shortlog_subnav_first_link]
+<a href="?p=%project%&a=shortlog">first</a>
+
+# [shortlog_subnav_first]
+first
+
+# [shortlog_subnav_prev_link]
+<a title="Alt-p" accesskey="p" href="?p=%project%&a=shortlog&pg=%page%">prev</a>
+
+# [shortlog_subnav_prev]
+prev
+
+# [shortlog_subnav_next_link]
+<a title="Alt-n" accesskey="n" href="?p=%project%&a=shortlog&pg=%page%">next</a>
+
+# [shortlog_subnav_next]
+next
+