<?php require_once($INC . "/util.inc.php"); require_once($INC . "/log.inc.php"); require_once($INC . "/prof.inc.php"); require_once($INC . "/events.inc.php"); $rg_git_zero = "0000000000000000000000000000000000000000"; $rg_git_empty = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; define('GIT_LINK_MASK', intval(base_convert('160000', 8, 10))); $rg_git_error = ""; function rg_git_set_error($str) { global $rg_git_error; $rg_git_error = $str; rg_log($str); } function rg_git_error() { global $rg_git_error; return $rg_git_error; } function rg_git_fatal($msg) { $x = explode("\n", trim($msg)); foreach ($x as $line) { rg_log("FATAL: $line"); echo "RocketGit: Error: $line\n"; } flush(); exit(1); } /* * Returns the short version for a reference */ function rg_git_short($ref) { if (strncmp($ref, 'refs/heads/', 11) == 0) return substr($ref, 11); if (strncmp($ref, '/refs/heads/', 12) == 0) return substr($ref, 12); return $ref; } function rg_git_info($band, $msg) { echo $band; $x = explode("\n", trim($msg)); foreach ($x as $line) { rg_log("INFO: $line"); echo 'RocketGit: Info: ' . $line . "\n"; } } function rg_git_info_pack($band, $msg) { $s = $band; $x = explode("\n", trim($msg)); foreach ($x as $line) { rg_log("INFO: $line"); $s .= 'RocketGit: Info: ' . $line . "\n"; } echo rg_git_pack($s); } /* * Installs rg hooks instead of original ones, by making a link */ function rg_git_install_hooks($dst) { global $php_errormsg; global $rg_scripts; rg_prof_start("git_install_hooks"); rg_log_enter("git_install_hooks: dst=$dst"); $ret = FALSE; while (1) { if (file_exists($dst . "/hooks")) { if (is_link($dst . "/hooks")) { $_dir = readlink($dst . "/hooks"); if ($_dir === FALSE) { rg_git_set_error("cannot read hooks link"); break; } if (strcmp($_dir, $rg_scripts . "/hooks") == 0) { $ret = TRUE; break; } } } rg_log("Removing original hooks dir..."); if (!rg_rmdir($dst . "/hooks")) { rg_git_set_error("cannot remove hooks dir" . " (" . rg_util_error() . ")"); break; } rg_log("Link hooks dir..."); if (symlink($rg_scripts . "/hooks", $dst . "/hooks") === FALSE) { rg_git_set_error("cannot make symlink [$rg_scripts/hooks]" . "->[$dst/] ($php_errormsg)."); break; } $ret = TRUE; break; } rg_log_exit(); rg_prof_end("git_install_hooks"); return $ret; } /* * Init a dir to host a git repository */ function rg_git_init($dst) { global $php_errormsg; rg_prof_start("git_init"); rg_log_enter("git_init: dst=$dst"); $ret = FALSE; while (1) { $dir = dirname($dst); if (!file_exists($dir)) { $r = @mkdir($dir, 0700, TRUE); if ($r === FALSE) { rg_git_set_error("cannot create dir [$dir] ($php_errormsg)"); break; } } // TODO: What to do if the creation fails? if (!is_dir($dst . "/rocketgit")) { $dst2 = $dst . '.tmp'; $cmd = 'git init --bare ' . escapeshellarg($dst2); $a = rg_exec($cmd, '', FALSE, FALSE); if ($a['ok'] != 1) { rg_git_set_error("error on init " . $a['errmsg'] . ")"); break; } if (!@mkdir($dst2 . '/rocketgit')) { rg_git_set_error("cannot create '$dst/rocketgit' dir ($php_errormsg)"); break; } $r = @rename($dst2, $dst); if ($r === FALSE) { rg_git_set_error('cannot rename git dir from .tmp'); break; } } if (rg_git_install_hooks($dst) !== TRUE) break; $ret = TRUE; break; } rg_log_exit(); rg_prof_end("git_init"); return $ret; } function rg_git_clone($src, $dst) { global $php_errormsg; rg_prof_start("git_clone"); rg_log_enter("git_clone: src=$src, dst=$dst"); $ret = FALSE; while (1) { $dir = dirname($dst); if (!file_exists($dir)) { $r = @mkdir($dir, 0700, TRUE); if ($r === FALSE) { rg_git_set_error("cannot create dir [$dir] ($php_errormsg)"); break; } } if (!file_exists($dst . "/rocketgit")) { $cmd = "git clone --bare " . escapeshellarg($src) . " " . escapeshellarg($dst); $a = rg_exec($cmd, '', FALSE, FALSE); if ($a['ok'] != 1) { rg_git_set_error("error on clone (" . $a['errmsg'] . ")"); break; } if (!@mkdir($dst . "/rocketgit", 0700)) { rg_git_set_error("cannot create '$dst/rocketgit' dir ($php_errormsg)"); break; } } if (rg_git_install_hooks($dst) !== TRUE) break; $ret = TRUE; break; } rg_log_exit(); rg_prof_end("git_clone"); return $ret; } /* * Returns type for an object */ function rg_git_type($obj) { global $rg_git_zero; rg_prof_start('git_type'); rg_log_enter('git_type: obj=' . $obj); $ret = FALSE; while (1) { if (strcmp($obj, $rg_git_zero) == 0) { $ret = 'zero'; break; } $cmd = 'git cat-file -t ' . escapeshellarg($obj); $a = rg_exec($cmd, '', FALSE, FALSE); if ($a['ok'] != 1) { rg_git_set_error('error on cat-file (' . $a['errmsg'] . ')'); break; } $ret = trim($a['data']); break; } rg_log_exit(); rg_prof_end('git_type'); return $ret; } /* * Outputs the content (array) of an object */ function rg_git_content($obj) { rg_prof_start('git_content'); rg_log_enter('git_content: obj=' . $obj); $ret = FALSE; while (1) { $cmd = 'git cat-file -p ' . escapeshellarg($obj); $a = rg_exec($cmd, '', FALSE, FALSE); if ($a['ok'] != 1) { rg_git_set_error('error on cat-file (' . $a['errmsg'] . ')'); break; } $ret = $a['data']; break; } rg_log_exit(); rg_prof_end('git_content'); return $ret; } /* * Corrects a revision */ function rg_git_rev($rev) { return preg_replace("/[^a-zA-Z0-9^~]/", "", $rev); } /* * Validates a reference */ function rg_git_reference($refname) { // We do not accept '..' chars if (strstr($refname, '..')) { rg_git_set_error('we do not accept \'..\' inside the ref name'); return FALSE; } if (strstr($refname, '/.')) { rg_git_set_error('we do not accept \'/.\' inside the ref name'); return FALSE; } if (strstr($refname, '\\\\')) { rg_git_set_error('we do not accept \'\\\\\' inside the ref name'); return FALSE; } // We do not accept ending '.lock' chars if (preg_match('/(.lock|\/|\.)$/', $refname) !== 0) { rg_git_set_error('we do not accept a ref name ending in .lock' . ' or slash or dot'); return FALSE; } $pattern = "/^[-a-zA-Z0-9\/_.]*$/uD"; $r = preg_match($pattern, $refname); if ($r === FALSE) { rg_internal_error("preg_match failed!"); return ""; } if ($r !== 1) { $chars = preg_replace($pattern, '', $refname); rg_git_set_error('we do not accept [' . $chars . '] inside a ref name'); return FALSE; } return $refname; } // Check a revision if is OK // TODO: Unit testing function rg_git_rev_ok($rev) { rg_prof_start("git_rev_ok"); rg_log_enter("git_rev_ok: rev=$rev"); $ret = FALSE; while (1) { $cmd = 'git rev-parse --verify ' . escapeshellarg($rev); $a = rg_exec($cmd, '', FALSE, FALSE); if ($a['ok'] != 1) { rg_git_set_error("error on rev-parse (" . $a['errmsg'] . ")"); break; } $ret = trim($a['data']); break; } rg_log_exit(); rg_prof_end("git_rev_ok"); return $ret; } /* * Returns FALSE if bad whitespace detected * TODO: Unit testing: pay attention to return code */ function rg_git_whitespace_ok($old, $new) { global $rg_git_zero; global $rg_git_empty; rg_prof_start("git_whitespace_ok"); rg_log_enter("git_whitespace_ok: old=$old new=$new"); $ret = FALSE; while (1) { if (strcmp($old, $rg_git_zero) == 0) $old = $rg_git_empty; $cmd = "git diff --check" . " " . escapeshellarg($old) . " " . escapeshellarg($new); $a = rg_exec($cmd, '', FALSE, FALSE); rg_log("a:" . rg_array2string($a)); if ($a['ok'] != 1) { rg_git_set_error("error on diff (" . $a['errmsg'] . ")"); $ret = $a['data']; // TODO: should we return FALSE?! } else { $ret = TRUE; } break; } rg_log_exit(); rg_prof_end("git_whitespace_ok"); return $ret; } /* * Loads refs/heads/BRANCH */ function rg_git_load_ref($repo_path, $ref) { global $rg_git_empty; $b = rg_git_reference($ref); if ($b === FALSE) return FALSE; $b = rg_git_short($b); $path = $repo_path . '/refs/heads/' . $b; $r = @file_get_contents($path); if ($r === FALSE) { // probably is a sha1 $r = $ref; } $ret = trim($r); rg_log('DEBUG: git_load_ref[' . $ref . ']=' . $ret); return $ret; } /* * Returns a common ancestor between two commits * TODO: Unit testing */ function rg_git_merge_base($repo_path, $a, $b) { global $rg_git_zero; global $rg_git_empty; rg_prof_start('git_merge_base'); rg_log_enter('git_merge_base' . ' a=' . $a . ' b=' . $b); $ret = FALSE; while (1) { if (empty($repo_path)) $add = ''; else $add = ' --git-dir=' . escapeshellarg($repo_path); if (!empty($repo_path)) { $head = rg_git_load_ref($repo_path, $a); if ($head === FALSE) break; $key = 'git' . '::' . sha1($repo_path) . '::' . 'merge-base' . '::' . escapeshellarg($head) . '::' . escapeshellarg($b); $r = rg_cache_get($key); if ($r !== FALSE) { $ret = $r; break; } } $cmd = 'git' . $add . ' merge-base' . ' ' . escapeshellarg($a) . ' ' . escapeshellarg($b); $a = rg_exec($cmd, '', FALSE, FALSE); if ($a['ok'] != 1) { rg_git_set_error('error on git merge_base (' . $a['errmsg'] . ')'); break; } $ret = trim($a['data']); if (!empty($repo_path)) rg_cache_set($key, $ret, RG_SOCKET_NO_WAIT); break; } rg_log_exit(); rg_prof_end('git_merge_base'); return $ret; } /* * Safely update a reference - used to update main namespace from other ns * If @new is empty, we assume a delete * TODO: Unit testing */ function rg_git_update_ref($repo_path, $ref, $old, $new, $reason) { rg_prof_start("git_update_ref"); rg_log_enter("git_update_ref: ref=$ref old=$old new=$new reason=$reason"); $ret = FALSE; while (1) { $cmd = 'git --git-dir=' . escapeshellarg($repo_path); $cmd .= ' update-ref'; if (!empty($reason)) $cmd .= " -m " . escapeshellarg($reason); if (empty($new)) $cmd .= " -d " . escapeshellarg($ref); else $cmd .= " " . escapeshellarg($ref) . " " . escapeshellarg($new); if (!empty($old)) $cmd .= " " . escapeshellarg($old); $a = rg_exec($cmd, '', FALSE, FALSE); if ($a['ok'] != 1) { rg_git_set_error("error on update-ref (" . $a['errmsg'] . ")"); break; } $ret = TRUE; break; } rg_log_exit(); rg_prof_end("git_update_ref"); return $ret; } /* * git shortlog command */ function rg_git_shortlog($repo_path, $a, $b) { rg_prof_start('git_shortlog'); rg_log_enter('git_shortlog: a=' . $a . ' b=' . $b); $ret = FALSE; while (1) { $cmd = 'git shortlog' . ' --git-dir=' . escapeshellarg($repo_path) . ' ' . escapeshellarg($a) . '..' . escapeshellarg($b); $r = rg_exec($cmd, '', FALSE, FALSE); if ($r['ok'] != 1) { rg_git_set_error('error on shortlog (' . $r['errmsg'] . ')'); break; } $ret = trim($r['data']); break; } rg_log_exit(); rg_prof_end("git_shortlog"); return $ret; } /* * Returns a tree (git ls-tree) */ function rg_git_ls_tree($repo_path, $tree, $path) { rg_prof_start("git_ls_tree"); rg_log_enter("rg_git_ls_tree: repo_path=$repo_path" . " tree=$tree path=$path"); $ret = FALSE; while (1) { $op = " "; if (empty($tree)) { $op = " --full-tree"; $tree = " HEAD"; } $cmd = "git --git-dir=" . escapeshellarg($repo_path) . " ls-tree --long" . $op . escapeshellarg($tree); if (!empty($path)) $cmd .= ' ' . escapeshellarg($path); $a = rg_exec($cmd, '', FALSE, FALSE); if ($a['ok'] != 1) { rg_git_set_error("error on ls-tree (" . $a['errmsg'] . ")"); break; } $a['data'] = trim($a['data']); if (empty($a['data'])) { rg_git_set_error("path does not exists"); break; } $output = explode("\n", trim($a['data'])); $ret = array(); foreach ($output as $line) { //rg_log('DEBUG: processing line [' . $line . ']'); $_y = array(); $_t = explode("\t", $line); $_y['file'] = trim($_t[1]); $_i = preg_replace("/([0-9]*) ([a-z]*) ([a-z0-9]*) ( *)([0-9]*)/", '${1} ${2} ${3} ${5}', $_t[0]); $_t = explode(" ", $_i); $_y['mode'] = $_t[0]; $_y['mode_int'] = intval(base_convert($_t[0], 8, 10)); if (($_y['mode_int'] & GIT_LINK_MASK) == GIT_LINK_MASK) $_y['is_link'] = 1; else $_y['is_link'] = 0; $_y['type'] = $_t[1]; $_y['ref'] = $_t[2]; $_y['size'] = $_t[3]; $ret[] = $_y; } break; } // We are forced to use print_r instead of array2string because // it may be a multilevel array. //rg_log_ml("DEBUG: ls-tree: " . print_r($ret, TRUE)); rg_log_exit(); rg_prof_end("git_ls_tree"); return $ret; } /* * Transforms a diff into an array (ready for rg_git_diff) */ function rg_git_diff2array($diff, &$extra) { rg_prof_start("git_diff2array"); //rg_log_ml("DEBUG: git_diff2array: diff: " . $diff); $ret = array(); $extra['lines_add'] = 0; $extra['lines_del'] = 0; $lines = explode("\n", $diff); //rg_log_ml("DEBUG: lines: " . print_r($lines, TRUE)); $file = -1; foreach ($lines as $line) { //rg_log("DEBUG: line=$line"); // format: diff --git a/a b/a if (strncmp($line, "diff --git ", 11) == 0) { $file++; $ret[$file] = array(); $ret[$file]['flags'] = ""; $ret[$file]['old_mode'] = ""; $ret[$file]['mode'] = ""; $ret[$file]['similarity'] = ""; $ret[$file]['dissimilarity'] = ""; $ret[$file]['lines_add'] = 0; $ret[$file]['lines_del'] = 0; $rest = substr($line, 11); //rg_log("DEBUG: rest=$rest."); $rest = str_replace('" "', ' ', $rest); $rest = trim($rest, '"'); $rest = substr($rest, 2); /* skip 'a/' */ $_t = explode(' b/', $rest); foreach ($_t as &$_file) { if (strncmp($_file, '"', 1) == 0) $_file = substr($_file, 1, -1); $_file = str_replace('\"', '"', $_file); } $ret[$file]['file_from'] = $_t[0]; $ret[$file]['file'] = $_t[1]; $ret[$file]['index'] = ""; $ret[$file]['chunks'] = array(); continue; } if (strncmp($line, "old mode ", 9) == 0) { $ret[$file]['old_mode'] = substr($line, 9); continue; } if (strncmp($line, "new mode ", 9) == 0) { $ret[$file]['mode'] = substr($line, 9); continue; } if (strncmp($line, "deleted file mode ", 18) == 0) { $ret[$file]['flags'] .= "D"; $ret[$file]['old_mode'] = substr($line, 18); continue; } if (strncmp($line, "new file mode ", 14) == 0) { $ret[$file]['flags'] .= "N"; $ret[$file]['mode'] = substr($line, 14); continue; } if (strncmp($line, "copy from ", 10) == 0) { $ret[$file]['flags'] .= "C"; continue; } if (strncmp($line, "copy to ", 8) == 0) continue; if (strncmp($line, "rename from ", 12) == 0) { $ret[$file]['flags'] .= "R"; continue; } if (strncmp($line, "rename to ", 10) == 0) continue; if (strncmp($line, "similarity index ", 17) == 0) { $ret[$file]['similarity'] = substr($line, 17); continue; } if (strncmp($line, "dissimilarity index ", 20) == 0) { $ret[$file]['dissimilarity'] = substr($line, 20); continue; } if (strncmp($line, "Binary files", 12) == 0) { continue; } if (strncmp($line, "index ", 6) == 0) { $rest = substr($line, 6); $_t = explode(' ', $rest); $ret[$file]['index'] = $_t[0]; if (isset($_t[1])) $ret[$file]['mode'] = $_t[1]; continue; } if (strncmp($line, "--- ", 4) == 0) continue; if (strncmp($line, "+++ ", 4) == 0) continue; // parse line "@@ -14,6 +14,8 @@ function..." // @@ from_file_range to_file_range @@ ... if (strncmp($line, "@@", 2) == 0) { //rg_log("DEBUG: chunks: $line"); $_t = explode(" ", $line, 5); if (count($_t) < 4) { rg_internal_error("invalid line [$line]: count < 4"); return FALSE; } $chunk = $_t[1] . " " . $_t[2]; $ret[$file]['chunks'][$chunk] = array(); $ret[$file]['chunks'][$chunk]['section'] = isset($_t[4]) ? trim($_t[4]) : ""; if (strcmp($_t[1], '-1') == 0) { $from = '1'; } else { $from = explode(",", substr($_t[1], 1)); /* split '-14,6'; 1: skip '-' prefix */ $from = intval($from[0]); } $ret[$file]['chunks'][$chunk]['from'] = $from; if (strcmp($_t[2], '+1') == 0) { $to = '1'; } else { $to = explode(",", substr($_t[2], 1)); /* split '+14,8'; 1: skip '+' prefix */ $to = intval($to[0]); } $ret[$file]['chunks'][$chunk]['to'] = $to; continue; } if (empty($line)) { //rg_log("WARN: empty line [$line]!"); continue; } if (strncmp($line, "\0", 1) == 0) { //rg_log("\tWARN: \0 line!"); continue; } if ((strncmp($line, " ", 1) == 0) || (strncmp($line, "+", 1) == 0) || (strncmp($line, "-", 1) == 0)) { $ret[$file]['chunks'][$chunk]['lines'][] = $line; if (strncmp($line, '+', 1) == 0) { $ret[$file]['lines_add']++; $extra['lines_add']++; } else if (strncmp($line, '-', 1) == 0) { $ret[$file]['lines_del']++; $extra['lines_del']++; } continue; } // Ignore, for now, "\ No newline at end of file" (TODO) if (strncmp($line, "\\", 1) == 0) { rg_log("INFO: warn line: [$line]."); continue; } rg_internal_error("I do not know how to parse [" . trim($line) . "]!"); $ret = FALSE; break; } rg_prof_end("git_diff2array"); return $ret; } /* * Show last @max commits, no merges, sort by topo * @also_patch = TRUE if caller needs also the patch * TODO: $also_merges: remove --no-merges */ function rg_git_log($path, $max, $from, $to, $also_patch) { global $rg_git_empty, $rg_git_zero; rg_prof_start("git_log"); rg_log_enter("git_log: path=$path from=$from to=$to max=$max"); $ret = FALSE; while (1) { $test_for_master = TRUE; if (empty($from) && empty($to)) { rg_log('from/to empty'); $from_to = ''; } else if (empty($from)) { rg_log('from empty'); $from_to = $to; } else if (strcmp($from, $rg_git_zero) == 0) { rg_log('from zero'); $from_to = $rg_git_empty . '..' . $to; $test_for_master = FALSE; } else { $from_to = $from . '..' . $to; } if ($test_for_master) { if (!file_exists($path . "/refs/heads/master")) { if (!file_exists($path . "/.git/refs/heads/master")) { rg_log("Repo is empty."); $ret = array(); break; } } } $max_count = ($max == 0) ? "" : " --max-count=$max"; $patches = $also_patch ? " --patch" : " --shortstat"; $cmd = "git --no-pager" . " --git-dir=" . escapeshellarg($path) . " log" . " --find-copies" . " --no-merges" . " -z" . $max_count . $patches . " --pretty=\"format:" . "%x00-=ROCKETGIT=-%x00" . "sha1:%H%x00\"\"" . "sha1_short:%h%x00\"\"" . "tree:%T%x00\"\"" . "tree_short:%t%x00\"\"" . "parents:%P%x00\"\"" . "parents_short:%p%x00\"\"" . "author name:%aN%x00\"\"" . "author email:%aE%x00\"\"" . "author date:%at%x00\"\"" . "committer name:%cN%x00\"\"" . "committer email:%ce%x00\"\"" . "committer date:%ct%x00\"\"" . "encoding:%e%x00\"\"" . "ref_names:%d%x00\"\"" . "sign_key:%GK%x00\"\"" . "subject:%s%x00\"\"" . "body:%b%x00\"\"" . "notes:%N%x00\"\"" . "%x00ROCKETGIT_END_OF_VARS%x00\""; if (!empty($from_to)) $cmd .= ' ' . escapeshellarg($from_to); $a = rg_exec($cmd, '', FALSE, FALSE); if ($a['ok'] != 1) { rg_internal_error("error on log (" . $a['errmsg'] . ")"); rg_git_set_error("could not generate log; try again later"); break; } //rg_log_ml("DEBUG: OUTPUT OF GIT LOG: " . $a['data']); // because data starts with -=ROCK..., we remove it $a['data'] = substr($a['data'], 14); $blocks = explode("\0-=ROCKETGIT=-\0", "\0" . $a['data']); $ret = array(); foreach ($blocks as $junk => $block) { $y = array("vars" => array(), "files" => array()); // some defaults $y['vars']['commit_url'] = ""; // split block in two: vars and patches $parts = explode("\0ROCKETGIT_END_OF_VARS\0", $block, 2); // vars $y['vars']['lines_add'] = 0; $y['vars']['lines_del'] = 0; $x = explode ("\0", trim($parts[0])); $count = count($x); for ($i = 0; $i < $count - 1; $i++) { $_t = explode(":", $x[$i], 2); if (isset($_t[1])) { $y['vars'][$_t[0]] = trim($_t[1]); } else if (empty($_t[0])) { // do nothing } else { //rg_log("DEBUG: Var [" . $_t[0] . "] has no value!"); } } if ($also_patch) { // patches $y['files'] = rg_git_diff2array($parts[1], $_extra); if ($y['files'] === FALSE) break; $y['vars']['lines_add'] = $_extra['lines_add']; $y['vars']['lines_del'] = $_extra['lines_del']; } else { // stortstat //rg_log('DEBUG parts[1]: ' . print_r($parts[1], TRUE)); $t = explode(',', $parts[1]); for ($i = 1; $i < 3; $i++) { if (!isset($t[$i])) break; $x = trim($t[$i]); //rg_log('DEBUG: x=[' . $x . ']'); if (strstr($x, 'insert')) $y['vars']['lines_add'] += intval($x); else if (strstr($x, 'deletion')) $y['vars']['lines_del'] += intval($x); else rg_log('BUG: unknown field: ' . $x); } //rg_log('DEBUG lines_add=' . $y['vars']['lines_add']); //rg_log('DEBUG lines_del=' . $y['vars']['lines_del']); } // final additions $y['vars']['author date UTC'] = gmdate("Y-m-d H:i:s", $y['vars']['author date']); $y['vars']['committer date UTC'] = gmdate("Y-m-d H:i:s", $y['vars']['committer date']); $ret[] = $y; } break; } rg_log_exit(); rg_prof_end("git_log"); return $ret; } /* * Outputs the result of replacing variables in a template with real variables * @log = TODO (output of rg_git_log?) */ function rg_git_log_template($log, $dir, $rg) { $t = array(); if ((is_array($log) && !empty($log))) { foreach ($log as $index => $info) { $v = array(); foreach ($info['vars'] as $var => $value) $v[$var] = $value; $t[] = $v; } } return rg_template_table($dir, $t, $rg); } /* * Build statistics * TODO: Use caching * TODO: count merges * Do not forget that the log is from most recent to the oldest */ function rg_git_stats($log) { $i = array( 'commits' => 0, 'lines_add' => 0, 'lines_del' => 0, 'start_date' => '', 'start_author' => '', 'last_date' => '', 'last_author' => '' ); $ret = array('authors' => array(), 'global' => $i); foreach ($log as $index => $ci) { $v = $ci['vars']; if (empty($ret['global']['last_date'])) { $ret['global']['last_date'] = $v['author date UTC']; $ret['global']['last_author'] = $v['author name']; } $ret['global']['start_date'] = $v['author date UTC']; $ret['global']['start_author'] = $v['author name']; // global stats $ret['global']['lines_add'] += intval($v['lines_add']); $ret['global']['lines_del'] += intval($v['lines_del']); $ret['global']['commits']++; // stats per author $a = $v['author email']; if (!isset($ret['authors'][$a])) { $ret['authors'][$a] = $i; $ret['authors'][$a]['author'] = $v['author name']; } $ret['authors'][$a]['commits']++; $ret['authors'][$a]['lines_add'] += intval($v['lines_add']); $ret['authors'][$a]['lines_del'] += intval($v['lines_del']); if (empty($ret['authors'][$a]['last_date'])) $ret['authors'][$a]['last_date'] = $v['author date UTC']; $ret['authors'][$a]['start_date'] = $v['author date UTC']; } return $ret; } /* * Returns a list with the filenames changed between two revisions * TODO: what if old is empty? */ function rg_git_files($old, $new) { global $rg_git_zero; global $rg_git_empty; rg_prof_start("git_files"); rg_log_enter("rg_git_files old=$old new=$new"); $ret = FALSE; while (1) { if (strcmp($old, $rg_git_zero) == 0) $old = $rg_git_empty; $cmd = 'git diff --name-only ' . escapeshellarg($old) . ' ' . escapeshellarg($new); $a = rg_exec($cmd, '', FALSE, FALSE); if ($a['ok'] != 1) { rg_git_set_error("error on git diff (" . $a['errmsg'] . ")"); break; } $ret = explode("\n", trim($a['data'])); break; } rg_log_exit(); rg_prof_end("git_files"); return $ret; } /* * Nice diff per file * Outputs the result of replacing variables in a template with real variables * @id - uniq id, most of the time the commit sha1; used to differentiate * between same files in different commits. * @a - output of rg_git_diff2array[index]['files'] * TODO: Switch to rg_template_table? */ function rg_git_diff($id, $a, $template_file) { rg_prof_start("git_diff"); //rg_log_enter("DEBUG: git_diff: a: " . rg_array2string($a)); $id = rg_xss_safe($id); $ret = "<div class=\"diff\">\n"; $x = array(); $template = rg_template($template_file, $x, TRUE /* xss */); // for each file changed foreach ($a as $fileindex => $finfo) { //rg_log_ml("DEBUG: finfo: " . print_r($finfo, TRUE)); $ret .= "<br />\n"; $f = rg_xss_safe($finfo['file']); $ret .= '<a name="file-' . $id . '-' . $f . '"></a>' . "\n"; $ret .= "<table class=\"chunk\">\n"; $ret .= "<tr style=\"border: 1px; background: #dddddd\"><td colspan=\"4\">"; if (strstr($finfo['flags'], "N")) $ret .= "File <b>$f</b> added"; else if (strstr($finfo['flags'], "D")) $ret .= "File <b>$f</b> deleted"; else if (strstr($finfo['flags'], "C")) $ret .= "File <b>$f</b> copied from " . rg_xss_safe($finfo['file_from']); else if (strstr($finfo['flags'], "R")) $ret .= "File <b>$f</b> renamed from " . rg_xss_safe($finfo['file_from']); else $ret .= "File <b>$f</b> changed"; if (!empty($finfo['similarity'])) $ret .= " (similarity " . rg_xss_safe($finfo['similarity']) . ")"; if (!empty($finfo['dissimilarity'])) $ret .= " (dissimilarity " . rg_xss_safe($finfo['dissimilarity']) . ")"; if (!empty($finfo['mode'])) { $ret .= " (mode: " . rg_xss_safe($finfo['mode']); if (!empty($finfo['old_mode'])) $ret .= " -> " . rg_xss_safe($finfo['old_mode']); $ret .= ")"; } if (!empty($finfo['index'])) $ret .= " (index " . rg_xss_safe($finfo['index']) . ")"; // TODO: Before stats we must show commit hash, author etc. (source/log/commit/xxxxxx) // TODO: what about committer and time and rest? $ret .= ":"; $ret .= "</td></tr>\n"; $empty_line = ""; foreach ($finfo['chunks'] as $chunk => $ci) { //rg_log_ml("DEBUG: ci: " . print_r($ci, TRUE)); $ret .= $empty_line; $empty_line = "<tr style=\"border: 1px\"><td colspan=\"4\"> </td></tr>\n"; if (!empty($ci['section'])) { $ret .= "<tr>\n"; $ret .= " <td class=\"numbers\">...</td>\n"; $ret .= " <td class=\"numbers\">...</td>\n"; $ret .= " <td style=\"background: #dddddd\" colspan=\"2\"><i>" . $ci['section'] . "</i></td>\n"; $ret .= "</tr>\n"; } $line_no_left = $ci['from']; $line_no_right = $ci['to']; foreach ($ci['lines'] as $line) { $left_class = 'cl-e'; $right_class = 'cl-e'; $c = substr($line, 0, 1); $line = rg_xss_safe(substr($line, 1)); if (strcmp($c, "+") == 0) { $left = ''; $right = $line; $right_class = "cl-g"; $line_left = ''; $line_right = $line_no_right; $line_no_right++; } else if (strcmp($c, "-") == 0) { $left = $line; $left_class = "cl-r"; $right = ''; $line_left = $line_no_left; $line_right = ''; $line_no_left++; } else { // ' ' or any other character $left = $line; $right = $line; $line_left = $line_no_left; $line_right = $line_no_right; $line_no_left++; $line_no_right++; } $a = array( 'line_left' => $line_left, 'line_right' => $line_right, 'left' => $left, 'right' => $right, 'left_class' => $left_class, 'right_class' => $right_class ); $ret .= rg_template_string($template, 0 /*off*/, $a, FALSE /*xss*/); } } $ret .= "</table>\n"; } $ret .= "</div>\n"; rg_prof_end("git_diff"); return $ret; } /* * Show stats for files changed * @a = rg_git_log[0]['files'] */ function rg_git_files_stats($a, $dir) { $t = array(); foreach ($a as $index => $info) { $line = array(); $line['file'] = $info['file']; $line['add'] = $info['lines_add']; $line['del'] = $info['lines_del']; $t[] = $line; } $rg = array(); return rg_template_table($dir, $t, $rg); } /* * Helper for 'update' hook - tags (un-annotated or annotated) */ function rg_git_update_tag($db, $a) { global $rg_git_zero; rg_prof_start("git_update_tag"); rg_log_enter("git_update_tag: " . rg_array2string($a)); $x = array(); $x['obj_id'] = $a['repo_id']; $x['type'] = 'repo_refs'; $x['owner'] = $a['repo_uid']; $x['uid'] = $a['login_uid']; $x['username'] = $a['ui']['username']; $x['needed_rights'] = ''; $x['ip'] = $a['ip']; $x['misc'] = $a['refname']; $history = array('ri' => array(), 'ui' => array()); $history['ri']['repo_id'] = $a['repo_id']; $history['ui']['uid'] = $a['login_uid']; if (strcmp($a['new_rev_type'], "tag") == 0) { // Annotated if (strcmp($a['old_rev'], $rg_git_zero) == 0) { // create $x['needed_rights'] = 'S'; if (rg_rights_allow($db, $x) !== TRUE) rg_git_fatal($a['refname'] . "\nNo rights to" . " create an annotated tag."); $history['history_category'] = REPO_CAT_GIT_ATAG_CREATE; $history['history_message'] = 'Annotated tag ' . $a['refname'] . ' created (' . $a['new_rev'] . ')'; } else if (strcmp($a['new_rev'], $rg_git_zero) == 0) { // delete rg_log("delete ann tag"); $x['needed_rights'] = 'n'; if (rg_rights_allow($db, $x) !== TRUE) rg_git_fatal($a['refname'] . "\nNo rights to" . " delete an annotated tag."); $history['history_category'] = REPO_CAT_GIT_ATAG_DELETE; $history['history_message'] = 'Annotated tag ' . $a['refname'] . ' deleted" . " (' . $a['old_rev'] . ')'; } else { // change rg_log("This seems it cannot happen in recent git."); $x['needed_rights'] = 'S'; if (rg_rights_allow($db, $x) !== TRUE) rg_git_fatal($a['refname'] . "\nNo rights to" . " change an annotated tag."); $history['history_category'] = REPO_CAT_GIT_ATAG_UPDATE; $history['history_message'] = 'Annotated tag ' . $a['refname'] . ' updated from ' . $a['old_rev'] . ' to ' . $a['new_rev']; } } else { // Un-annotated if (strcmp($a['old_rev'], $rg_git_zero) == 0) { // create $x['needed_rights'] = 'Y'; if (rg_rights_allow($db, $x) !== TRUE) rg_git_fatal($a['refname'] . "\nNo rights to" . " create an un-annotated tag."); $history['history_category'] = REPO_CAT_GIT_UTAG_CREATE; $history['history_message'] = 'Un-annotated tag ' . $a['refname'] . ' created' . ' (' . $a['new_rev'] . ')'; } else if (strcmp($a['new_rev'], $rg_git_zero) == 0) { // delete $x['needed_rights'] = 'u'; if (rg_rights_allow($db, $x) !== TRUE) rg_git_fatal($a['refname'] . "\nNo rights to" . " delete an un-annotated tag."); $history['history_category'] = REPO_CAT_GIT_UTAG_DELETE; $history['history_message'] = 'Un-annotated tag ' . $a['refname'] . ' deleted' . ' (' . $a['old_rev'] . ')'; } else { // change $x['needed_rights'] = 'U'; if (rg_rights_allow($db, $x) !== TRUE) rg_git_fatal($a['refname'] . "\nNo rights to" . " change an un-annotated tag."); $history['history_category'] = REPO_CAT_GIT_UTAG_UPDATE; $history['history_message'] = 'Annotated tag ' . $a['refname'] . ' updated from ' . $a['old_rev'] . ' to ' . $a['new_rev']; } } // If we do not have a namespace, we let git to update the ref. // Not clear when we do not have a namespace. if (!empty($a['namespace'])) { // Update the main ref (not a namespace) $reason = $a['ui']['username'] . ' pushed tag ' . $a['refname']; $r = rg_git_update_ref($a['repo_path'], $a['refname'], $a['old_rev'], $a['new_rev'], $reason); if ($r !== TRUE) { rg_git_fatal($a['refname'] . "\nCannot update ref (" . rg_git_error() . ")"); } } rg_repo_history_insert($db, $history); rg_log_exit(); rg_prof_end("git_update_tag"); } /* * Called from hooks */ function rg_git_update_branch($db, $a) { global $rg_git_zero; rg_prof_start("git_update_branch"); rg_log_enter("git_update_branch: " . rg_array2string($a)); while (1) { $_x = array(); $_x['obj_id'] = $a['repo_id']; $_x['type'] = 'repo_refs'; $_x['owner'] = $a['repo_uid']; $_x['uid'] = $a['login_uid']; $_x['username'] = $a['ui']['username']; $_x['needed_rights'] = ''; $_x['ip'] = $a['ip']; $_x['misc'] = $a['refname']; $history = array('ri' => array(), 'ui' => array()); $history['ri']['repo_id'] = $a['repo_id']; $history['ui']['uid'] = $a['login_uid']; if (strcmp($a['new_rev'], $rg_git_zero) == 0) { // delete $x = $_x; $x['needed_rights'] = 'D'; if (rg_rights_allow($db, $x) !== TRUE) rg_git_fatal($a['refname'] . "\nYou have no rights to delete a branch."); $history['history_category'] = REPO_CAT_GIT_BRANCH_DELETE; $history['history_message'] = 'Reference ' . $a['refname'] . ' deleted'; rg_repo_history_insert($db, $history); break; } // If we have 'H' (anonymous push), we have also 'create branch' right $check_fast_forward = 1; if (strcmp($a['old_rev'], $rg_git_zero) == 0) { // create $x = $_x; $x['needed_rights'] = 'H|C'; if (rg_rights_allow($db, $x) !== TRUE) rg_git_fatal($a['refname'] . "\nYou have no rights to create a branch."); $check_fast_forward = 0; } // Create or change // Check for non fast-forward update $x = $_x; $x['needed_rights'] = 'O'; if ((rg_rights_allow($db, $x) !== TRUE) && ($check_fast_forward == 1)) { $merge_base = rg_git_merge_base($a['repo_path'], $a['old_rev'], $a['new_rev']); if ($merge_base === FALSE) { rg_log("Error in merge_base: " . rg_git_error()); rg_git_fatal($a['refname'] . "\nInternal error." . " Please try again later."); } if (strcmp($merge_base, $a['old_rev']) != 0) rg_git_fatal($a['refname'] . "\nYou have no rights to do a non fast-forward push"); } // Check if user pushes a merge commit // TODO: Check all commits, not only the last one! $x = $_x; $x['needed_rights'] = 'M'; if (rg_rights_allow($db, $x) !== TRUE) { if (rg_git_rev_ok($a['new_rev'] . "^2") !== FALSE) rg_git_fatal($a['refname'] . "\nYou have no rights to push merges."); } // Check for bad whitespace $x = $_x; $x['needed_rights'] = 'W'; if (rg_rights_allow($db, $x) !== TRUE) { // TODO: add caching because we may check again below $w = rg_git_whitespace_ok($a['old_rev'], $a['new_rev']); if ($w !== TRUE) rg_git_fatal($a['refname'] . "\nYou have no rights to push bad whitespace:" . "\n" . $w); } rg_log_enter('DEBUG: Checking repo_path rights'); $r = rg_git_files($a['old_rev'], $a['new_rev']); if ($r === FALSE) rg_git_fatal($a['refname'] . "\nInternal error, try again later\n"); $x = $_x; $x['type'] = 'repo_path'; foreach ($r as $file) { $x['needed_rights'] = 'P'; $x['misc'] = $file; if (rg_rights_allow($db, $x) !== TRUE) { rg_git_fatal($a['refname'] . "\nNo rights to push file [$file]\n"); } $x['needed_rights'] = 'W'; if (rg_rights_allow($db, $x) !== TRUE) { $w = rg_git_whitespace_ok($a['old_rev'], $a['new_rev']); if ($w !== TRUE) { rg_git_fatal($a['refname'] . "\nYou have no rights to push bad whitespace on path [$file]:" . "\n" . $w); } } } rg_log_exit(); $x = $_x; $x['type'] = 'repo_refs'; $x['needed_rights'] = 'P'; $x['misc'] = $a['refname']; if (rg_rights_allow($db, $x) !== TRUE) { rg_log("DEBUG: Push is not allowed, let's see the anon one"); $x['needed_rights'] = 'H'; if (rg_rights_allow($db, $x) !== TRUE) { $_z = array(); $msg = rg_template("msg/push_not_allowed.txt", $_z, FALSE /*xss*/); rg_git_fatal($a['refname']. "\n" . $msg); } // anonymous push - create a merge request $ev = $a; $ev['category'] = 'mr_event_new'; $ev['prio'] = 100; $r = rg_event_add($db, $ev); if ($r !== TRUE) rg_git_fatal($a['refname'] . ": " . rg_event_error()); rg_event_signal_daemon('', 0); $_x = array(); $msg = rg_template("msg/push_merge_request.txt", $_x, FALSE /*xss*/); rg_git_info('', $a['refname'] . ':' . "\n" . $msg); $history['history_category'] = REPO_CAT_GIT_BRANCH_ANON_PUSH; $history['history_message'] = 'Anonymous push to ref ' . $a['refname'] . ' into namespace ' . $a['namespace']; } else { rg_log("DEBUG: We are allowed to push."); // If we do not have a namespace, we let git to update the ref. // Not clear when we do not have a namespace. if (!empty($a['namespace'])) { // Updating main ref (not a namespace) $reason = $a['ui']['username'] . ' pushed ref ' . $a['refname']; $r = rg_git_update_ref($a['repo_path'], $a['refname'], $a['old_rev'], $a['new_rev'], $reason); if ($r !== TRUE) { rg_git_fatal($a['refname'] . "\nCannot update ref (" . rg_git_error() . ")"); } $ev = $a; $ev['category'] = 3007; $ev['prio'] = 50; $ev['ri'] = array( 'repo_id' => $a['repo_id'], 'name' => $a['repo_name'], 'url' => rg_base_url() . $a['login_url'] . '/' . $a['repo_name'], 'clone_url' => $a['repo_clone_url_http'] ); unset($ev['repo_id']); unset($ev['repo_name']); $r = rg_event_add($db, $ev); if ($r !== TRUE) rg_git_fatal($a['refname'] . ": " . rg_event_error()); rg_event_signal_daemon('', 0); // TODO: Here, the namespace ref is not yet updated } if (strcmp($a['old_rev'], $rg_git_zero) == 0) { $history['history_category'] = REPO_CAT_GIT_BRANCH_CREATE; $history['history_message'] = 'Reference ' . $a['refname'] . ' created' . ' (' . $a['new_rev'] . ')'; } else { $history['history_category'] = REPO_CAT_GIT_BRANCH_UPDATE; $history['history_message'] = 'Reference ' . $a['refname'] . ' updated from ' . $a['old_rev'] . ' to ' . $a['new_rev']; } } rg_repo_history_insert($db, $history); break; } rg_log_exit(); rg_prof_end("git_update_branch"); } /* * Returns the tags, HEAD and branches */ function rg_git_refs($repo_path) { $ret = array(); $ret['tag'] = rg_dir_load_deep($repo_path . "/refs/tags"); $ret['branch'] = rg_dir_load_deep($repo_path . "/refs/heads"); return $ret; } /* * Returns true if a ref is valid * @refs - the output of rg_git_refs * @type - 'tag' or 'branch' */ function rg_git_ref_valid($refs, $type, $ref) { if (!isset($refs[$type])) return FALSE; foreach ($refs[$type] as $name) { if (strcmp($name, $ref) == 0) return TRUE; } return FALSE; } /* * Returns an array with links to branches and tags * @refs is the output of rg_git_refs function */ function rg_git_branches_and_tags($refs, $base_url, $current_ref) { rg_log_enter("git_branches_and_tags: base_url=$base_url" . " current_ref=$current_ref"); $ret = array(); $ret['HTML:branches_and_tags'] = ''; $current = ltrim($current_ref, '/'); if (empty($current)) $current = 'branch/master'; //rg_log("DEBUG: current=[$current]"); $_l = array(); foreach ($refs as $o => $list) { if (empty($list)) continue; foreach ($list as $name) { $name = rg_xss_safe($name); $ename = str_replace('/', ',', $name); //rg_log("DEBUG: compare with [" . $o . "/" . $ename . "]"); if (strcmp($current, $o . "/" . $ename) == 0) { $add_s = "<b>"; $add_e = "</b>"; } else { $add_s = ""; $add_e = ""; } $_l[] = "<span class=\"" . $o . "\">" . "<a href=\"" . $base_url . "/source/log/$o/$ename" . "\">" . $add_s . $name . $add_e . "</a>" . "</span>\n"; } } if (!empty($_l)) { $ret['HTML:branches_and_tags'] = "<div class=\"branches_and_tags\">\n"; $ret['HTML:branches_and_tags'] .= implode("\n", $_l); $ret['HTML:branches_and_tags'] .= "</div>\n"; } //rg_log("DEBUG: rg_git_branches_and_tags: ret:" . rg_array2string($ret)); rg_log_exit(); return $ret; } /* * Identify branch/tag * @paras: Example: tag|v1.1 or branch|stuff,branch3 */ function rg_git_parse_ref(&$paras) { rg_log("git_parse_ref: " . rg_array2string($paras) . "."); $ret = array("ref_type" => "", "ref_url" => "", "ref_val" => "", "ref_path" => ""); if (count($paras) < 2) return $ret; if (strcmp($paras[0], "tag") == 0) { $ret['ref_type'] = "tag"; $ret['ref_path'] = "refs/tags/"; } else if (strcmp($paras[0], "branch") == 0) { $ret['ref_type'] = "branch"; $ret['ref_path'] = "refs/heads/"; } else { return $ret; } array_shift($paras); $val = trim(array_shift($paras)); $ret['ref_url'] = "/" . $ret['ref_type'] . "/" . $val; $val = str_replace(',', '/', $val); $ret['ref_val'] = $val; $ret['ref_path'] .= $val; return $ret; } /* * Returns a diff between two trees */ function rg_git_diff_tree($tree1, $tree2) { rg_prof_start("git_diff_tree"); rg_log_enter("rg_git_diff_tree: tree1=$tree1 tree2=$tree2"); $ret = FALSE; while (1) { $cmd = "git diff-tree -r " . escapeshellarg($tree1) . " " . escapeshellarg($tree2); $a = rg_exec($cmd, '', FALSE, FALSE); if ($a['ok'] != 1) { rg_git_set_error("error on diff-tree (" . $a['errmsg'] . ")"); break; } $output = explode("\n", trim($a['data'])); $ret = array(); foreach ($output as $line) { $_y = array(); $_t = explode(" ", $line, 5); $_y['mode1'] = $_t[0]; $_y['mode2'] = $_t[1]; $_y['ref1'] = $_t[2]; $_y['ref2'] = $_t[3]; $_op_file = explode("\t", $_t[4], 2); $_y['op'] = $_op_file[0]; $_y['file'] = $_op_file[1]; // TODO: here, the filename is not UTF-8! $ret[] = $_y; } break; } //rg_log("DEBUG: diff-tree: " . rg_array2string($ret)); rg_log_exit(); rg_prof_end("git_diff_tree"); return $ret; } /* * Outputs the content of a file, at a specific revision */ function rg_git_content_by_file($treeish, $file) { rg_prof_start("git_content_by_file"); rg_log_enter("git_content_by_file: treeish=$treeish file=$file"); $ret = FALSE; while (1) { $cmd = 'git show ' . escapeshellarg($treeish) . ':' . escapeshellarg($file); $a = rg_exec($cmd, '', FALSE, FALSE); if ($a['ok'] != 1) { rg_git_set_error("error on show (" . $a['errmsg'] . ")"); break; } $ret = $a['data']; break; } rg_log_exit(); rg_prof_end("git_content"); return $ret; } /* * High level function that shows commits between two points * Input is the array returned by rg_git_log() * @commit_table - TRUE if you want commit table to show (FALSE for log/commit */ function rg_git_log2listing($log, $rg, $commit_table) { if ($log === FALSE) return rg_template('repo/not_init.html', $rg, TRUE/*xss*/); $ret = ''; if ($commit_table) { // Show a short list of commits // Set 'url' foreach ($log as $index => $i) $log[$index]['vars']['commit_url'] = rg_xss_safe($rg['mr']['id']) . "#sha1-" . rg_xss_safe($i['vars']['sha1']); $ret .= rg_git_log_template($log, 'repo/log', $rg); } // TODO: move this into a template! $ret .= '<div style="margin-top: 8pt; margin-left: 8pt">' . "\n"; foreach ($log as $junk => $i) { //rg_log_ml('DEBUG: i=' . print_r($i, TRUE)); // Some info about commit $ret .= "<b>" . "<a name=\"sha1-" . rg_xss_safe($i['vars']['sha1']) . "\">" . "Commit " . rg_xss_safe($i['vars']['sha1']) . "</a></b> - " . rg_xss_safe($i['vars']['subject']) . "\n"; if (!empty($i['vars']['body'])) $ret .= "<br />\n" . nl2br(rg_xss_safe($i['vars']['body'])); $ret .= "<br /><b>Author</b>: " . rg_xss_safe($i['vars']['author name']); $ret .= "<br /><b>Author date (UTC)</b>: " . gmdate("Y-m-d H:i", $i['vars']['author date']); if (!empty($i['vars']['committer name'])) $ret .= "<br /><b>Committer</b>: " . rg_xss_safe($i['vars']['committer name']); $ret .= '<br /><b>Commit date (UTC)</b>: ' . gmdate("Y-m-d H:i", $i['vars']['committer date']); $ret .= '<br /><b>Tree</b>: ' . $i['vars']['tree']; if (!empty($i['vars']['parents'])) $ret .= '<br /><b>Parents</b>: ' . $i['vars']['parents']; if (!empty($i['vars']['sign_key'])) $ret .= '<br /><b>Signing key</b>: ' . $i['vars']['sign_key']; // stats $r = rg_git_files_stats($i['files'], 'repo/fstat'); if ($r === FALSE) return "Internal error"; $ret .= $r; // diff //rg_log_ml("DEBUG: i[files]=" . print_r($i['files'], TRUE)); $r = rg_git_diff($i['vars']['sha1'], $i['files'], 'repo/diff.html'); if ($r === FALSE) return "Internal error"; $ret .= $r; } if (!empty($rg['HTML:commit_labels'])) $ret .= '<br />' . $rg['HTML:commit_labels']; $ret .= '</div>' . "\n"; return $ret; } /* * Creates an archive from a git repo */ function rg_git_archive($repo_path, $treeish, $archive_name, $format) { rg_prof_start('git_archive'); rg_log_enter('git_archives repo_path=' . $repo_path . ' treeish=' . $treeish . ' archive_name=' . $archive_name . ' format=' . $format); $ret = FALSE; while (1) { $cmd = 'git --git-dir=' . escapeshellarg($repo_path) . ' archive --format=' . escapeshellarg($format) . ' --output=' . escapeshellarg($archive_name) . ' ' . escapeshellarg($treeish); $a = rg_exec($cmd, '', FALSE, FALSE); if ($a['ok'] != 1) { rg_git_set_error('error on git archive' . ' (' . $a['errmsg'] . ')'); break; } $ret = TRUE; break; } rg_log_exit(); rg_prof_end("git_archive"); return $ret; } /* * Tests if a merge will result in conflicts ****************** Example for conflict: * changed in both * base 100644 72943a16fb2c8f38f9dde202b7a70ccc19c52f34 a * our 100644 959479a9d0d05bf843067e5f9fb4b2abef354f70 a * their 100644 dbee0265d31298531773537e6e37e4fd1ee71d62 a * @@ -1,2 +1,6 @@ * aaa * +<<<<<<< .our * ccc * +======= * +bbb * +>>>>>>> .their * ****************** Example for a merge without conflicts: * merged * result 100644 3c5556ce5ed90f293dd05e5942ccdbff43a71556 a * our 100644 959479a9d0d05bf843067e5f9fb4b2abef354f70 a * @@ -1,2 +1,2 @@ * aaa * -ccc * +ddd */ function rg_git_merge_tree($repo_path, $base, $a, $b) { global $rg_git_zero; global $rg_git_empty; rg_prof_start('git_merge_tree'); rg_log_enter('rg_git_merge_tree base=' . $base . ' a=' . $a . ' b=' . $b); $ret = FALSE; while (1) { $head = rg_git_load_ref($repo_path, $a); if ($head === FALSE) break; $key = 'git' . '::' . sha1($repo_path) . '::' . 'merge-tree' . '::' . $head . '::' . $b; $r = rg_cache_get($key); if ($r !== FALSE) { $ret = $r; break; } $cmd = 'git --git-dir=' . escapeshellarg($repo_path) . ' merge-tree' . ' ' . escapeshellarg($base) . ' ' . escapeshellarg($a) . ' ' . escapeshellarg($b); $a = rg_exec($cmd, '', FALSE, FALSE); if ($a['ok'] != 1) { rg_git_set_error('error on git merge-tree (' . $a['errmsg'] . ')'); break; } $ret = trim($a['data']); rg_cache_set($key, $ret, RG_SOCKET_NO_WAIT); rg_log_ml('DEBUG: merge-tree: ' . $ret); break; } rg_log_exit(); rg_prof_end('git_merge_tree'); return $ret; } function rg_git_merge_tree_html($repo_path, $base, $a, $b) { $r = rg_git_merge_tree($repo_path, $base, $a, $b); if ($r === FALSE) return rg_git_error(); return nl2br(rg_xss_safe($r)); } /* * Returns the status of a pull request * Returns FALSE on error, 1 if merge is without conflict, else 0 */ function rg_git_merge_without_conflict($repo_path, $a, $b) { rg_log_enter('git_merge_without_conflict a=' . $a . ' b=' . $b); $ret = FALSE; while (1) { $base = rg_git_merge_base($repo_path, $a, $b); if ($base === FALSE) break; $out = rg_git_merge_tree($repo_path, $base, $a, $b); if($out === FALSE) break; $r = strstr($out, "\n+<<<<<<<"); if ($r === FALSE) { $ret = 1; break; } $ret = 0; break; } rg_log_exit(); return $ret; } /* * Do a merge * @ff - 0 = no fast-forward, 1 = fast-forward allowed * @msg - merge message * Returns the output of the command or FALSE */ function rg_git_merge($repo_path, $a, $b, $ff, $msg) { global $rg_git_zero; global $rg_git_empty; rg_prof_start('git_merge'); rg_log_enter('git_merge' . ' a=' . $a . ' b=' . $b . ' ff=' . $ff . ' msg=' . $msg); $ret = FALSE; while (1) { if (empty($repo_path)) $add = ''; else $add = ' --git-dir=' . escapeshellarg($repo_path); $work_tree = rg_tmp_path('git_merge_' . rg_id(10)); $r = @mkdir($work_tree, 0700, TRUE); if ($r !== TRUE) { rg_git_set_error('cannot create temporary dir for merge'); break; } $add .= ' --work-tree ' . escapeshellarg($work_tree); if ($ff == 1) $add_ff = ' --ff'; else $add_ff = ' --no-ff'; $cmd = 'git' . $add . ' merge' . $add_ff . ' --stat' . ' -m ' . escapeshellarg($msg) . ' ' . escapeshellarg($a) . ' ' . escapeshellarg($b); $a = rg_exec($cmd, '', FALSE, FALSE); rg_rmdir($work_tree); if ($a['ok'] != 1) { rg_git_set_error('error on git merge (' . $a['errmsg'] . ')'); break; } $ret = trim($a['data']); break; } rg_log_exit(); rg_prof_end('git_merge'); return $ret; } /* * Creates a pull request against a user repo to be sent by e-mail */ function rg_git_request_pull($repo_path, $start, $url, $end, $patch) { global $rg_git_zero; global $rg_git_empty; rg_prof_start('git_request_pull'); rg_log_enter('git_request_pull' . ' start=' . $start . ' url=' . $url . ' end=' . $end . ' patch=' . ($patch ? 'yes' : 'no')); $text = ''; $ret = FALSE; while (1) { $rg['req'] = array(); $rg['req']['start'] = $start; $rg['req']['url'] = $url; $rg['req']['end'] = $end; $text .= rg_template('repo/mr/req-pull.txt', $rg, TRUE/*xss*/); $r = rg_git_shortlog($repo_path, $baserev, $headrev); if ($r === FALSE) break; $text .= $r; $cmd = 'git diff -M --stat --summary'; if ($patch) $cmd .= ' --patch'; $cmd .= escapeshellarg($start) . '..' . escapeshellarg($end); $r = rg_exec($cmd, '', FALSE, FALSE); if ($a['ok'] != 1) { rg_git_set_error('error on git diff: ' . $a['errmsg']); break; } $text .= $r['data']; $ret = $text; break; } rg_log_exit(); rg_prof_end('git_pull_request'); return $ret; } /* * Prepares a git packet from a string */ function rg_git_pack($str) { return sprintf("%04x", 4 + strlen($str)) . $str; } /* * Prepares a 'flush' packet */ function rg_git_flush() { return '0000'; } /* * Callback used by rg_exec to output in band 1 */ function rg_git_band_1($s) { echo rg_git_pack("\x01" . $s); } /* * Callback used by rg_exec to output in band 2 */ function rg_git_band_2($s) { echo rg_git_pack("\x02" . $s); } /* * Callback used by rg_exec to output in band 3 */ function rg_git_band_3($s) { echo rg_git_pack("\x03" . $s); } ?>