Allow browsing of tags/branches.
[viewgit.git] / index.php
blobbe627a12295daa017a397964b609220c10d6c25b
1 <?php
2 error_reporting(E_ALL);
4 require_once('inc/config.php');
6 function debug($msg)
8 file_put_contents('/tmp/viewgit.log', strftime('%H:%M:%S') ." $_SERVER[REMOTE_ADDR]:$_SERVER[REMOTE_PORT] $msg\n", FILE_APPEND);
11 /**
12 * Formats "git diff" output into xhtml.
13 * @return array(array of filenames, xhtml)
15 function format_diff($text)
17 $files = array();
19 // match every "^diff --git a/<path> b/<path>$" line
20 foreach (explode("\n", $text) as $line) {
21 if (preg_match('#^diff --git a/(.*) b/(.*)$#', $line, $matches) > 0) {
22 $files[$matches[1]] = urlencode($matches[1]);
26 $text = htmlentities($text);
28 $text = preg_replace(
29 array(
30 '/^(\+.*)$/m',
31 '/^(-.*)$/m',
32 '/^(@.*)$/m',
33 '/^([^d\+-@].*)$/m',
35 array(
36 '<span class="add">$1</span>',
37 '<span class="del">$1</span>',
38 '<span class="pos">$1</span>',
39 '<span class="etc">$1</span>',
41 $text);
42 $text = preg_replace_callback('#^diff --git a/(.*) b/(.*)$#m',
43 create_function(
44 '$m',
45 'return "<span class=\"diffline\"><a name=\"". urlencode($m[1]) ."\">diff --git a/$m[1] b/$m[2]</a></span>";'
47 $text);
49 return array($files, $text);
52 function get_project_info($name)
54 global $conf;
56 $info = $conf['projects'][$name];
57 $info['name'] = $name;
58 $info['description'] = file_get_contents($info['repo'] .'/description');
60 $headinfo = git_get_commit_info($name, 'HEAD');
61 $info['head_stamp'] = $headinfo['author_utcstamp'];
62 $info['head_datetime'] = strftime($conf['datetime'], $headinfo['author_utcstamp']);
63 $info['head_hash'] = $headinfo['h'];
64 $info['head_tree'] = $headinfo['tree'];
66 return $info;
69 /**
70 * Get details of a commit: tree, parent, author/committer (name, mail, date), message
72 function git_get_commit_info($project, $hash = 'HEAD')
74 global $conf;
76 $info = array();
77 $info['h_name'] = $hash;
78 $info['message_full'] = '';
80 $output = run_git($project, "git rev-list --header --max-count=1 $hash");
81 // tree <h>
82 // parent <h>
83 // author <name> "<"<mail>">" <stamp> <timezone>
84 // committer
85 // <empty>
86 // <message>
87 $pattern = '/^(author|committer) ([^<]+) <([^>]*)> ([0-9]+) (.*)$/';
88 foreach ($output as $line) {
89 if (substr($line, 0, 4) === 'tree') {
90 $info['tree'] = substr($line, 5);
92 elseif (substr($line, 0, 6) === 'parent') {
93 $info['parent'] = substr($line, 7);
95 elseif (preg_match($pattern, $line, $matches) > 0) {
96 $info[$matches[1] .'_name'] = $matches[2];
97 $info[$matches[1] .'_mail'] = $matches[3];
98 $info[$matches[1] .'_stamp'] = $matches[4];
99 $info[$matches[1] .'_timezone'] = $matches[5];
100 $info[$matches[1] .'_utcstamp'] = $matches[4] - ((intval($matches[5]) / 100.0) * 3600);
102 elseif (substr($line, 0, 4) === ' ') {
103 $info['message_full'] .= substr($line, 4) ."\n";
104 if (!isset($info['message'])) {
105 $info['message'] = substr($line, 4, $conf['commit_message_maxlen']);
106 $info['message_firstline'] = substr($line, 4);
109 elseif (preg_match('/^[0-9a-f]{40}$/', $line) > 0) {
110 $info['h'] = $line;
114 return $info;
117 function git_get_heads($project)
119 $heads = array();
121 $output = run_git($project, 'git show-ref --heads');
122 foreach ($output as $line) {
123 $fullname = substr($line, 41);
124 $name = array_pop(explode('/', $fullname));
125 $heads[] = array('h' => substr($line, 0, 40), 'fullname' => "$fullname", 'name' => "$name");
128 return $heads;
131 function git_get_rev_list($project, $max_count = null, $start = 'HEAD')
133 $cmd = "git rev-list $start";
134 if (!is_null($max_count)) {
135 $cmd = "git rev-list --max-count=$max_count $start";
138 return run_git($project, $cmd);
141 function git_get_tags($project)
143 $tags = array();
145 $output = run_git($project, 'git show-ref --tags');
146 foreach ($output as $line) {
147 $fullname = substr($line, 41);
148 $name = array_pop(explode('/', $fullname));
149 $tags[] = array('h' => substr($line, 0, 40), 'fullname' => $fullname, 'name' => $name);
151 return $tags;
154 function git_ls_tree($project, $tree)
156 $entries = array();
157 $output = run_git($project, "git ls-tree $tree");
158 // 100644 blob 493b7fc4296d64af45dac64bceac2d9a96c958c1 .gitignore
159 // 040000 tree 715c78b1011dc58106da2a1af2fe0aa4c829542f doc
160 foreach ($output as $line) {
161 $parts = preg_split('/\s+/', $line, 4);
162 $entries[] = array('name' => $parts[3], 'mode' => $parts[0], 'type' => $parts[1], 'hash' => $parts[2]);
165 return $entries;
168 function makelink($dict)
170 $params = array();
171 foreach ($dict as $k => $v) {
172 $params[] = rawurlencode($k) .'='. str_replace('%2F', '/', rawurlencode($v));
174 if (count($params) > 0) {
175 return '?'. htmlentities(join('&', $params));
177 return '';
181 * Executes a git command in the project repo.
182 * @return array of output lines
184 function run_git($project, $command)
186 global $conf;
188 $output = array();
189 $cmd = "GIT_DIR=". $conf['projects'][$project]['repo'] ." $command";
190 exec($cmd, &$output);
191 return $output;
195 * Executes a git command in the project repo, sending output directly to the
196 * client.
198 function run_git_passthru($project, $command)
200 global $conf;
202 $cmd = "GIT_DIR=". $conf['projects'][$project]['repo'] ." $command";
203 $result = 0;
204 passthru($cmd, &$result);
205 return $result;
209 * Makes sure the given project is valid. If it's not, this function will
210 * die().
211 * @return the project
213 function validate_project($project)
215 global $conf;
217 if (!in_array($project, array_keys($conf['projects']))) {
218 die('Invalid project');
220 return $project;
224 * Makes sure the given hash is valid. If it's not, this function will die().
225 * @return the hash
227 function validate_hash($hash)
229 if (!preg_match('/^[0-9a-z]{40}$/', $hash) && !preg_match('!^refs/(heads|tags)/[-.0-9a-z]+$!', $hash)) {
230 die('Invalid hash');
233 return $hash;
236 $action = 'index';
237 $template = 'index';
238 $page['title'] = 'ViewGit';
240 if (isset($_REQUEST['a'])) {
241 $action = strtolower($_REQUEST['a']);
243 $page['action'] = $action;
245 if ($action === 'index') {
246 $template = 'index';
247 $page['title'] = 'List of projects - ViewGit';
249 foreach (array_keys($conf['projects']) as $p) {
250 $page['projects'][] = get_project_info($p);
253 elseif ($action === 'archive') {
254 $project = validate_project($_REQUEST['p']);
255 $tree = validate_hash($_REQUEST['h']);
256 $type = $_REQUEST['t'];
258 $basename = "$project-tree-$tree";
259 if (isset($_REQUEST['n'])) {
260 $basename = "$project-$_REQUEST[n]-". substr($tree, 0, 6);
263 if ($type === 'targz') {
264 header("Content-Type: application/x-tar-gz");
265 header("Content-Transfer-Encoding: binary");
266 header("Content-Disposition: attachment; filename=\"$basename.tar.gz\";");
267 run_git_passthru($project, "git archive --format=tar $tree |gzip");
269 elseif ($type === 'zip') {
270 header("Content-Type: application/x-zip");
271 header("Content-Transfer-Encoding: binary");
272 header("Content-Disposition: attachment; filename=\"$basename.zip\";");
273 run_git_passthru($project, "git archive --format=zip $tree");
275 else {
276 die('Invalid archive type requested');
279 die();
281 // blob: send a blob to browser with filename suggestion
282 elseif ($action === 'blob') {
283 $project = validate_project($_REQUEST['p']);
284 $hash = validate_hash($_REQUEST['h']);
285 $name = $_REQUEST['n'];
287 header('Content-type: application/octet-stream');
288 header("Content-Disposition: attachment; filename=$name"); // FIXME needs quotation
290 run_git_passthru($project, "git cat-file blob $hash");
291 die();
294 * git checkout.
296 elseif ($action === 'co') {
297 if (!$conf['allow_checkout']) { die('Checkout not allowed'); }
299 // For debugging
300 debug("Project: $_REQUEST[p] Request: $_REQUEST[r]");
302 // eg. info/refs, HEAD
303 $p = validate_project($_REQUEST['p']); // project
304 $r = $_REQUEST['r']; // path
306 $gitdir = $conf['projects'][$p]['repo'];
307 $filename = $gitdir .'/'. $r;
309 // make sure the request is legit (no reading of other files besides those under git projects)
310 if ($r === 'HEAD' || $r === 'info/refs' || preg_match('!^objects/info/(packs|http-alternates|alternates)$!', $r) > 0 || preg_match('!^objects/[0-9a-f]{2}/[0-9a-f]{38}$!', $r) > 0) {
311 if (file_exists($filename)) {
312 debug('OK, sending');
313 readfile($filename);
314 } else {
315 debug('Not found');
316 header('404');
318 } else {
319 debug("Denied");
322 die();
324 elseif ($action === 'commit') {
325 $template = 'commit';
326 $page['project'] = validate_project($_REQUEST['p']);
327 $page['title'] = "$page[project] - Commit - ViewGit";
328 $page['commit_id'] = validate_hash($_REQUEST['h']);
330 $info = git_get_commit_info($page['project'], $page['commit_id']);
332 $page['author_name'] = $info['author_name'];
333 $page['author_mail'] = $info['author_mail'];
334 $page['author_datetime'] = strftime($conf['datetime'], $info['author_utcstamp']);
335 $page['author_datetime_local'] = strftime($conf['datetime'], $info['author_stamp']) .' '. $info['author_timezone'];
336 $page['committer_name'] = $info['committer_name'];
337 $page['committer_mail'] = $info['committer_mail'];
338 $page['committer_datetime'] = strftime($conf['datetime'], $info['committer_utcstamp']);
339 $page['committer_datetime_local'] = strftime($conf['datetime'], $info['committer_stamp']) .' '. $info['committer_timezone'];
340 $page['tree_id'] = $info['tree'];
341 $page['parent'] = $info['parent'];
342 $page['message'] = $info['message'];
343 $page['message_firstline'] = $info['message_firstline'];
344 $page['message_full'] = $info['message_full'];
347 elseif ($action === 'commitdiff') {
348 $template = 'commitdiff';
349 $page['project'] = validate_project($_REQUEST['p']);
350 $page['title'] = "$page[project] - Commitdiff - ViewGit";
351 $hash = validate_hash($_REQUEST['h']);
352 $page['commit_id'] = $hash;
354 $info = git_get_commit_info($page['project'], $hash);
356 $page['tree_id'] = $info['tree'];
358 $page['message'] = $info['message'];
359 $page['message_firstline'] = $info['message_firstline'];
360 $page['message_full'] = $info['message_full'];
361 $page['author_name'] = $info['author_name'];
362 $page['author_mail'] = $info['author_mail'];
363 $page['author_datetime'] = strftime($conf['datetime'], $info['author_utcstamp']);
365 $text = join("\n", run_git($page['project'], "git diff $hash^..$hash"));
366 list($page['files'], $page['diffdata']) = format_diff($text);
367 //$page['diffdata'] = format_diff($text);
369 elseif ($action === 'shortlog') {
370 $template = 'shortlog';
371 $page['project'] = validate_project($_REQUEST['p']);
372 $page['title'] = "$page[project] - Shortlog - ViewGit";
373 if (isset($_REQUEST['h'])) {
374 $page['ref'] = validate_hash($_REQUEST['h']);
375 } else {
376 $page['ref'] = 'HEAD';
379 $info = git_get_commit_info($page['project'], $page['ref']);
380 $page['commit_id'] = $info['h'];
381 $page['tree_id'] = $info['tree'];
383 // TODO merge the logic with 'summary' below
384 $revs = git_get_rev_list($page['project'], $conf['summary_shortlog'], $page['ref']); // TODO pass first rev as parameter
385 foreach ($revs as $rev) {
386 $info = git_get_commit_info($page['project'], $rev);
387 $page['shortlog'][] = array(
388 'author' => $info['author_name'],
389 'date' => strftime($conf['datetime'], $info['author_utcstamp']),
390 'message' => $info['message'],
391 'commit_id' => $rev,
392 'tree' => $info['tree'],
396 elseif ($action === 'summary') {
397 $template = 'summary';
398 $page['project'] = validate_project($_REQUEST['p']);
399 $page['title'] = "$page[project] - Summary - ViewGit";
401 $info = git_get_commit_info($page['project']);
402 $page['commit_id'] = $info['h'];
403 $page['tree_id'] = $info['tree'];
405 $revs = git_get_rev_list($page['project'], $conf['summary_shortlog']);
406 foreach ($revs as $rev) {
407 $info = git_get_commit_info($page['project'], $rev);
408 $page['shortlog'][] = array(
409 'author' => $info['author_name'],
410 'date' => strftime($conf['datetime'], $info['author_utcstamp']),
411 'message' => $info['message'],
412 'commit_id' => $rev,
413 'tree' => $info['tree'],
417 $tags = git_get_tags($page['project']);
418 $page['tags'] = array();
419 foreach ($tags as $tag) {
420 $info = git_get_commit_info($page['project'], $tag['h']);
421 $page['tags'][] = array(
422 'date' => strftime($conf['datetime'], $info['author_utcstamp']),
423 'h' => $tag['h'],
424 'fullname' => $tag['fullname'],
425 'name' => $tag['name'],
429 $heads = git_get_heads($page['project']);
430 $page['heads'] = array();
431 foreach ($heads as $h) {
432 $info = git_get_commit_info($page['project'], $h['h']);
433 $page['heads'][] = array(
434 'date' => strftime($conf['datetime'], $info['author_utcstamp']),
435 'h' => $h['h'],
436 'fullname' => $h['fullname'],
437 'name' => $h['name'],
442 * Shows a tree, with list of directories/files, links to them and download
443 * links to archives.
445 * @param p project
446 * @param h tree hash
447 * @param hb OPTIONAL base commit (trees can be part of multiple commits, this
448 * one denotes which commit the user navigated from)
450 elseif ($action === 'tree') {
451 $template = 'tree';
452 $page['project'] = validate_project($_REQUEST['p']);
453 $page['tree_id'] = validate_hash($_REQUEST['h']);
454 $page['title'] = "$page[project] - Tree - ViewGit";
456 // 'hb' optionally contains the commit_id this tree is related to
457 if (isset($_REQUEST['hb'])) {
458 $page['commit_id'] = validate_hash($_REQUEST['hb']);
460 else {
461 // for the header
462 $info = git_get_commit_info($page['project']);
463 $page['commit_id'] = $info['h'];
466 $page['path'] = '';
467 if (isset($_REQUEST['f'])) {
468 $page['path'] = $_REQUEST['f']; // TODO validate?
471 $page['entries'] = git_ls_tree($page['project'], $page['tree_id']);
474 * View a blob as inline, embedded on the page.
475 * @param p project
476 * @param h blob hash
477 * @param hb OPTIONAL base commit
479 elseif ($action === 'viewblob') {
480 $template = 'blob';
481 $page['project'] = validate_project($_REQUEST['p']);
482 $page['hash'] = validate_hash($_REQUEST['h']);
483 $page['title'] = "$page[project] - Blob - ViewGit";
484 if (isset($_REQUEST['hb'])) {
485 $page['commit_id'] = validate_hash($_REQUEST['hb']);
487 else {
488 $page['commit_id'] = 'HEAD';
491 $page['path'] = '';
492 if (isset($_REQUEST['f'])) {
493 $page['path'] = $_REQUEST['f']; // TODO validate?
496 // For the header's pagenav
497 $info = git_get_commit_info($page['project'], $page['commit_id']);
498 $page['commit_id'] = $info['h'];
499 $page['tree_id'] = $info['tree'];
501 $page['data'] = join("\n", run_git($page['project'], "git cat-file blob $page[hash]"));
503 else {
504 die('Invalid action');
507 require 'templates/header.php';
508 require "templates/$template.php";
509 require 'templates/footer.php';