More todo items.
[viewgit.git] / index.php
blob4b07d6d1e612af57c073e747ea1f71c4c82401a1
1 <?php
2 /** @file
3 * The main "controller" file of ViewGit.
5 * All requests come to this file. You can think of it as the controller in the
6 * Model-View-Controller pattern. It reads config, processes user input,
7 * fetches required data using git commandline, and finally passes the data to
8 * templates to be shown to the user.
9 */
10 error_reporting(E_ALL);
12 require_once('inc/config.php');
14 function debug($msg)
16 file_put_contents('/tmp/viewgit.log', strftime('%H:%M:%S') ." $_SERVER[REMOTE_ADDR]:$_SERVER[REMOTE_PORT] $msg\n", FILE_APPEND);
19 /**
20 * Formats "git diff" output into xhtml.
21 * @return array(array of filenames, xhtml)
23 function format_diff($text)
25 $files = array();
27 // match every "^diff --git a/<path> b/<path>$" line
28 foreach (explode("\n", $text) as $line) {
29 if (preg_match('#^diff --git a/(.*) b/(.*)$#', $line, $matches) > 0) {
30 $files[$matches[1]] = urlencode($matches[1]);
34 $text = htmlentities($text);
36 $text = preg_replace(
37 array(
38 '/^(\+.*)$/m',
39 '/^(-.*)$/m',
40 '/^(@.*)$/m',
41 '/^([^d\+-@].*)$/m',
43 array(
44 '<span class="add">$1</span>',
45 '<span class="del">$1</span>',
46 '<span class="pos">$1</span>',
47 '<span class="etc">$1</span>',
49 $text);
50 $text = preg_replace_callback('#^diff --git a/(.*) b/(.*)$#m',
51 create_function(
52 '$m',
53 'return "<span class=\"diffline\"><a name=\"". urlencode($m[1]) ."\">diff --git a/$m[1] b/$m[2]</a></span>";'
55 $text);
57 return array($files, $text);
60 /**
61 * Get project information from config and git, name/description and HEAD
62 * commit info are returned in an array.
64 function get_project_info($name)
66 global $conf;
68 $info = $conf['projects'][$name];
69 $info['name'] = $name;
70 $info['description'] = file_get_contents($info['repo'] .'/description');
72 $headinfo = git_get_commit_info($name, 'HEAD');
73 $info['head_stamp'] = $headinfo['author_utcstamp'];
74 $info['head_datetime'] = strftime($conf['datetime'], $headinfo['author_utcstamp']);
75 $info['head_hash'] = $headinfo['h'];
76 $info['head_tree'] = $headinfo['tree'];
78 return $info;
81 /**
82 * Get details of a commit: tree, parents, author/committer (name, mail, date), message
84 function git_get_commit_info($project, $hash = 'HEAD')
86 global $conf;
88 $info = array();
89 $info['h_name'] = $hash;
90 $info['message_full'] = '';
91 $info['parents'] = array();
93 $output = run_git($project, "git rev-list --header --max-count=1 $hash");
94 // tree <h>
95 // parent <h>
96 // author <name> "<"<mail>">" <stamp> <timezone>
97 // committer
98 // <empty>
99 // <message>
100 $pattern = '/^(author|committer) ([^<]+) <([^>]*)> ([0-9]+) (.*)$/';
101 foreach ($output as $line) {
102 if (substr($line, 0, 4) === 'tree') {
103 $info['tree'] = substr($line, 5);
105 // may be repeated multiple times for merge/octopus
106 elseif (substr($line, 0, 6) === 'parent') {
107 $info['parents'][] = substr($line, 7);
109 elseif (preg_match($pattern, $line, $matches) > 0) {
110 $info[$matches[1] .'_name'] = $matches[2];
111 $info[$matches[1] .'_mail'] = $matches[3];
112 $info[$matches[1] .'_stamp'] = $matches[4];
113 $info[$matches[1] .'_timezone'] = $matches[5];
114 $info[$matches[1] .'_utcstamp'] = $matches[4] - ((intval($matches[5]) / 100.0) * 3600);
116 elseif (substr($line, 0, 4) === ' ') {
117 $info['message_full'] .= substr($line, 4) ."\n";
118 if (!isset($info['message'])) {
119 $info['message'] = substr($line, 4, $conf['commit_message_maxlen']);
120 $info['message_firstline'] = substr($line, 4);
123 elseif (preg_match('/^[0-9a-f]{40}$/', $line) > 0) {
124 $info['h'] = $line;
128 return $info;
132 * Get list of heads (branches) for a project.
134 function git_get_heads($project)
136 $heads = array();
138 $output = run_git($project, 'git show-ref --heads');
139 foreach ($output as $line) {
140 $fullname = substr($line, 41);
141 $name = array_pop(explode('/', $fullname));
142 $heads[] = array('h' => substr($line, 0, 40), 'fullname' => "$fullname", 'name' => "$name");
145 return $heads;
149 * Get array containing path information for parts, starting from root_hash.
151 * @param root_hash commit/tree hash for the root tree
152 * @param parts array of path fragments
154 function git_get_path_info($project, $root_hash, $parts)
156 $pathinfo = array();
158 $tid = $root_hash;
159 $pathinfo = array();
160 foreach ($parts as $p) {
161 $entry = git_ls_tree_part($project, $tid, $p);
162 $pathinfo[] = $entry;
163 $tid = $entry['hash'];
166 return $pathinfo;
170 * Get revision list starting from given commit.
171 * @param max_count number of commit hashes to return, or all if not given
172 * @param start revision to start from, or HEAD if not given
174 function git_get_rev_list($project, $max_count = null, $start = 'HEAD')
176 $cmd = "git rev-list $start";
177 if (!is_null($max_count)) {
178 $cmd = "git rev-list --max-count=$max_count $start";
181 return run_git($project, $cmd);
185 * Get list of tags for a project.
187 function git_get_tags($project)
189 $tags = array();
191 $output = run_git($project, 'git show-ref --tags');
192 foreach ($output as $line) {
193 $fullname = substr($line, 41);
194 $name = array_pop(explode('/', $fullname));
195 $tags[] = array('h' => substr($line, 0, 40), 'fullname' => $fullname, 'name' => $name);
197 return $tags;
201 * Get information about objects in a tree.
202 * @param tree tree or commit hash
203 * @return list of arrays containing name, mode, type, hash
205 function git_ls_tree($project, $tree)
207 $entries = array();
208 $output = run_git($project, "git ls-tree $tree");
209 // 100644 blob 493b7fc4296d64af45dac64bceac2d9a96c958c1 .gitignore
210 // 040000 tree 715c78b1011dc58106da2a1af2fe0aa4c829542f doc
211 foreach ($output as $line) {
212 $parts = preg_split('/\s+/', $line, 4);
213 $entries[] = array('name' => $parts[3], 'mode' => $parts[0], 'type' => $parts[1], 'hash' => $parts[2]);
216 return $entries;
220 * Get information about the given object in a tree, or null if not in the tree.
222 function git_ls_tree_part($project, $tree, $name)
224 $entries = git_ls_tree($project, $tree);
225 foreach ($entries as $entry) {
226 if ($entry['name'] === $name) {
227 return $entry;
230 return null;
234 * Return a URL that contains the given parameters.
236 function makelink($dict)
238 $params = array();
239 foreach ($dict as $k => $v) {
240 $params[] = rawurlencode($k) .'='. str_replace('%2F', '/', rawurlencode($v));
242 if (count($params) > 0) {
243 return '?'. htmlentities(join('&', $params));
245 return '';
249 * Executes a git command in the project repo.
250 * @return array of output lines
252 function run_git($project, $command)
254 global $conf;
256 $output = array();
257 $cmd = "GIT_DIR=". $conf['projects'][$project]['repo'] ." $command";
258 exec($cmd, &$output);
259 return $output;
263 * Executes a git command in the project repo, sending output directly to the
264 * client.
266 function run_git_passthru($project, $command)
268 global $conf;
270 $cmd = "GIT_DIR=". $conf['projects'][$project]['repo'] ." $command";
271 $result = 0;
272 passthru($cmd, &$result);
273 return $result;
277 * Makes sure the given project is valid. If it's not, this function will
278 * die().
279 * @return the project
281 function validate_project($project)
283 global $conf;
285 if (!in_array($project, array_keys($conf['projects']))) {
286 die('Invalid project');
288 return $project;
292 * Makes sure the given hash is valid. If it's not, this function will die().
293 * @return the hash
295 function validate_hash($hash)
297 if (!preg_match('/^[0-9a-z]{40}$/', $hash) && !preg_match('!^refs/(heads|tags)/[-.0-9a-z]+$!', $hash)) {
298 die('Invalid hash');
301 return $hash;
304 $action = 'index';
305 $template = 'index';
306 $page['title'] = 'ViewGit';
308 if (isset($_REQUEST['a'])) {
309 $action = strtolower($_REQUEST['a']);
311 $page['action'] = $action;
313 if ($action === 'index') {
314 $template = 'index';
315 $page['title'] = 'List of projects - ViewGit';
317 foreach (array_keys($conf['projects']) as $p) {
318 $page['projects'][] = get_project_info($p);
321 elseif ($action === 'archive') {
322 $project = validate_project($_REQUEST['p']);
323 $tree = validate_hash($_REQUEST['h']);
324 $type = $_REQUEST['t'];
326 $basename = "$project-tree-". substr($tree, 0, 7);
327 if (isset($_REQUEST['n'])) {
328 $basename = "$project-$_REQUEST[n]-". substr($tree, 0, 6);
331 if ($type === 'targz') {
332 header("Content-Type: application/x-tar-gz");
333 header("Content-Transfer-Encoding: binary");
334 header("Content-Disposition: attachment; filename=\"$basename.tar.gz\";");
335 run_git_passthru($project, "git archive --format=tar $tree |gzip");
337 elseif ($type === 'zip') {
338 header("Content-Type: application/x-zip");
339 header("Content-Transfer-Encoding: binary");
340 header("Content-Disposition: attachment; filename=\"$basename.zip\";");
341 run_git_passthru($project, "git archive --format=zip $tree");
343 else {
344 die('Invalid archive type requested');
347 die();
349 // blob: send a blob to browser with filename suggestion
350 elseif ($action === 'blob') {
351 $project = validate_project($_REQUEST['p']);
352 $hash = validate_hash($_REQUEST['h']);
353 $name = $_REQUEST['n'];
355 header('Content-type: application/octet-stream');
356 header("Content-Disposition: attachment; filename=$name"); // FIXME needs quotation
358 run_git_passthru($project, "git cat-file blob $hash");
359 die();
362 * git checkout.
364 elseif ($action === 'co') {
365 if (!$conf['allow_checkout']) { die('Checkout not allowed'); }
367 // For debugging
368 debug("Project: $_REQUEST[p] Request: $_REQUEST[r]");
370 // eg. info/refs, HEAD
371 $p = validate_project($_REQUEST['p']); // project
372 $r = $_REQUEST['r']; // path
374 $gitdir = $conf['projects'][$p]['repo'];
375 $filename = $gitdir .'/'. $r;
377 // make sure the request is legit (no reading of other files besides those under git projects)
378 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) {
379 if (file_exists($filename)) {
380 debug('OK, sending');
381 readfile($filename);
382 } else {
383 debug('Not found');
384 header('404');
386 } else {
387 debug("Denied");
390 die();
392 elseif ($action === 'commit') {
393 $template = 'commit';
394 $page['project'] = validate_project($_REQUEST['p']);
395 $page['title'] = "$page[project] - Commit - ViewGit";
396 $page['commit_id'] = validate_hash($_REQUEST['h']);
398 $info = git_get_commit_info($page['project'], $page['commit_id']);
400 $page['author_name'] = $info['author_name'];
401 $page['author_mail'] = $info['author_mail'];
402 $page['author_datetime'] = strftime($conf['datetime'], $info['author_utcstamp']);
403 $page['author_datetime_local'] = strftime($conf['datetime'], $info['author_stamp']) .' '. $info['author_timezone'];
404 $page['committer_name'] = $info['committer_name'];
405 $page['committer_mail'] = $info['committer_mail'];
406 $page['committer_datetime'] = strftime($conf['datetime'], $info['committer_utcstamp']);
407 $page['committer_datetime_local'] = strftime($conf['datetime'], $info['committer_stamp']) .' '. $info['committer_timezone'];
408 $page['tree_id'] = $info['tree'];
409 $page['parents'] = $info['parents'];
410 $page['message'] = $info['message'];
411 $page['message_firstline'] = $info['message_firstline'];
412 $page['message_full'] = $info['message_full'];
415 elseif ($action === 'commitdiff') {
416 $template = 'commitdiff';
417 $page['project'] = validate_project($_REQUEST['p']);
418 $page['title'] = "$page[project] - Commitdiff - ViewGit";
419 $hash = validate_hash($_REQUEST['h']);
420 $page['commit_id'] = $hash;
422 $info = git_get_commit_info($page['project'], $hash);
424 $page['tree_id'] = $info['tree'];
426 $page['message'] = $info['message'];
427 $page['message_firstline'] = $info['message_firstline'];
428 $page['message_full'] = $info['message_full'];
429 $page['author_name'] = $info['author_name'];
430 $page['author_mail'] = $info['author_mail'];
431 $page['author_datetime'] = strftime($conf['datetime'], $info['author_utcstamp']);
433 $text = join("\n", run_git($page['project'], "git diff $hash^..$hash"));
434 list($page['files'], $page['diffdata']) = format_diff($text);
435 //$page['diffdata'] = format_diff($text);
437 elseif ($action === 'shortlog') {
438 $template = 'shortlog';
439 $page['project'] = validate_project($_REQUEST['p']);
440 $page['title'] = "$page[project] - Shortlog - ViewGit";
441 if (isset($_REQUEST['h'])) {
442 $page['ref'] = validate_hash($_REQUEST['h']);
443 } else {
444 $page['ref'] = 'HEAD';
447 $info = git_get_commit_info($page['project'], $page['ref']);
448 $page['commit_id'] = $info['h'];
449 $page['tree_id'] = $info['tree'];
451 // TODO merge the logic with 'summary' below
452 $revs = git_get_rev_list($page['project'], $conf['summary_shortlog'], $page['ref']); // TODO pass first rev as parameter
453 foreach ($revs as $rev) {
454 $info = git_get_commit_info($page['project'], $rev);
455 $page['shortlog'][] = array(
456 'author' => $info['author_name'],
457 'date' => strftime($conf['datetime'], $info['author_utcstamp']),
458 'message' => $info['message'],
459 'commit_id' => $rev,
460 'tree' => $info['tree'],
464 elseif ($action === 'summary') {
465 $template = 'summary';
466 $page['project'] = validate_project($_REQUEST['p']);
467 $page['title'] = "$page[project] - Summary - ViewGit";
469 $info = git_get_commit_info($page['project']);
470 $page['commit_id'] = $info['h'];
471 $page['tree_id'] = $info['tree'];
473 $revs = git_get_rev_list($page['project'], $conf['summary_shortlog']);
474 foreach ($revs as $rev) {
475 $info = git_get_commit_info($page['project'], $rev);
476 $page['shortlog'][] = array(
477 'author' => $info['author_name'],
478 'date' => strftime($conf['datetime'], $info['author_utcstamp']),
479 'message' => $info['message'],
480 'commit_id' => $rev,
481 'tree' => $info['tree'],
485 $tags = git_get_tags($page['project']);
486 $page['tags'] = array();
487 foreach ($tags as $tag) {
488 $info = git_get_commit_info($page['project'], $tag['h']);
489 $page['tags'][] = array(
490 'stamp' => $info['author_utcstamp'],
491 'date' => strftime($conf['datetime'], $info['author_utcstamp']),
492 'h' => $tag['h'],
493 'fullname' => $tag['fullname'],
494 'name' => $tag['name'],
497 // sort tags newest first
498 // aka. two more reasons to hate PHP (figuring those out is your homework:)
499 $arr = $page['tags'];
500 usort($arr, create_function(
501 '$x, $y',
502 '$a = $x["stamp"]; $b = $y["stamp"]; return ($a == $b ? 0 : ($a > $b ? -1 : 1));'
504 // it's actually three, I lied.
505 $page['tags'] = $arr;
507 $heads = git_get_heads($page['project']);
508 $page['heads'] = array();
509 foreach ($heads as $h) {
510 $info = git_get_commit_info($page['project'], $h['h']);
511 $page['heads'][] = array(
512 'date' => strftime($conf['datetime'], $info['author_utcstamp']),
513 'h' => $h['h'],
514 'fullname' => $h['fullname'],
515 'name' => $h['name'],
520 * Shows a tree, with list of directories/files, links to them and download
521 * links to archives.
523 * @param p project
524 * @param h tree hash
525 * @param hb OPTIONAL base commit (trees can be part of multiple commits, this
526 * one denotes which commit the user navigated from)
527 * @param f OPTIONAL path the user has followed to view this tree
529 elseif ($action === 'tree') {
530 $template = 'tree';
531 $page['project'] = validate_project($_REQUEST['p']);
532 $page['tree_id'] = validate_hash($_REQUEST['h']);
533 $page['title'] = "$page[project] - Tree - ViewGit";
535 // 'hb' optionally contains the commit_id this tree is related to
536 if (isset($_REQUEST['hb'])) {
537 $page['commit_id'] = validate_hash($_REQUEST['hb']);
539 else {
540 // for the header
541 $info = git_get_commit_info($page['project']);
542 $page['commit_id'] = $info['h'];
545 $page['path'] = '';
546 if (isset($_REQUEST['f'])) {
547 $page['path'] = $_REQUEST['f']; // TODO validate?
550 // get path info for the header
551 $page['pathinfo'] = git_get_path_info($page['project'], $page['commit_id'], explode('/', $page['path']));
553 $page['entries'] = git_ls_tree($page['project'], $page['tree_id']);
556 * View a blob as inline, embedded on the page.
557 * @param p project
558 * @param h blob hash
559 * @param hb OPTIONAL base commit
561 elseif ($action === 'viewblob') {
562 $template = 'blob';
563 $page['project'] = validate_project($_REQUEST['p']);
564 $page['hash'] = validate_hash($_REQUEST['h']);
565 $page['title'] = "$page[project] - Blob - ViewGit";
566 if (isset($_REQUEST['hb'])) {
567 $page['commit_id'] = validate_hash($_REQUEST['hb']);
569 else {
570 $page['commit_id'] = 'HEAD';
573 $page['path'] = '';
574 if (isset($_REQUEST['f'])) {
575 $page['path'] = $_REQUEST['f']; // TODO validate?
578 // For the header's pagenav
579 $info = git_get_commit_info($page['project'], $page['commit_id']);
580 $page['commit_id'] = $info['h'];
581 $page['tree_id'] = $info['tree'];
583 $page['pathinfo'] = git_get_path_info($page['project'], $page['commit_id'], explode('/', $page['path']));
585 $page['data'] = join("\n", run_git($page['project'], "git cat-file blob $page[hash]"));
587 else {
588 die('Invalid action');
591 require 'templates/header.php';
592 require "templates/$template.php";
593 require 'templates/footer.php';