<?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_log($str); $rg_git_error = $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"; 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) { $pattern = "[a-zA-Z0-9^~\/_]"; if (preg_match('/^' . $pattern . '$/uD', $refname) === FALSE) { $chars = preg_replace('/' . $pattern . '/', '', $refname); rg_log("git_reference: ref [$refname] contains invalid chars ($chars)"); return ""; } 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; } // TODO: Unit testing /* * Safely update a reference - used to update main namespace from other namespace * If @new is empty, we assume a delete */ 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) { rg_prof_start("git_diff2array"); $ret = array(); $lines = explode("\n", $diff); $file = 0; foreach ($lines as $line) { if (strncmp($line, "diff ", 5) == 0) { $file++; $ret[$file] = array(); $ret[$file]['flags'] = ""; $ret[$file]['chunks'] = array(); $file_name_sel = "dst"; $file_name_tmp = array(); continue; } if (strncmp($line, "new file ", 9) == 0) { $ret[$file]['flags'] .= "N"; continue; } if (strncmp($line, "deleted file ", 13) == 0) { $ret[$file]['flags'] .= "D"; $file_name_sel = "src"; continue; } if (strncmp($line, "index ", 6) == 0) { $ret[$file]['index'] = substr($line, 6); continue; } if (strncmp($line, "--- ", 4) == 0) { if (strncmp($line, "--- a/", 2) == 0) $file_name_tmp['src'] = substr($line, 6); else $file_name_tmp['src'] = substr($line, 4); continue; } if (strncmp($line, "+++ ", 4) == 0) { if (strncmp($line, "+++ b/", 2) == 0) $file_name_tmp['dst'] = substr($line, 6); else $file_name_tmp['dst'] = substr($line, 4); continue; } // parse line "@@ -14,6 +14,8 @@ function..." // @@ from_file_range to_file_range @@ ... if (strncmp($line, "@@ ", 3) == 0) { $_t = explode(" ", $line, 5); if (count($_t) < 4) { rg_internal_error("invalid line [$line]: count != 5"); return FALSE; } $chunk = $_t[1] . " " . $_t[2]; $ret[$file]['chunks'][$chunk] = array(); $ret[$file]['chunks'][$chunk]['section'] = isset($_t[4]) ? trim($_t[4]) : ""; $from = explode(",", substr($_t[1], 1)); $ret[$file]['chunks'][$chunk]['from'] = intval($from[0]); $to = explode(",", substr($_t[2], 1)); $ret[$file]['chunks'][$chunk]['to'] = intval($to[0]); continue; } if (!isset($file_name_tmp[$file_name_sel])) { rg_internal_error("file_name_tmp[$file_name_sel] does not exists" . "; file_name_tmp: " . print_r($file_name_tmp, TRUE)); return FALSE; } if (!isset($ret[$file]['file'])) $ret[$file]['file'] = $file_name_tmp[$file_name_sel]; // empty is because somehow git log pass an empty line. // TODO: we should check this theory. if (empty($line) || (strncmp($line, " ", 1) == 0) || (strncmp($line, "+", 1) == 0) || (strncmp($line, "-", 1) == 0)) { $ret[$file]['chunks'][$chunk]['lines'][] = $line; continue; } // Ignore, for now, "\ No newline at end of file" (TODO) if (strncmp($line, "\\", 1) == 0) { rg_log("\tINFO: warn line: [$line]."); continue; } if (empty($line)) { rg_log("\tWARN: empty line [$line]!"); continue; } rg_internal_error("I do not know how to parse [" . trim($line) . "]!"); $ret = FALSE; } 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 */ 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."); $ret = ""; 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" . " --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\"" . " --numstat" . $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; } // we prepend a \0 because data starts with -=ROCK... $blocks = explode("\0-=ROCKETGIT=-\0", "\0" . $a['data']); // ignore first entry because is empty unset($blocks[0]); $ret = array(); foreach ($blocks as $junk => $block) { $y = array("vars" => array(), "files" => array(), "patches" => array()); // split block in two: vars and stats + patches $parts = explode("\0ROCKETGIT_END_OF_VARS\0", $block, 2); // vars $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 echo "Var " . $_t[0] . " has no value!\n"; } // stats & patches $stats_and_patches = trim($parts[1]); $_sp = explode("\0\0", $stats_and_patches, 2); $stats = $_sp[0]; if (isset($_sp[1])) { $y['patches'] = rg_git_diff2array($_sp[1]); if ($y['patches'] === FALSE) break; } // stats $_t = explode("\0", $stats); $y['vars']['files_changed'] = count($_t); $total_add = 0; $total_del = 0; foreach ($_t as $junk => $fi) { $__t = explode("\t", $fi); $y['files'][$__t[2]] = array( "add" => $__t[0], "del" => $__t[1]); $total_add += intval($__t[0]); $total_del += intval($__t[1]); } $y['vars']['lines_add'] = $total_add; $y['vars']['lines_del'] = $total_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] * 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) { if (!isset($finfo['file'])) rg_log("BAD finfo:" . rg_array2string($finfo)); $ret .= "<br />\n"; $f = htmlspecialchars($finfo['file']); $ret .= "<a name=\"$f\">\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 $ret .= "File <b>$f</b> changed:"; $ret .= "</td></tr>\n"; $empty_line = ""; foreach ($finfo['chunks'] as $chunk => $ci) { $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@@/", htmlspecialchars($left), $v); $v = preg_replace("/@@right@@/", htmlspecialchars($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 */ function rg_git_files_stats($a, $dir) { $t = array(); foreach ($a as $file => $info) { $line = array(); $line['file'] = $file; $line['add'] = $info['add']; $line['del'] = $info['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)); $ip = $a['ip']; $uid = $a['login_uid']; if (strcmp($a['new_rev_type'], "tag") == 0) { // Annotated if (strcmp($a['old_rev'], $rg_git_zero) == 0) { // create if (!rg_rights_allow($db, $a['repo_id'], "repo_refs", $a['repo.uid'], $uid, "S", $ip, $a['refname'])) rg_git_fatal($a['refname'] . "\nNo rights to" . " create an annotated tag."); } else if (strcmp($a['new_rev'], $rg_git_zero) == 0) { // delete rg_log("delete ann tag"); if (!rg_rights_allow($db, $a['repo_id'], "repo_refs", $a['repo.uid'], $uid, "n", $ip, $a['refname'])) rg_git_fatal($a['refname'] . "\nNo rights to" . " delete an annotated tag."); } else { // change rg_log("This seems it cannot happen in recent git."); if (!rg_rights_allow($db, $a['repo_id'], "repo_refs", $a['repo.uid'], $uid, "S", $ip, $a['refname'])) rg_git_fatal($a['refname'] . "\nNo rights to" . " change an annotated tag."); } } else { // Un-annotated if (strcmp($a['old_rev'], $rg_git_zero) == 0) { // create if (!rg_rights_allow($db, $a['repo_id'], "repo_refs", $a['repo.uid'], $uid, "Y", $ip, $a['refname'])) rg_git_fatal($a['refname'] . "\nNo rights to" . " create an un-annotated tag."); } else if (strcmp($a['new_rev'], $rg_git_zero) == 0) { // delete if (!rg_rights_allow($db, $a['repo_id'], "repo_refs", $a['repo.uid'], $uid, "u", $ip, $a['refname'])) rg_git_fatal($a['refname'] . "\nNo rights to" . " delete an un-annotated tag."); } else { // change if (!rg_rights_allow($db, $a['repo_id'], "repo_refs", $a['repo.uid'], $uid, "U", $ip, $a['refname'])) rg_git_fatal($a['refname'] . "\nNo rights to" . " change an un-annotated tag."); } } // 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 $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 namespace - TODO } 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)); $ip = $a['ip']; $uid = $a['login_uid']; if (strcmp($a['new_rev'], $rg_git_zero) == 0) { // delete if (!rg_rights_allow($db, $a['repo_id'], "repo_refs", $a['repo.uid'], $uid, "D", $ip, $a['refname'])) rg_git_fatal($a['refname'] . "\nNo rights to delete" . " a branch."); 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 if (!rg_rights_allow($db, $a['repo_id'], "repo_refs", $a['repo.uid'], $uid, "H|C", $ip, $a['refname'])) 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 if (!rg_rights_allow($db, $a['repo_id'], "repo_refs", $a['repo.uid'], $uid, "O", $ip, $a['refname']) && ($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! if (!rg_rights_allow($db, $a['repo_id'], "repo_refs", $a['repo.uid'], $uid, "M", $ip, $a['refname'])) { if (rg_git_rev_ok($a['new_rev'] . "^2")) rg_git_fatal($a['refname'] . "\nNo rights to push merges."); } // Check for bad whitespace if (!rg_rights_allow($db, $a['repo_id'], "repo_refs", $a['repo.uid'], $uid, "W", $ip, $a['refname'])) { // 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"); foreach ($r as $file) { if (rg_rights_allow($db, $a['repo_id'], "repo_path", $a['repo.uid'], $uid, "P", $ip, $file) !== TRUE) { rg_git_fatal($a['refname'] . "\nNo rights to push file [$file]\n"); } if (!rg_rights_allow($db, $a['repo_id'], "repo_path", $a['repo.uid'], $uid, "W", $ip, $a['refname'])) { $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); } } } if (rg_rights_allow($db, $a['repo_id'], "repo_refs", $a['repo.uid'], $uid, "P", $ip, $a['refname']) !== TRUE) { rg_log("\tPush is not allowed, let's see the anon one"); if (rg_rights_allow($db, $a['repo_id'], "repo_refs", $a['repo.uid'], $uid, "H", $ip, $a['refname']) === FALSE) { $_x = array(); $msg = rg_template("msg/push_not_allowed.txt", $_x); 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']); $r = rg_git_update_ref($mr, "", $a['new_rev'], "mr"); if ($r !== TRUE) { rg_log("Cannot update-ref: " . rg_git_error()); rg_git_fatal($a['refname'] . ": Cannot set refs/mr/." . " Try again later."); } $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); } 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 $r = rg_git_update_ref($a['refname'], $a['old_rev'], $a['new_rev'], "push"); if ($r !== TRUE) { rg_git_fatal($a['refname'] . "\nCannot update ref (" . rg_git_error() . ")"); } // We can clean now the namespace - TODO } } 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 = htmlspecialchars($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; } ?>