<?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_patch_limit_default = 5000;
$rg_git_debug = 0;
$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 limit for diff for 'git log --patch'
*/
function rg_git_patch_limit($db)
{
global $rg_git_patch_limit_force;
global $rg_git_patch_limit_default;
// This is for functionl tests
if (isset($rg_git_patch_limit_force))
return $rg_git_patch_limit_force;
$r = rg_state_get($db, 'git_patch_limit');
if (($r === FALSE) || empty($r))
return $rg_git_patch_limit_default;
return intval($r);
}
/*
* 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);
}
/*
* Fix from/to references
* Helper for several functions.
*/
function rg_git_from_to($from, $to)
{
global $rg_git_zero;
global $rg_git_empty;
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;
} else {
$from_to = $from . '..' . $to;
}
return $from_to;
}
/*
* 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']) && !empty($path)) {
rg_git_set_error('path does not exists');
break;
}
$ret = array();
if (empty($a['data'])) {
// It seems to be an empty tree
break;
}
$output = explode("\n", trim($a['data']));
unset($a['data']); // manually free data
foreach ($output as $line) {
//rg_log('DEBUG: processing line [' . $line . ']');
$_y = array();
$_t = explode("\t", $line);
unset($line); // manually free data
$_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);
unset($_i); // manually free data
$_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)
* @out - will be populated with the chunks
*/
function rg_git_diff2array($diff, &$out)
{
global $rg_git_debug;
rg_prof_start("git_diff2array");
//rg_log_ml("DEBUG: git_diff2array: diff: " . $diff);
$ret = TRUE;
$lines = explode("\n", $diff);
unset($diff); // manually free data
//rg_log_ml("DEBUG: lines: " . print_r($lines, TRUE));
foreach ($lines as $line) {
if ($rg_git_debug > 90)
rg_log("DEBUG: line=$line");
// format: diff --git a/a b/a
if (strncmp($line, "diff --git ", 11) == 0) {
$a = array();
$a['flags'] = '';
$a['old_mode'] = '';
$a['mode'] = '';
$a['similarity'] = '';
$a['dissimilarity'] = '';
$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);
unset($rest); // manually free data
foreach ($_t as &$_file) {
if (strncmp($_file, '"', 1) == 0)
$_file = substr($_file, 1, -1);
$_file = str_replace('\"', '"', $_file);
}
$a['file_from'] = $_t[0];
$a['file'] = $_t[1];
$a['index'] = '';
$a['chunks'] = array();
$file = $a['file'];
if (!isset($out[$file])) {
rg_git_set_error('internal error');
rg_internal_error('we have a diff for a non-existing file');
$ret = FALSE;
break;
}
foreach ($a as $k => $v)
$out[$file][$k] = $v;
continue;
}
if (strncmp($line, "old mode ", 9) == 0) {
$out[$file]['old_mode'] = substr($line, 9);
continue;
}
if (strncmp($line, "new mode ", 9) == 0) {
$out[$file]['mode'] = substr($line, 9);
continue;
}
if (strncmp($line, "deleted file mode ", 18) == 0) {
$out[$file]['flags'] .= 'D';
$out[$file]['old_mode'] = substr($line, 18);
continue;
}
if (strncmp($line, "new file mode ", 14) == 0) {
$out[$file]['flags'] .= 'N';
$out[$file]['mode'] = substr($line, 14);
continue;
}
if (strncmp($line, "copy from ", 10) == 0) {
$out[$file]['flags'] .= 'C';
continue;
}
if (strncmp($line, "copy to ", 8) == 0)
continue;
if (strncmp($line, "rename from ", 12) == 0) {
$out[$file]['flags'] .= 'R';
continue;
}
if (strncmp($line, "rename to ", 10) == 0)
continue;
if (strncmp($line, "similarity index ", 17) == 0) {
$out[$file]['similarity'] = substr($line, 17);
continue;
}
if (strncmp($line, "dissimilarity index ", 20) == 0) {
$out[$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);
unset($rest); // manually free data
$out[$file]['index'] = $_t[0];
if (isset($_t[1]))
$out[$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");
$ret = FALSE;
break;
}
$chunk = $_t[1] . " " . $_t[2];
$out[$file]['chunks'][$chunk] = array();
$out[$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]);
}
$out[$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]);
}
$out[$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)) {
$out[$file]['chunks'][$chunk]['lines'][] = $line;
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
* @files - restrict the log to some files; if empty, all are returned.
* TODO: $also_merges: remove --no-merges
* 'simple' because rg_git_log will do the filtering of big diffs.
*/
function rg_git_log_simple($repo_path, $max, $from, $to, $also_patch, $files,
$patch_limit)
{
global $rg_git_debug;
rg_prof_start('git_log_simple');
rg_log_enter('git_log_simple: repo_path=' . $repo_path
. ' max=' . $max . ' from=' . $from . ' to=' . $to
. ' also_patch=' . ($also_patch ? 'true' : 'false')
. ' files:' . rg_array2string($files)
. ' patch_limit=' . $patch_limit);
$ret = FALSE;
while (1) {
if (!file_exists($repo_path . '/refs/heads/master')) {
if (!file_exists($repo_path . '/.git/refs/heads/master')) {
rg_log('Repo is empty.');
$ret = array();
break;
}
}
$max_count = ($max == 0) ? "" : " --max-count=$max";
$patches = $also_patch ? " --patch" : "";
$from_to = rg_git_from_to($from, $to);
$id = rg_id(16);
$sep_start = '-=ROCKETGIT-START-' . $id . '=-';
$sep_end = '-=ROCKETGIT_END_OF_VARS-' . $id . '=-';
$cmd = "git --no-pager"
. " --git-dir=" . escapeshellarg($repo_path)
. " log"
. " --find-copies"
. " --find-renames"
. ' --find-copies-harder'
. " --no-merges"
. ' --numstat'
. " -z"
. $max_count
. $patches
. " --pretty=\"format:"
. $sep_start
. "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\"\""
. $sep_end
. "\"";
if (!empty($from_to))
$cmd .= ' ' . escapeshellarg($from_to);
if (!empty($files)) {
$cmd .= ' --';
foreach ($files as $f)
$cmd .= ' ' . escapeshellarg($f);
}
$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;
}
if ($rg_git_debug > 70)
rg_log_ml("DEBUG: OUTPUT OF GIT LOG: " . $a['data']);
$blocks = explode($sep_start, $a['data']);
unset($a['data']); // manually free memory
// because data starts with the separator, we remove it
unset($blocks[0]);
//rg_log_ml('DEBUG: blocks: ' . print_r($blocks, TRUE));
$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($sep_end, $block, 2);
unset($block); // manually free memory
//rg_log_ml('DEBUG: parts: ' . print_r($parts, TRUE));
// vars
$y['vars']['lines_add'] = 0;
$y['vars']['lines_del'] = 0;
$x = explode ("\0", $parts[0]);
unset($parts[0]); // manually free data
$count = count($x) - 1; // last is empty
for ($i = 0; $i < $count; $i++) {
$_t = explode(':', $x[$i], 2);
if (isset($_t[1])) {
$y['vars'][$_t[0]] = $_t[1];
} else if (empty($_t[0])) {
// do nothing
} else {
//rg_log("DEBUG: Var [" . $_t[0] . "] has no value!");
}
}
// Some 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']);
if (!isset($parts[1])) {
// we do nothing
$ret[] = $y;
continue;
}
//rg_log_ml('DEBUG parts[1]: ' . print_r($parts[1], TRUE));
// numstat [+ diff separated by \0\0]
$n_d = explode("\0\0", $parts[1]);
unset($parts[1]); // manually free data
//rg_log_ml('DEBUG: n_d: ' . print_r($n_d, TRUE));
// numstat
$numstat = explode("\0", trim($n_d[0]));
//rg_log_ml('DEBUG: numstat: ' . print_r($numstat, TRUE));
$tc = count($numstat);
while ($tc > 0) {
$a = explode("\t", array_shift($numstat)); $tc--;
//rg_log_ml('DEBUG: a: ' . print_r($a, TRUE));
// We may have an empty commit
if (count($a) == 1)
break;
if (count($a) != 3) {
rg_internal_error('invalid numstat: c=' . count($a)
. ' numstat: ' . print_r($n_d[0], TRUE) . '.');
break;
}
$f = $a[2];
if (empty($f)) { // it is a rename or copy
if ($tc < 2) {
rg_internal_error('a rename or copy with invalid fields');
rg_internal_error('numstat: '
. print_r($n_d[0], TRUE));
break;
}
// throw away the source (not needed)
array_shift($numstat);
$f = array_shift($numstat);
$tc -= 2;
}
$y['files'][$f] = array(
'file' => $f,
'chunks' => array(),
'flags' => '',
'lines_add' => intval($a[0]),
'lines_del' => intval($a[1])
);
$y['files'][$f]['changes'] =
$y['files'][$f]['lines_add']
+ $y['files'][$f]['lines_del'];
// We will mark over sized diffs for later use
$changes = $y['files'][$f]['lines_add']
+ $y['files'][$f]['lines_del'];
//rg_log('DEBUG: File [' . $f . '] '
// . $changes . ' changes');
$y['files'][$f]['oversize_diff'] =
$changes > $patch_limit ? 1 : 0;
// Add to total
$y['vars']['lines_add'] += $y['files'][$f]['lines_add'];
$y['vars']['lines_del'] += $y['files'][$f]['lines_del'];
}
//rg_log_ml('DEBUG: files: ' . print_r($y['files'], TRUE));
if ($also_patch === FALSE) {
$ret[] = $y;
continue;
}
if (!isset($n_d[1])) {
// TODO: can happen if the diff is empty?
rg_internal_error('patch, but diff missing!');
rg_git_set_error('internal error');
$ret = FALSE;
break;
}
// now, patch, if present
if ($rg_git_debug > 80)
rg_log_ml('DEBUG: n_d[1]:' . "\n"
. print_r($n_d[1], TRUE));
$r = rg_git_diff2array($n_d[1], $y['files']);
if ($r === FALSE)
break;
//rg_log_ml('DEBUG: diff2array: ' . print_r($y['files'], TRUE));
//rg_log_ml('DEBUG: y: ' . print_r($y, TRUE));
$ret[] = $y;
}
break;
}
//rg_log_ml('DEBUG: simple: ' . print_r($ret, TRUE));
rg_log_exit();
rg_prof_end('git_log_simple');
return $ret;
}
/*
* Works on git_log (without patch) output and detect big diffs.
* Returns an array with all the info needed to prepage a 'git log'.
* Will return an empty array if normal log should be called.
*/
function rg_git_log_detect_big_diff($stat, $from)
{
rg_log_enter('git_log_detect_big_diff from=' . $from);
//rg_log_ml('DEBUG: stat: ' . print_r($stat, TRUE));
$ret = array();
$pos = 0;
$last_was_good = 2; // 2 = not good or bad
$ret[0] = array('from' => $from);
$at_least_one_bad = FALSE;
foreach ($stat as $junk => $per_commit) {
$hash = $per_commit['vars']['sha1'];
$good_files = array();
$we_have_bad_files = FALSE;
foreach ($per_commit['files'] as $fname => $i) {
if ($i['oversize_diff']) {
$we_have_bad_files = TRUE;
$at_least_one_bad = TRUE;
} else {
$good_files[] = $fname;
}
}
if ($we_have_bad_files === FALSE)
$good_files = array(); // = all
//rg_log_enter('DEBUG: hash ' . $hash);
//rg_log('DEBUG: we_have_bad_files='
// . ($we_have_bad_files ? 'TRUE' : 'FALSE'));
//rg_log('DEBUG: last_was_good=' . $last_was_good);
//rg_log_ml('DEBUG: good_files: ' . print_r($good_files, TRUE));
if ($we_have_bad_files) {
if ($last_was_good < 2) {
rg_log('last_was_good == 0/1');
$ret[$pos]['from'] = $hash;
$ret[$pos]['from_to'] = rg_git_from_to($ret[$pos]['from'], $ret[$pos]['to']);
$pos++;
}
$ret[$pos] = array(
'type' => 'bad',
'from' => '', // we do not need it
'to' => $hash,
'good_files' => $good_files
);
$ret[$pos]['from_to'] = rg_git_from_to($ret[$pos]['from'], $ret[$pos]['to']);
$last_was_good = 0;
} else {
// we have no bad files
if ($last_was_good == 1) {
rg_log('last_was_good == 1; do nothing');
} else {
if ($last_was_good == 0) {
rg_log('last_was_good == 0');
$ret[$pos]['from'] = $hash;
$ret[$pos]['from_to'] = rg_git_from_to($ret[$pos]['from'], $ret[$pos]['to']);
$pos++;
} else {
rg_log('last_was_good == 2');
}
$ret[$pos]['type'] = 'good';
$ret[$pos]['to'] = $hash;
}
$last_was_good = 1;
}
//rg_log_exit();
}
// We may not had the chance to set 'from'
if (!isset($ret[$pos]['from'])) {
$ret[$pos]['from'] = $from;
$ret[$pos]['from_to'] = rg_git_from_to($ret[$pos]['from'], $ret[$pos]['to']);
}
//rg_log_ml('DEBUG final (after detect big diff): ' . print_r($ret, TRUE));
// No need to parse the array
if ($at_least_one_bad === FALSE)
$ret = array();
rg_log_exit();
return $ret;
}
/*
* Show last @max commits, no merges, sort by topo
* @also_patch = TRUE if caller needs also the diff
* TODO: $also_merges: remove --no-merges
* '@from' - it will be excluded from the list of commits
*/
function rg_git_log($repo_path, $max, $from, $to, $also_patch, $patch_limit)
{
rg_prof_start('git_log');
rg_log_enter('git_log: repo_path=' . $repo_path . ' from=' . $from
. ' to=' . $to .' max=' . $max);
$ret = FALSE;
while (1) {
$good_files = array(); // = all
$stat = rg_git_log_simple($repo_path, $max, $from, $to,
FALSE /*also_patch*/, $good_files, $patch_limit);
// First, if 'also_path' is FALSE, we just call simple version
// because we do not show a diff which should be filtered.
if ($also_patch === FALSE) {
$ret = $stat;
break;
}
$r = rg_git_log_detect_big_diff($stat, $from);
if (empty($r)) { // = no big diff
$good_files = array(); // = all
$ret = rg_git_log_simple($repo_path, $max, $from, $to,
$also_patch, $good_files, $patch_limit);
break;
}
foreach ($r as $i) {
//rg_log_ml('DEBUG: Generating log for ' . print_r($i, TRUE));
if (strcmp($i['type'], 'good') == 0)
$_files = array();
else
$_files = $i['good_files'];
$x = rg_git_log_simple($repo_path, $max,
$i['from'], $i['to'], $also_patch, $_files,
$patch_limit);
if ($x === FALSE)
break;
//rg_log_ml('DEBUG: x: ' . print_r($x, TRUE));
// Overwrite $stat with the latest info
foreach ($x as $hash => $per_hash) {
foreach ($per_hash['files'] as $f => $per_file)
$stat[$hash]['files'][$f] = $per_file;
}
unset($x);
// TODO: sort files
}
/*
// TODO: now, seems ok, but we need a functinal test
// with more combinations:
// We have: GGBBBBG
// We need B...G...B and others
*/
$ret = $stat;
break;
}
rg_log_ml('FINAL: ' . print_r($ret, TRUE));
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']));
unset($a['data']); // manually free 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 ($finfo['oversize_diff'] == 1)
$ret .= rg_template('repo/diff_too_big.html',
$finfo, TRUE /*xss*/);
else 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 file '
. '<b>' . rg_xss_safe($finfo['file_from']) . '</b>';
else if (strstr($finfo['flags'], "R"))
$ret .= 'File <b>' . $f . '</b> renamed from '
. '<b>' . rg_xss_safe($finfo['file_from']) . '</b>';
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 .= "</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
* @hash - commit hash
* @a = rg_git_log[0]['files']
*/
function rg_git_files_stats($hash, $a, $dir)
{
$t = array();
foreach ($a as $index => $info) {
$line = array();
$line['hash'] = $hash;
$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;"
. " Do a fetch and re-base before pushing.");
}
// 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.");
$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']
);
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);
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:'
. ' refs: ' . rg_array2string($refs)
. ' 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']));
unset($a['data']); // manually free 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['vars']['sha1'], $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);
}
?>