<?php require_once($INC . "/util.inc.php"); require_once($INC . "/log.inc.php"); require_once($INC . "/prof.inc.php"); $rg_git_zero = "0000000000000000000000000000000000000000"; $rg_git_empty = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; $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) { echo "==========\n"; $x = explode("\n", trim($msg)); foreach ($x as $line) { rg_log("FATAL: $line"); echo "RocketGit: $line\n"; } echo "==========\n"; flush(); exit(1); } function rg_git_info($msg) { echo "==========\n"; $x = explode("\n", trim($msg)); foreach ($x as $line) { rg_log("INFO: $line"); echo "RocketGit: $line\n"; } echo "==========\n"; } /* * Installs rg hooks instead of original ones, by making a link */ function rg_git_install_hooks($dst) { 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("\tRemoving original hooks dir..."); if (!rg_rmdir($dst . "/hooks")) { rg_git_set_error("cannot remove hooks dir" . " (" . rg_util_error() . ")"); break; } rg_log("\tLink 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) { 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, 0755, TRUE); if ($r === FALSE) { rg_git_set_error("cannot create dir [$dir] ($php_errormsg)"); break; } } // TODO: What if the git init does not finish?! // Should we create it in a tmp dir and rename? // Does git has any protection? if (!is_dir($dst . "/rocketgit")) { $cmd = "git init --bare " . escapeshellarg($dst); $a = rg_exec($cmd); if ($a['ok'] != 1) { rg_git_set_error("error on init " . $a['errmsg'] . ")"); break; } if (!@mkdir($dst . "/rocketgit")) { 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_init"); return $ret; } function rg_git_clone($src, $dst) { 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, 0755, 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); 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 '" . $obj . "'"; $a = rg_exec($cmd); 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 '" . $obj . "'"; $a = rg_exec($cmd); 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 (preg_match('/\.\./', $refname) !== 0) { rg_git_set_error('we do not accept \'..\' inside the ref name'); return FALSE; } // We do not accept '/.' chars if (preg_match('/\/\./', $refname) !== 0) { rg_git_set_error('we do not accept \'/.\' inside the ref name'); return FALSE; } // We do not accept '\\' chars if (preg_match('/\\\\/', $refname) !== 0) { 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\/_.]*"; $r = preg_match('/^' . $pattern . '$/uD', $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 '" . $rev . "'"; $a = rg_exec($cmd); if ($a['ok'] != 1) { rg_git_set_error("error on rev-parse (" . $a['errmsg'] . ")"); break; } $ret = TRUE; 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); rg_log("\ta:" . 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; } // TODO: Unit testing function rg_git_merge_base($old, $new) { rg_prof_start("git_merge_base"); rg_log_enter("git_merge_base: old=$old new=$new"); $ret = FALSE; while (1) { $cmd = "git merge-base " . $old . " " . $new; $a = rg_exec($cmd); if ($a['ok'] != 1) { rg_git_set_error("error on merge-base (" . $a['errmsg'] . ")"); break; } $ret = trim($a['data']); 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($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 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); 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; } /* * Returns a tree (git ls-tree) */ function rg_git_ls_tree($tree, $path) { rg_prof_start("git_ls_tree"); rg_log_enter("rg_git_ls_tree: tree=$tree path=$path"); $ret = FALSE; while (1) { $op = " "; if (empty($tree)) { $op = " --full-tree"; $tree = " HEAD"; } $cmd = "git ls-tree --long" . $op . $tree; if (!empty($path)) $cmd .= " " . escapeshellarg($path); rg_log("DEBUG: cmd=$cmd"); $a = rg_exec($cmd); if ($a['ok'] != 1) { rg_git_set_error("error on ls-tree (" . $a['errmsg'] . ")"); break; } if (empty($a['data'])) { rg_git_set_error("error on ls-tree: empty answer"); break; } $output = explode("\n", trim($a['data'])); $ret = array(); foreach ($output as $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['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, "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("\tWARN: 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("\tINFO: 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) { rg_prof_start("git_log"); rg_log_enter("git_log: path=$path from=$from to=$to max=$max"); $ret = FALSE; while (1) { if (!file_exists($path . "/refs/heads/master")) { rg_log("\tRepo is empty."); break; } $max_count = ($max == 0) ? "" : " --max-count=$max"; $patches = $also_patch ? " --patch" : ""; if (empty($from) && empty($to)) { $from_to = ""; } else { if (empty($from)) $from_to = " " . $to; else $from_to = " " . $from . ".." . $to; } $cmd = "git --no-pager" . " --git-dir=" . escapeshellarg($path) . " log" . " --find-copies" . " --no-merges" . " -z" . $max_count . $patches . " --pretty=\"format:" . "%x00-=ROCKETGIT=-%x00" . "sha1_short:%h%x00\"\"" . "sha1_long:%H%x00\"\"" . "tree:%t%x00\"\"" . "parents_short:%p%x00\"\"" . "parents_long:%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\"\"" . "subject:%s%x00\"\"" . "body:%b%x00\"\"" . "notes:%N%x00\"\"" . "%x00ROCKETGIT_END_OF_VARS%x00\"" . $from_to; $a = rg_exec($cmd); if ($a['ok'] != 1) { rg_git_set_error("error on log (" . $a['errmsg'] . ")"); rg_internal_error("Could not generate log."); 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!"); } } // patches if (isset($parts[1])) { $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']; } // 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 */ function rg_git_stats($log) { $ret = array( "authors" => array(), "commits" => 0, "lines_add" => 0, "lines_del" => 0 ); foreach ($log as $index => $ci) { $v = $ci['vars']; if (!isset($ret['project_start_date'])) { $ret['project_start_date'] = $v['author date']; $ret['project_start_author'] = $v['author name']; } $ret['project_last_date'] = $v['author date']; $ret['project_last_author'] = $v['author name']; // global stats $ret['lines_add'] += intval($v['lines_add']); $ret['lines_del'] += intval($v['lines_del']); $ret['commits']++; // stats per author $a = $v['author name']; if (!isset($ret['authors'][$a])) { $ret['authors'][$a] = array( 'first_commit' => 0, 'last_commit' => 0, 'commits' => 0, 'lines_add' => 0, 'lines_del' => 0); } $ret['authors'][$a]['commits']++; $ret['authors'][$a]['lines_add'] += intval($v['lines_add']); $ret['authors'][$a]['lines_del'] += intval($v['lines_del']); if ($ret['authors'][$a]['first_commit'] == 0) $ret['authors'][$a]['first_commit'] = $v['author date']; $ret['authors'][$a]['last_commit'] = $v['author date']; } 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"); // TODO: Here we can deny non ascii file names. Move to update_branch? // git diff --cached --name-only --diff-filter=A -z $against | LC_ALL=C tr -d '[ -~]\0') $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); if ($a['ok'] != 1) { rg_git_set_error("error on git diff (" . $a['errmsg'] . ")"); break; } if (empty($a['data'])) { rg_git_set_error("error on ls-tree: empty answer"); 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 * @a - output of rg_git_diff2array[index]['files'] * TODO: Switch to rg_template_table? */ function rg_git_diff($a, $template_file) { rg_prof_start("git_diff"); //rg_log_enter("DEBUG: git_diff: a: " . rg_array2string($a)); $ret = "<div class=\"diff\">\n"; $x = array(); $template = rg_template($template_file, $x); // 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=\"$f\"></a>\n"; $ret .= "<table class=\"chunk\" width=\"100%\">\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 commiter 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) { $v = $template; $left_color = "#eeeeee"; $right_color = "#eeeeee"; $c = substr($line, 0, 1); $line = substr($line, 1); if (strcmp($c, "+") == 0) { $left = ""; $right = $line; $right_color = "#00ff00"; $line_left = " "; $line_right = $line_no_right; $line_no_right++; } else if (strcmp($c, "-") == 0) { $left = $line; $left_color = "#ff0000"; $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++; } $v = preg_replace("/@@line_left@@/", $line_left, $v); $v = preg_replace("/@@line_right@@/", $line_right, $v); $v = preg_replace("/@@left@@/", rg_xss_safe($left), $v); $v = preg_replace("/@@right@@/", rg_xss_safe($right), $v); $v = preg_replace("/@@left_color@@/", $left_color, $v); $v = preg_replace("/@@right_color@@/", $right_color, $v); $ret .= $v; } } $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['login_username']; $x['needed_rights'] = ''; $x['ip'] = $a['ip']; $x['misc'] = $a['refname']; $history = 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 namespace $reason = $a['login_username'] . ' pushed tag ' . $a['refname']; $r = rg_git_update_ref($a['refname'], $a['old_rev'], $a['new_rev'], $reason); if ($r !== TRUE) { rg_git_fatal($a['refname'] . "\nCannot update ref (" . rg_git_error() . ")"); } // We can clean now the tmp namespace - TODO } rg_repo_history_insert($db, $history); rg_log_exit(); rg_prof_end("git_update_tag"); } /* * */ function rg_git_update_branch($db, $a) { global $rg_git_zero; rg_prof_start("git_update_branch"); rg_log("git_update_branch: " . 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['login_username']; $_x['needed_rights'] = ''; $_x['ip'] = $a['ip']; $_x['misc'] = $a['refname']; $history = 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'] . "\nNo 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); return; } // If we have 'H' (anonymous push), we have also create branch $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['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'] . "\nNon fast-forward is" . " not allowed."); } // 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")) rg_git_fatal($a['refname'] . "\nNo 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'] . "\nNo rights to push bad whitespace:" . "\n" . $w); } // Check repo_path rights TODO $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'] . "\nNo rights to push bad whitespace on path [$file]:" . "\n" . $w); } } } $x = $_x; $x['type'] = 'repo_refs'; $x['needed_rights'] = 'P'; $x['misc'] = $a['refname']; if (rg_rights_allow($db, $x) !== TRUE) { rg_log("\tPush 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); rg_git_fatal($a['refname']. "\n" . $msg); } // anonymous push - create a merge request // TODO: git may fail to update the reference after this hook; // the mr code should check if the update was done. $mr = "refs/mr/" . preg_replace('/refs\/heads\//', '', $a['refname']) . "_" . preg_replace('/rg_/', '', $a['namespace']); $reason = $a['login_username'] . ' pushed a merge request' . ' for ref ' . $a['refname'] . ' into namespace ' . $a['namespace']; $r = rg_git_update_ref($mr, "", $a['new_rev'], $reason); if ($r !== TRUE) { rg_log("Cannot update-ref: " . rg_git_error()); rg_git_fatal($a['refname'] . ": Cannot set refs/mr/." . " Try again later."); } // TODO: here or inside below function we should record // a hisotry event that an anon push was done. $r = rg_mr_queue_add($a['repo_id'], $a['namespace'], $a['old_rev'], $a['new_rev'], $a['refname'], $a['ip']); if ($r !== TRUE) rg_git_fatal($a['refname'] . ": " . rg_mr_error()); $_x = array(); $msg = rg_template("msg/push_merge_request.txt", $_x); 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']; // TODO: we shoult notify by e-mail that a merge request is // waiting. } else { rg_log("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'])) { // Update the main namespace $reason = $a['login_username'] . ' pushed ref ' . $a['refname']; $r = rg_git_update_ref($a['refname'], $a['old_rev'], $a['new_rev'], $reason); if ($r !== TRUE) { rg_git_fatal($a['refname'] . "\nCannot update ref (" . rg_git_error() . ")"); } // We can clean now the tmp namespace - TODO } 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); 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 an array with links to branches and tags */ function rg_git_branches_and_tags($repo_dir, $base_url, $current_ref) { rg_log_enter("git_branches_and_tags: repo_dir=$repo_dir 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]"); $refs = rg_git_refs($repo_dir); $_l = array(); foreach ($refs as $o => $list) { if (empty($list)) continue; foreach ($list as $name) { $name = rg_xss_safe($name); $ename = preg_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("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 = array_shift($paras); $ret['ref_url'] = "/" . $ret['ref_type'] . "/" . $val; $val = preg_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); 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); 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); $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']) . "#" . rg_xss_safe($i['vars']['sha1_short']); $ret .= rg_git_log_template($log, 'repo/log', $rg); } // TODO: move this into a template! foreach ($log as $junk => $i) { // Some info about commit $ret .= "<br /><b>" . "<a name=\"" . rg_xss_safe($i['vars']['sha1_short']) . "\">" . "Commit " . rg_xss_safe($i['vars']['sha1_short']) . "</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 />\n" . "<b>Author</b>: " . rg_xss_safe($i['vars']['author name']); if (!empty($i['vars']['commiter name'])) $ret .= "<br />\n" . "<b>Commiter</b>: " . rg_xss_safe($i['vars']['commiter name']); $ret .= "<br />\n" . "<b>Date (UTC)</b>: " . gmdate("Y-m-d H:i", $i['vars']['author date']); $ret .= "<br />\n"; // stats $r = rg_git_files_stats($i['files'], 'repo/fstat'); if ($r === FALSE) return "Internal error"; $ret .= $r; // diff $r = rg_git_diff($i['files'], 'repo/diff.html'); if ($r === FALSE) return "Internal error"; $ret .= $r; } return $ret; } ?>