RSS: use gmdate() instead of date().
[viewgit.git] / inc / functions.php
blob81033e485619839fa999e81597ced541ebc5f6bb
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', gmstrftime('%H:%M:%S') ." viewgit: $_SERVER[REMOTE_ADDR]:$_SERVER[REMOTE_PORT] $msg\n", FILE_APPEND);
15 /**
16 * Formats "git diff" output into xhtml.
17 * @return array(array of filenames, xhtml)
19 function format_diff($text)
21 $files = array();
23 // match every "^diff --git a/<path> b/<path>$" line
24 foreach (explode("\n", $text) as $line) {
25 if (preg_match('#^diff --git a/(.*) b/(.*)$#', $line, $matches) > 0) {
26 $files[$matches[1]] = urlencode($matches[1]);
30 $text = htmlentities_wrapper($text);
32 $text = preg_replace(
33 array(
34 '/^(\+.*)$/m',
35 '/^(-.*)$/m',
36 '/^(@.*)$/m',
37 '/^([^d\+-@].*)$/m',
39 array(
40 '<span class="add">$1</span>',
41 '<span class="del">$1</span>',
42 '<span class="pos">$1</span>',
43 '<span class="etc">$1</span>',
45 $text);
46 $text = preg_replace_callback('#^diff --git a/(.*) b/(.*)$#m',
47 create_function(
48 '$m',
49 'return "<span class=\"diffline\"><a name=\"". urlencode($m[1]) ."\">diff --git a/$m[1] b/$m[2]</a></span>";'
51 $text);
53 return array($files, $text);
56 /**
57 * Get project information from config and git, name/description and HEAD
58 * commit info are returned in an array.
60 function get_project_info($name)
62 global $conf;
64 $info = $conf['projects'][$name];
65 $info['name'] = $name;
66 $info['description'] = file_get_contents($info['repo'] .'/description');
68 $headinfo = git_get_commit_info($name, 'HEAD');
69 $info['head_stamp'] = $headinfo['author_utcstamp'];
70 $info['head_datetime'] = gmstrftime($conf['datetime'], $headinfo['author_utcstamp']);
71 $info['head_hash'] = $headinfo['h'];
72 $info['head_tree'] = $headinfo['tree'];
74 return $info;
77 /**
78 * Get diff between given revisions as text.
80 function git_diff($project, $from, $to)
82 return join("\n", run_git($project, "diff $from..$to"));
85 function git_diffstat($project, $commit, $commit_base = null)
87 if (is_null($commit_base)) {
88 $commit_base = "$commit^";
90 return join("\n", run_git($project, "diff --stat $commit_base..$commit"));
93 /**
94 * Get details of a commit: tree, parents, author/committer (name, mail, date), message
96 function git_get_commit_info($project, $hash = 'HEAD')
98 global $conf;
100 $info = array();
101 $info['h_name'] = $hash;
102 $info['message_full'] = '';
103 $info['parents'] = array();
105 $output = run_git($project, "rev-list --header --max-count=1 $hash");
106 // tree <h>
107 // parent <h>
108 // author <name> "<"<mail>">" <stamp> <timezone>
109 // committer
110 // <empty>
111 // <message>
112 $pattern = '/^(author|committer) ([^<]+) <([^>]*)> ([0-9]+) (.*)$/';
113 foreach ($output as $line) {
114 if (substr($line, 0, 4) === 'tree') {
115 $info['tree'] = substr($line, 5);
117 // may be repeated multiple times for merge/octopus
118 elseif (substr($line, 0, 6) === 'parent') {
119 $info['parents'][] = substr($line, 7);
121 elseif (preg_match($pattern, $line, $matches) > 0) {
122 $info[$matches[1] .'_name'] = $matches[2];
123 $info[$matches[1] .'_mail'] = $matches[3];
124 $info[$matches[1] .'_stamp'] = $matches[4] + ((intval($matches[5]) / 100.0) * 3600);
125 $info[$matches[1] .'_timezone'] = $matches[5];
126 $info[$matches[1] .'_utcstamp'] = $matches[4];
128 if (isset($conf['mail_filter'])) {
129 $info[$matches[1] .'_mail'] = $conf['mail_filter']($info[$matches[1] .'_mail']);
132 // Lines starting with four spaces and empty lines after first such line are part of commit message
133 elseif (substr($line, 0, 4) === ' ' || (strlen($line) == 0 && isset($info['message']))) {
134 $info['message_full'] .= substr($line, 4) ."\n";
135 if (!isset($info['message'])) {
136 $info['message'] = substr($line, 4, $conf['commit_message_maxlen']);
137 $info['message_firstline'] = substr($line, 4);
140 elseif (preg_match('/^[0-9a-f]{40}$/', $line) > 0) {
141 $info['h'] = $line;
145 return $info;
149 * Get list of heads (branches) for a project.
151 function git_get_heads($project)
153 $heads = array();
155 $output = run_git($project, 'show-ref --heads');
156 foreach ($output as $line) {
157 $fullname = substr($line, 41);
158 $tmp = explode('/', $fullname);
159 $name = array_pop($tmp);
160 $heads[] = array('h' => substr($line, 0, 40), 'fullname' => "$fullname", 'name' => "$name");
163 return $heads;
167 * Get array containing path information for parts, starting from root_hash.
169 * @param root_hash commit/tree hash for the root tree
170 * @param path path
172 function git_get_path_info($project, $root_hash, $path)
174 if (strlen($path) > 0) {
175 $parts = explode('/', $path);
176 } else {
177 $parts = array();
180 $pathinfo = array();
182 $tid = $root_hash;
183 $pathinfo = array();
184 foreach ($parts as $p) {
185 $entry = git_ls_tree_part($project, $tid, $p);
186 if (is_null($entry)) {
187 die("Invalid path info: $path");
189 $pathinfo[] = $entry;
190 $tid = $entry['hash'];
193 return $pathinfo;
197 * Get revision list starting from given commit.
198 * @param max_count number of commit hashes to return, or all if not given
199 * @param start revision to start from, or HEAD if not given
201 function git_get_rev_list($project, $max_count = null, $start = 'HEAD')
203 $cmd = "rev-list $start";
204 if (!is_null($max_count)) {
205 $cmd = "rev-list --max-count=$max_count $start";
208 return run_git($project, $cmd);
212 * Get list of tags for a project.
214 function git_get_tags($project)
216 $tags = array();
218 $output = run_git($project, 'show-ref --tags');
219 foreach ($output as $line) {
220 $fullname = substr($line, 41);
221 $tmp = explode('/', $fullname);
222 $name = array_pop($tmp);
223 $tags[] = array('h' => substr($line, 0, 40), 'fullname' => $fullname, 'name' => $name);
225 return $tags;
229 * Get information about objects in a tree.
230 * @param tree tree or commit hash
231 * @return list of arrays containing name, mode, type, hash
233 function git_ls_tree($project, $tree)
235 $entries = array();
236 $output = run_git($project, "ls-tree $tree");
237 // 100644 blob 493b7fc4296d64af45dac64bceac2d9a96c958c1 .gitignore
238 // 040000 tree 715c78b1011dc58106da2a1af2fe0aa4c829542f doc
239 foreach ($output as $line) {
240 $parts = preg_split('/\s+/', $line, 4);
241 $entries[] = array('name' => $parts[3], 'mode' => $parts[0], 'type' => $parts[1], 'hash' => $parts[2]);
244 return $entries;
248 * Get information about the given object in a tree, or null if not in the tree.
250 function git_ls_tree_part($project, $tree, $name)
252 $entries = git_ls_tree($project, $tree);
253 foreach ($entries as $entry) {
254 if ($entry['name'] === $name) {
255 return $entry;
258 return null;
262 * Get the ref list as dict: hash -> list of names.
263 * @param tags whether to show tags
264 * @param heads whether to show heads
265 * @param remotes whether to show remote heads, currently implies tags and heads too.
267 function git_ref_list($project, $tags = true, $heads = true, $remotes = true)
269 $cmd = "show-ref --dereference";
270 if (!$remotes) {
271 if ($tags) { $cmd .= " --tags"; }
272 if ($heads) { $cmd .= " --heads"; }
275 $result = array();
276 $output = run_git($project, $cmd);
277 foreach ($output as $line) {
278 // <hash> <ref>
279 $parts = explode(' ', $line, 2);
280 $name = str_replace(array('refs/', '^{}'), array('', ''), $parts[1]);
281 $result[$parts[0]][] = $name;
283 return $result;
287 * Find commits based on search type and string.
289 function git_search_commits($project, $type, $string)
291 // git log -sFOO
292 if ($type == 'change') {
293 $cmd = 'log -S'. escapeshellarg($string);
295 elseif ($type == 'commit') {
296 $cmd = 'log -i --grep='. escapeshellarg($string);
298 elseif ($type == 'author') {
299 $cmd = 'log -i --author='. escapeshellarg($string);
301 elseif ($type == 'committer') {
302 $cmd = 'log -i --committer='. escapeshellarg($string);
304 else {
305 die('Unsupported type');
307 $lines = run_git($project, $cmd);
309 $result = array();
310 foreach ($lines as $line) {
311 if (preg_match('/^commit (.*?)$/', $line, $matches)) {
312 $result[] = $matches[1];
315 return $result;
319 * Get shortlog entries for the given project.
321 function handle_shortlog($project, $hash = 'HEAD')
323 global $conf;
325 $refs_by_hash = git_ref_list($project, true, true, $conf['shortlog_remote_labels']);
327 $result = array();
328 $revs = git_get_rev_list($project, $conf['summary_shortlog'], $hash);
329 foreach ($revs as $rev) {
330 $info = git_get_commit_info($project, $rev);
331 $refs = array();
332 if (in_array($rev, array_keys($refs_by_hash))) {
333 $refs = $refs_by_hash[$rev];
335 $result[] = array(
336 'author' => $info['author_name'],
337 'date' => gmstrftime($conf['datetime'], $info['author_utcstamp']),
338 'message' => $info['message'],
339 'commit_id' => $rev,
340 'tree' => $info['tree'],
341 'refs' => $refs,
344 #print_r($result);
345 #die();
347 return $result;
351 * Fetch tags data, newest first.
353 * @param limit maximum number of tags to return
355 function handle_tags($project, $limit = 0)
357 global $conf;
359 $tags = git_get_tags($project);
360 $result = array();
361 foreach ($tags as $tag) {
362 $info = git_get_commit_info($project, $tag['h']);
363 $result[] = array(
364 'stamp' => $info['author_utcstamp'],
365 'date' => gmstrftime($conf['datetime'], $info['author_utcstamp']),
366 'h' => $tag['h'],
367 'fullname' => $tag['fullname'],
368 'name' => $tag['name'],
372 // sort tags newest first
373 // aka. two more reasons to hate PHP (figuring those out is your homework:)
374 usort($result, create_function(
375 '$x, $y',
376 '$a = $x["stamp"]; $b = $y["stamp"]; return ($a == $b ? 0 : ($a > $b ? -1 : 1));'
379 // TODO optimize this some way, currently all tags are fetched when only a
380 // few are shown. The problem is that without fetching the commit info
381 // above, we can't sort using dates, only by tag name...
382 if ($limit > 0) {
383 $result = array_splice($result, 0, $limit);
386 return $result;
389 function htmlentities_wrapper($text)
391 return htmlentities(@iconv('UTF-8', 'UTF-8//IGNORE', $text), ENT_NOQUOTES, 'UTF-8');
394 function xmlentities_wrapper($text)
396 return str_replace(array('&', '<'), array('&#x26;', '&#x3C;'), @iconv('UTF-8', 'UTF-8//IGNORE', $text));
400 * Return a URL that contains the given parameters.
402 function makelink($dict)
404 $params = array();
405 foreach ($dict as $k => $v) {
406 $params[] = rawurlencode($k) .'='. str_replace('%2F', '/', rawurlencode($v));
408 if (count($params) > 0) {
409 return '?'. htmlentities_wrapper(join('&', $params));
411 return '';
415 * Obfuscate the e-mail address.
417 function obfuscate_mail($mail)
419 return str_replace(array('@', '.'), array(' at ', ' dot '), $mail);
423 * Used to format RSS item title and description.
425 * @param info commit info from git_get_commit_info()
427 function rss_item_format($format, $info)
429 return preg_replace(array(
430 '/{AUTHOR}/',
431 '/{AUTHOR_MAIL}/',
432 '/{SHORTLOG}/',
433 '/{LOG}/',
434 '/{COMMITTER}/',
435 '/{COMMITTER_MAIL}/',
436 '/{DIFFSTAT}/',
437 ), array(
438 htmlentities_wrapper($info['author_name']),
439 htmlentities_wrapper($info['author_mail']),
440 htmlentities_wrapper($info['message_firstline']),
441 htmlentities_wrapper($info['message_full']),
442 htmlentities_wrapper($info['committer_name']),
443 htmlentities_wrapper($info['committer_mail']),
444 htmlentities_wrapper(isset($info['diffstat']) ? $info['diffstat'] : ''),
445 ), $format);
448 function rss_pubdate($secs)
450 return gmdate('D, d M Y H:i:s O', $secs);
454 * Executes a git command in the project repo.
455 * @return array of output lines
457 function run_git($project, $command)
459 global $conf;
461 $output = array();
462 $cmd = $conf['git'] ." --git-dir=". escapeshellarg($conf['projects'][$project]['repo']) ." $command";
463 $ret = 0;
464 exec($cmd, $output, $ret);
465 //if ($ret != 0) { die('FATAL: exec() for git failed, is the path properly configured?'); }
466 return $output;
470 * Executes a git command in the project repo, sending output directly to the
471 * client.
473 function run_git_passthru($project, $command)
475 global $conf;
477 $cmd = $conf['git'] ." --git-dir=". escapeshellarg($conf['projects'][$project]['repo']) ." $command";
478 $result = 0;
479 passthru($cmd, $result);
480 return $result;
484 * Makes sure the given project is valid. If it's not, this function will
485 * die().
486 * @return the project
488 function validate_project($project)
490 global $conf;
492 if (!in_array($project, array_keys($conf['projects']))) {
493 die('Invalid project');
495 return $project;
499 * Makes sure the given hash is valid. If it's not, this function will die().
500 * @return the hash
502 function validate_hash($hash)
504 if (!preg_match('/^[0-9a-z]{40}$/', $hash) && !preg_match('!^refs/(heads|tags)/[-.0-9a-z]+$!', $hash) && $hash !== 'HEAD') {
505 die('Invalid hash');
508 return $hash;
512 * Custom error handler for ViewGit. The errors are pushed to $page['notices']
513 * and displayed by templates/header.php.
515 function vg_error_handler($errno, $errstr, $errfile, $errline)
517 global $page;
519 $mask = ini_get('error_reporting');
521 $class = 'error';
523 // If mask for this error is not enabled, return silently
524 if (!($errno & $mask)) {
525 return true;
528 // Remove any preceding path until viewgit's directory
529 $file = $errfile;
530 $file = strstr($file, 'viewgit/');
532 $message = "$file:$errline $errstr [$errno]";
534 switch ($errno) {
535 case E_ERROR:
536 $class = 'error';
537 break;
538 case E_WARNING:
539 $class = 'warning';
540 break;
541 case E_NOTICE:
542 case E_STRICT:
543 default:
544 $class = 'info';
545 break;
548 $page['notices'][] = array(
549 'message' => $message,
550 'class' => $class,
553 return true;