Trigger error for invalid path parts.
[viewgit.git] / inc / functions.php
blobae81599a31fbf37a94317584fd1c389bbd917865
1 <?php
2 /** @file
3 * Functions used by ViewGit.
4 */
6 function debug($msg)
8 global $conf;
10 if ($conf['debug']) {
11 file_put_contents('php://stderr', strftime('%H:%M:%S') ." viewgit: $_SERVER[REMOTE_ADDR]:$_SERVER[REMOTE_PORT] $msg\n", FILE_APPEND);
15 function fix_encoding($in_str)
17 if (function_exists("mb_detect_encoding") && function_exists("mb_check_encoding")) {
18 $cur_encoding = mb_detect_encoding($in_str) ;
19 if($cur_encoding == "UTF-8" && mb_check_encoding($in_str,"UTF-8")) {
20 return $in_str;
21 } else {
22 return utf8_encode($in_str);
24 } else {
25 return utf8_encode($in_str);
29 /**
30 * Format author's name & wrap it to links etc.
32 function format_author($author)
34 global $page;
36 if (isset($page['project'])) {
37 // FIXME 'h' - use only if available
38 return '<a href="'. makelink(array('a' => 'search', 'p' => $page['project'], 'h' => 'HEAD', 'st' => 'author', 's' => $author)) .'">'. htmlentities_wrapper($author) .'</a>';
39 } else {
40 return htmlentities_wrapper($author);
44 /**
45 * Formats "git diff" output into xhtml.
46 * @return array(array of filenames, xhtml)
48 function format_diff($text)
50 $files = array();
52 // match every "^diff --git a/<path> b/<path>$" line
53 foreach (explode("\n", $text) as $line) {
54 if (preg_match('#^diff --git a/(.*) b/(.*)$#', $line, $matches) > 0) {
55 $files[$matches[1]] = urlencode($matches[1]);
59 $text = htmlentities_wrapper($text);
61 $text = preg_replace(
62 array(
63 '/^(\+.*)$/m',
64 '/^(-.*)$/m',
65 '/^(@.*)$/m',
66 '/^([^d\+-@].*)$/m',
68 array(
69 '<span class="add">$1</span>',
70 '<span class="del">$1</span>',
71 '<span class="pos">$1</span>',
72 '<span class="etc">$1</span>',
74 $text);
75 $text = preg_replace_callback('#^diff --git a/(.*) b/(.*)$#m',
76 create_function(
77 '$m',
78 'return "<span class=\"diffline\"><a name=\"". urlencode($m[1]) ."\">diff --git a/$m[1] b/$m[2]</a></span>";'
80 $text);
82 return array($files, $text);
85 /**
86 * Get project information from config and git, name/description and HEAD
87 * commit info are returned in an array.
89 function get_project_info($name)
91 global $conf;
93 $info = $conf['projects'][$name];
94 $info['name'] = $name;
96 // If description is not set, read it from the repository's description
97 if (!isset($info['description'])) {
98 $info['description'] = file_get_contents($info['repo'] .'/description');
101 $headinfo = git_get_commit_info($name, 'HEAD');
102 $info['head_stamp'] = $headinfo['author_utcstamp'];
103 $info['head_datetime'] = strftime($conf['datetime'], $headinfo['author_utcstamp']);
104 $info['head_hash'] = $headinfo['h'];
105 $info['head_tree'] = $headinfo['tree'];
106 $info['message'] = $headinfo['message'];
108 return $info;
111 function git_describe($project, $commit)
113 $output = run_git($project, "describe --always ". escapeshellarg($commit));
114 return $output[0];
118 * Get diff between given revisions as text.
120 function git_diff($project, $from, $to)
122 return join("\n", run_git($project, "diff \"$from..$to\""));
125 function git_diffstat($project, $commit, $commit_base = null)
127 if (is_null($commit_base)) {
128 $commit_base = "$commit^";
130 return join("\n", run_git($project, "diff --stat $commit_base..$commit"));
134 * Get array of changed paths for a commit.
136 function git_get_changed_paths($project, $hash = 'HEAD')
138 $result = array();
139 $affected_files = run_git($project, "show --pretty=\"format:\" --name-only $hash");
140 foreach ($affected_files as $file ) {
141 // The format above contains a blank line; Skip it.
142 if ($file == '') {
143 continue;
146 $output = run_git($project, "ls-tree $hash $file");
147 foreach ($output as $line) {
148 $parts = preg_split('/\s+/', $line, 4);
149 $result[] = array('name' => $parts[3], 'hash' => $parts[2]);
152 return $result;
156 * Get details of a commit: tree, parents, author/committer (name, mail, date), message
158 function git_get_commit_info($project, $hash = 'HEAD', $path = null)
160 global $conf;
162 $info = array();
163 $info['h_name'] = $hash;
164 $info['message_full'] = '';
165 $info['parents'] = array();
167 $extra = '';
168 if (isset($path)) {
169 $extra = '-- '. escapeshellarg($path);
172 $output = run_git($project, "rev-list --header --max-count=1 $hash $extra");
173 // tree <h>
174 // parent <h>
175 // author <name> "<"<mail>">" <stamp> <timezone>
176 // committer
177 // <empty>
178 // <message>
179 $pattern = '/^(author|committer) ([^<]+) <([^>]*)> ([0-9]+) (.*)$/';
180 foreach ($output as $line) {
181 if (substr($line, 0, 4) === 'tree') {
182 $info['tree'] = substr($line, 5);
184 // may be repeated multiple times for merge/octopus
185 elseif (substr($line, 0, 6) === 'parent') {
186 $info['parents'][] = substr($line, 7);
188 elseif (preg_match($pattern, $line, $matches) > 0) {
189 $info[$matches[1] .'_name'] = $matches[2];
190 $info[$matches[1] .'_mail'] = $matches[3];
191 $info[$matches[1] .'_stamp'] = $matches[4] + ((intval($matches[5]) / 100.0) * 3600);
192 $info[$matches[1] .'_timezone'] = $matches[5];
193 $info[$matches[1] .'_utcstamp'] = $matches[4];
195 if (isset($conf['mail_filter'])) {
196 $info[$matches[1] .'_mail'] = $conf['mail_filter']($info[$matches[1] .'_mail']);
199 // Lines starting with four spaces and empty lines after first such line are part of commit message
200 elseif (substr($line, 0, 4) === ' ' || (strlen($line) == 0 && isset($info['message']))) {
201 $info['message_full'] .= substr($line, 4) ."\n";
202 if (!isset($info['message'])) {
203 $info['message'] = substr($line, 4, $conf['commit_message_maxlen']);
204 $info['message_firstline'] = substr($line, 4);
207 elseif (preg_match('/^[0-9a-f]{40}$/', $line) > 0) {
208 $info['h'] = $line;
212 // This is a workaround for the unlikely situation where a commit does
213 // not have a message. Such a commit can be created with the following
214 // command:
215 // git commit --allow-empty -m '' --cleanup=verbatim
216 if (!array_key_exists('message', $info)) {
217 $info['message'] = '(no message)';
218 $info['message_firstline'] = '(no message)';
221 $info['author_datetime'] = strftime($conf['datetime_full'], $info['author_utcstamp']);
222 $info['author_datetime_local'] = strftime($conf['datetime_full'], $info['author_stamp']) .' '. $info['author_timezone'];
223 $info['committer_datetime'] = strftime($conf['datetime_full'], $info['committer_utcstamp']);
224 $info['committer_datetime_local'] = strftime($conf['datetime_full'], $info['committer_stamp']) .' '. $info['committer_timezone'];
226 return $info;
230 * Get list of heads (branches) for a project.
232 function git_get_heads($project)
234 $heads = array();
236 $output = run_git($project, 'show-ref --heads');
237 foreach ($output as $line) {
238 $fullname = substr($line, 41);
239 $tmp = explode('/', $fullname);
240 $name = array_pop($tmp);
241 $pre = array_pop($tmp);
242 if ($pre != 'heads')
244 $name = $pre . '/' . $name;
246 $heads[] = array('h' => substr($line, 0, 40), 'fullname' => "$fullname", 'name' => "$name");
249 return $heads;
253 * Get array containing path information for parts, starting from root_hash.
255 * @param root_hash commit/tree hash for the root tree
256 * @param path path
258 function git_get_path_info($project, $root_hash, $path)
260 if (strlen($path) > 0) {
261 $parts = explode('/', $path);
262 } else {
263 $parts = array();
266 $pathinfo = array();
268 $tid = $root_hash;
269 $pathinfo = array();
270 foreach ($parts as $p) {
271 $entry = git_ls_tree_part($project, $tid, $p);
272 if (is_null($entry)) {
273 trigger_error("Invalid path info: $path");
274 break;
276 $pathinfo[] = $entry;
277 $tid = $entry['hash'];
280 return $pathinfo;
284 * Get revision list starting from given commit.
285 * @param skip how many hashes to skip from the beginning
286 * @param max_count number of commit hashes to return, or all if not given
287 * @param start revision to start from, or HEAD if not given
289 function git_get_rev_list($project, $skip = 0, $max_count = null, $start = 'HEAD')
291 $cmd = "rev-list ";
292 if ($skip != 0) {
293 $cmd .= "--skip=$skip ";
295 if (!is_null($max_count)) {
296 $cmd .= "--max-count=$max_count ";
298 $cmd .= $start;
300 return run_git($project, $cmd);
304 * Get list of tags for a project.
306 function git_get_tags($project)
308 $tags = array();
310 $output = run_git($project, 'show-ref --tags');
311 foreach ($output as $line) {
312 $fullname = substr($line, 41);
313 $tmp = explode('/', $fullname);
314 $name = array_pop($tmp);
315 $tags[] = array('h' => substr($line, 0, 40), 'fullname' => $fullname, 'name' => $name);
317 return $tags;
321 * Get information about objects in a tree.
322 * @param tree tree or commit hash
323 * @return list of arrays containing name, mode, type, hash
325 function git_ls_tree($project, $tree)
327 $entries = array();
328 $output = run_git($project, "ls-tree $tree");
329 // 100644 blob 493b7fc4296d64af45dac64bceac2d9a96c958c1 .gitignore
330 // 040000 tree 715c78b1011dc58106da2a1af2fe0aa4c829542f doc
331 foreach ($output as $line) {
332 $parts = preg_split('/\s+/', $line, 4);
333 $entries[] = array('name' => $parts[3], 'mode' => $parts[0], 'type' => $parts[1], 'hash' => $parts[2]);
336 return $entries;
340 * Get information about the given object in a tree, or null if not in the tree.
342 function git_ls_tree_part($project, $tree, $name)
344 $entries = git_ls_tree($project, $tree);
345 foreach ($entries as $entry) {
346 if ($entry['name'] === $name) {
347 return $entry;
350 return null;
354 * Get the ref list as dict: hash -> list of names.
355 * @param tags whether to show tags
356 * @param heads whether to show heads
357 * @param remotes whether to show remote heads, currently implies tags and heads too.
359 function git_ref_list($project, $tags = true, $heads = true, $remotes = true)
361 $cmd = "show-ref --dereference";
362 if (!$remotes) {
363 if ($tags) { $cmd .= " --tags"; }
364 if ($heads) { $cmd .= " --heads"; }
367 $result = array();
368 $output = run_git($project, $cmd);
369 foreach ($output as $line) {
370 // <hash> <ref>
371 $parts = explode(' ', $line, 2);
372 $name = str_replace(array('refs/', '^{}'), array('', ''), $parts[1]);
373 $result[$parts[0]][] = $name;
375 return $result;
379 * Find commits based on search type and string.
381 function git_search_commits($project, $branch, $type, $string)
383 // git log -sFOO
384 if ($type == 'change') {
385 $cmd = 'log -S'. escapeshellarg($string);
387 elseif ($type == 'commit') {
388 $cmd = 'log -i --grep='. escapeshellarg($string);
390 elseif ($type == 'author') {
391 $cmd = 'log -i --author='. escapeshellarg($string);
393 elseif ($type == 'committer') {
394 $cmd = 'log -i --committer='. escapeshellarg($string);
396 else {
397 die('Unsupported type');
399 $cmd .= ' '. $branch;
400 $lines = run_git($project, $cmd);
402 $result = array();
403 foreach ($lines as $line) {
404 if (preg_match('/^commit (.*?)$/', $line, $matches)) {
405 $result[] = $matches[1];
408 return $result;
412 * Get shortlog entries for the given project.
414 function handle_shortlog($project, $hash = 'HEAD', $page = 0)
416 global $conf;
418 $refs_by_hash = git_ref_list($project, true, true, $conf['shortlog_remote_labels']);
420 $result = array();
421 $revs = git_get_rev_list($project, $page * $conf['summary_shortlog'], $conf['summary_shortlog'], $hash);
422 foreach ($revs as $rev) {
423 $info = git_get_commit_info($project, $rev);
424 $refs = array();
425 if (in_array($rev, array_keys($refs_by_hash))) {
426 $refs = $refs_by_hash[$rev];
428 $result[] = array(
429 'author' => $info['author_name'],
430 'date' => strftime($conf['datetime'], $info['author_utcstamp']),
431 'message' => $info['message'],
432 'commit_id' => $rev,
433 'tree' => $info['tree'],
434 'refs' => $refs,
437 #print_r($result);
438 #die();
440 return $result;
444 * Fetch tags data, newest first.
446 * @param limit maximum number of tags to return
448 function handle_tags($project, $limit = 0)
450 global $conf;
452 $tags = git_get_tags($project);
453 $result = array();
454 foreach ($tags as $tag) {
455 $info = git_get_commit_info($project, $tag['h']);
456 $result[] = array(
457 'stamp' => $info['author_utcstamp'],
458 'date' => strftime($conf['datetime'], $info['author_utcstamp']),
459 'h' => $tag['h'],
460 'fullname' => $tag['fullname'],
461 'name' => $tag['name'],
465 // sort tags newest first
466 // aka. two more reasons to hate PHP (figuring those out is your homework:)
467 usort($result, create_function(
468 '$x, $y',
469 '$a = $x["stamp"]; $b = $y["stamp"]; return ($a == $b ? 0 : ($a > $b ? -1 : 1));'
472 // TODO optimize this some way, currently all tags are fetched when only a
473 // few are shown. The problem is that without fetching the commit info
474 // above, we can't sort using dates, only by tag name...
475 if ($limit > 0) {
476 $result = array_splice($result, 0, $limit);
479 return $result;
482 function htmlentities_wrapper($text)
484 return htmlentities(@iconv('UTF-8', 'UTF-8//IGNORE', $text), ENT_NOQUOTES, 'UTF-8');
487 function xmlentities_wrapper($text)
489 return str_replace(array('&', '<'), array('&#x26;', '&#x3C;'), @iconv('UTF-8', 'UTF-8//IGNORE', $text));
493 * Return a URL that contains the given parameters.
495 function makelink($dict)
497 $params = array();
498 foreach ($dict as $k => $v) {
499 $params[] = rawurlencode($k) .'='. str_replace('%2F', '/', rawurlencode($v));
501 if (count($params) > 0) {
502 return '?'. htmlentities_wrapper(join('&', $params));
504 return '';
508 * Obfuscate the e-mail address.
510 function obfuscate_mail($mail)
512 return str_replace(array('@', '.'), array(' at ', ' dot '), $mail);
516 * Used to format RSS item title and description.
518 * @param info commit info from git_get_commit_info()
520 function rss_item_format($format, $info)
522 return preg_replace(array(
523 '/{AUTHOR}/',
524 '/{AUTHOR_MAIL}/',
525 '/{SHORTLOG}/',
526 '/{LOG}/',
527 '/{COMMITTER}/',
528 '/{COMMITTER_MAIL}/',
529 '/{DIFFSTAT}/',
530 ), array(
531 htmlentities_wrapper($info['author_name']),
532 htmlentities_wrapper($info['author_mail']),
533 htmlentities_wrapper($info['message_firstline']),
534 htmlentities_wrapper($info['message_full']),
535 htmlentities_wrapper($info['committer_name']),
536 htmlentities_wrapper($info['committer_mail']),
537 htmlentities_wrapper(isset($info['diffstat']) ? $info['diffstat'] : ''),
538 ), $format);
541 function rss_pubdate($secs)
543 return gmdate('D, d M Y H:i:s O', $secs);
547 * Executes a git command in the project repo.
548 * @return array of output lines
550 function run_git($project, $command)
552 global $conf;
554 $output = array();
555 $cmd = $conf['git'] ." --git-dir=". escapeshellarg($conf['projects'][$project]['repo']) ." $command";
556 $ret = 0;
557 exec($cmd, $output, $ret);
558 if ($conf['debug_command_trace']) {
559 static $count = 0;
560 $count++;
561 trigger_error("[$count]\$ $cmd [exit $ret]");
563 //if ($ret != 0) { die('FATAL: exec() for git failed, is the path properly configured?'); }
564 return $output;
568 * Executes a git command in the project repo, sending output directly to the
569 * client.
571 function run_git_passthru($project, $command)
573 global $conf;
575 $cmd = $conf['git'] ." --git-dir=". escapeshellarg($conf['projects'][$project]['repo']) ." $command";
576 $result = 0;
577 passthru($cmd, $result);
578 return $result;
581 function tpl_extlink($link)
583 echo "<a href=\"$link\" class=\"external\">&#8599;</a>";
587 * Makes sure the given project is valid. If it's not, this function will
588 * die().
589 * @return the project
591 function validate_project($project)
593 global $conf;
595 if (!in_array($project, array_keys($conf['projects']))) {
596 die('Invalid project');
598 return $project;
602 * Makes sure the given hash is valid. If it's not, this function will die().
603 * @return the hash
605 function validate_hash($hash)
607 if (!preg_match('/^[0-9a-z]{40}$/', $hash) && !preg_match('!^refs/(heads|tags)/[-_.0-9a-zA-Z/]+$!', $hash) && $hash !== 'HEAD') {
608 die('Invalid hash');
611 return $hash;
615 * Custom error handler for ViewGit. The errors are pushed to $page['notices']
616 * and displayed by templates/header.php.
618 function vg_error_handler($errno, $errstr, $errfile, $errline)
620 global $page;
622 $mask = ini_get('error_reporting');
624 $class = 'error';
626 // If mask for this error is not enabled, return silently
627 if (!($errno & $mask)) {
628 return true;
631 // Remove any preceding path until viewgit's directory
632 $file = $errfile;
633 $file = strstr($file, 'viewgit/');
635 $message = "$file:$errline $errstr [$errno]";
637 switch ($errno) {
638 case E_ERROR:
639 $class = 'error';
640 break;
641 case E_WARNING:
642 $class = 'warning';
643 break;
644 case E_NOTICE:
645 case E_STRICT:
646 default:
647 $class = 'info';
648 break;
651 $page['notices'][] = array(
652 'message' => $message,
653 'class' => $class,
656 return true;