<?php
require_once(__DIR__ . '/util2.inc.php');
require_once(__DIR__ . '/log.inc.php');
require_once(__DIR__ . '/prof.inc.php');
require_once(__DIR__ . '/events.inc.php');
$rg_git_patch_limit_default = 5000;
if (!isset($rg_git_debug))
$rg_git_debug = 0;
define('RG_GIT_ZERO', '0000000000000000000000000000000000000000');
define('RG_GIT_ZERO_SHA256', '0000000000000000000000000000000000000000000000000000000000000000');
// generated by 'git hash-object -t tree /dev/null'
define('RG_GIT_EMPTY', '4b825dc642cb6eb9a060e54bf8d69288fbee4904');
define('RG_GIT_EMPTY_SHA256', '6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321');
define('GIT_LINK_MASK', intval(base_convert('160000', 8, 10)));
define('RG_GIT_HASH_LEN', 10);
define ('RG_GIT_CMD', 'git -c gc.auto=0');
$rg_git_error = "";
function rg_git_set_error($str)
{
global $rg_git_error;
$rg_git_error = $str;
rg_log('git_set_error: ' . $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 1 if a repo is empty, 0 if not, -1 on error
*/
function rg_git_repo_is_empty($repo_path)
{
if (empty($repo_path))
$repo_path = '.';
if (file_exists($repo_path . '/.git'))
$repo_path .= '/.git';
if (!file_exists($repo_path . '/refs/heads'))
return 1;
$scan = glob($repo_path . '/refs/heads/*');
if ($scan === FALSE) {
rg_internal_error('glob returned false');
return -1;
}
if (empty($scan))
return 1;
rg_log_debug('scan: ' . print_r($scan, TRUE));
return 0;
}
/*
* Returns the type of the repo (sha1 or sha256)
*/
function rg_git_repo_get_hash($repo_path)
{
$f = $repo_path . '/rocketgit/hash';
if (!file_exists($f))
return 'sha1';
$hash = @file_get_contents($f);
if ($hash === FALSE)
return '';
return trim($hash);
}
/*
* Returns true if the ref is present in the repo
*/
function rg_git_ref_exists($repo_path, $ref)
{
if (empty($repo_path))
$repo_path = '.';
if (file_exists($repo_path . '/.git'))
$repo_path .= '/.git';
if (file_exists($repo_path . '/refs/heads/' . $ref))
return TRUE;
return FALSE;
}
/*
* Locks a repo agains concurrent updates
* @timeout - in seconds
*/
function rg_git_lock($repo_path, $timeout)
{
global $rg_git_lock;
rg_prof_start('git_lock');
$ret = FALSE;
while (1) {
$f = @fopen($repo_path . '/rocketgit/rg_lock', 'w');
if ($f === FALSE) {
rg_git_set_error('cannot lock repo (open)');
break;
}
$_s = time();
$_exit = FALSE;
while (1) {
$r = @flock($f, LOCK_EX | LOCK_NB, $would_block);
if ($r === TRUE)
break;
if ($would_block != 1)
rg_git_set_error('cannot lock repo (flock)');
$_exit = TRUE;
break;
$_now = time();
if ($_now > $_s + $timeout) {
$_exit = TRUE;
break;
}
sleep(1);
}
if ($_exit) {
fclose($f);
break;
}
$rg_git_lock[$repo_path] = $f;
$ret = TRUE;
break;
}
return $ret;
}
/*
* Unlocks a repo
*/
function rg_git_unlock($repo_path)
{
global $rg_git_lock;
if (!isset($rg_git_lock[$repo_path]))
return;
$f = $rg_git_lock[$repo_path];
@flock($f, LOCK_UN);
fclose($f);
}
/*
* 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 functional tests
if (isset($rg_git_patch_limit_force))
return $rg_git_patch_limit_force;
$r = rg_state_get_uint($db, 'git_patch_limit');
if (($r === FALSE) || ($r === 0))
return $rg_git_patch_limit_default;
return $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;
}
/*
* 'main' -> 'refs/heads/main'
* 'refs/heads/main' -> same as above
*/
function rg_git_name2ref($name)
{
if (strpos($name, '/') !== FALSE)
return $name;
return 'refs/heads/' . $name;
}
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)
{
if (empty($from) && empty($to)) {
rg_log_debug('from/to empty');
$from_to = '';
} else if (empty($from)) {
rg_log_debug('from empty');
$from_to = $to;
} else if (strcmp($from, RG_GIT_ZERO) == 0) {
rg_log_debug('from zero');
$from_to = RG_GIT_EMPTY . '..' . $to;
} else if (strcmp($from, RG_GIT_ZERO_SHA256) == 0) {
rg_log_debug('from zero 256');
$from_to = RG_GIT_EMPTY_SHA256 . '..' . $to;
} else if (empty($to)) {
rg_log_debug('to empty');
$from_to = $from;
} else {
$from_to = $from . '..' . $to;
}
return $from_to;
}
/*
* Quotes a path name (see 'man git-config' and search for 'core.quotePath')
*/
function rg_git_quote($s)
{
$ret = '';
$len = strlen($s);
for ($i = 0; $i < $len; $i++) {
$b = ord($s[$i]);
if (($b <= 6) || (($b >= 0x0e) && ($b <= 0x1f)) || ($b >= 0x7f)) {
$ret .= chr(0x5c) . sprintf('%o', $b);
continue;
}
switch ($b) {
case 0x07: /* \a */ $ret .= chr(0x5c) . 'a'; break;
case 0x08: /* \b */ $ret .= chr(0x5c) . 'b'; break;
case 0x09: /* \t */ $ret .= chr(0x5c) . 't'; break;
case 0x0a: /* \n */ $ret .= chr(0x5c) . 'n'; break;
case 0x0b: /* \v */ $ret .= chr(0x5c) . 'v'; break;
case 0x0c: /* \f */ $ret .= chr(0x5c) . 'f'; break;
case 0x0d: /* \r */ $ret .= chr(0x5c) . 'r'; break;
case 0x22: /* " */ $ret .= chr(0x5c) . '"'; break;
case 0x5c: /* \ */ $ret .= chr(0x5c) . chr(0x5c); break;
default: $ret .= $s[$i];
}
}
return $ret;
}
/*
* 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("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/] (" . rg_php_err() . ").");
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, $branch, $hash)
{
rg_prof_start("git_init");
rg_log_enter('git_init: dst=' . $dst . ' branch=' . $branch
. ' hash=' . $hash);
$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] (" . rg_php_err() . ")");
break;
}
}
// TODO: What to do if the creation fails?
if (!is_dir($dst . "/rocketgit")) {
$dst2 = $dst . '.tmp';
if (strcmp($hash, 'sha256') == 0)
$hash_add = ' --object-format=sha256';
else
$hash_add = '';
$cmd = RG_GIT_CMD . ' init --bare'
. ' --initial-branch=' . escapeshellarg($branch)
. $hash_add
. ' ' . escapeshellarg($dst2);
$a = rg_exec($cmd, '', FALSE, FALSE, FALSE);
if ($a['ok'] != 1) {
rg_internal_error('git init: ' . $a['stderr']);
rg_git_set_error("error on init " . $a['errmsg'] . ")");
break;
}
if (!@mkdir($dst2 . '/rocketgit')) {
rg_git_set_error('cannot create [' . $dst . '/rocketgit]'
. ' dir: ' . rg_php_err());
break;
}
$r = @file_put_contents($dst2 . '/rocketgit/hash', $hash);
if ($r === FALSE) {
rg_git_set_error('cannot create [' . $dst
. '/rocketgit/hash] file (' . rg_php_err() . ')');
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)
{
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] (" . rg_php_err() . ")");
break;
}
}
if (!file_exists($dst . "/rocketgit")) {
$cmd = RG_GIT_CMD . "clone --bare " . escapeshellarg($src)
. " " . escapeshellarg($dst);
$a = rg_exec($cmd, '', FALSE, 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: ' . rg_php_err());
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)
{
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;
}
if (strcmp($obj, RG_GIT_ZERO_SHA256) == 0) {
$ret = 'zero';
break;
}
$cmd = RG_GIT_CMD . ' cat-file -t ' . escapeshellarg($obj);
$a = rg_exec($cmd, '', FALSE, 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 = RG_GIT_CMD . ' cat-file -p ' . escapeshellarg($obj);
$a = rg_exec($cmd, '', FALSE, 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' string
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;
}
$allowed_regexp = '-a-zA-Z0-9_.\/\p{L}\p{N}';
$r = rg_chars_allow($refname, $allowed_regexp, $invalid);
if ($r !== 1) {
rg_git_set_error('we do not accept [' . $invalid
. '] 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 = RG_GIT_CMD . ' rev-parse --verify ' . escapeshellarg($rev);
$a = rg_exec($cmd, '', FALSE, 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)
{
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;
else if (strcmp($old, RG_GIT_ZERO_SHA256) == 0)
$old = RG_GIT_EMPTY_SHA256;
$cmd = RG_GIT_CMD . " diff --check"
. " " . escapeshellarg($old)
. " " . escapeshellarg($new);
$a = rg_exec($cmd, '', FALSE, 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/REF
*/
function rg_git_load_ref($repo_path, $ref)
{
$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 (FALSE on error)
* TODO: Unit testing
*/
function rg_git_merge_base($repo_path, $refname_or_hash, $b)
{
rg_prof_start('git_merge_base');
rg_log_enter('git_merge_base' . ' refname_or_hash=' . $refname_or_hash
. ' b=' . $b);
$ret = FALSE;
while (1) {
$r = rg_git_repo_is_empty($repo_path);
if ($r === -1)
break;
if ($r === 1) {
rg_log('DEBUG_MERGE: repo is empty. Return rg_git_empty.');
$hash = rg_git_repo_get_hash($repo_path);
if (empty($hash))
break;
if (strcmp($hash, 'sha1') == 0)
$ret = RG_GIT_EMPTY;
else
$ret = RG_GIT_EMPTY_SHA256;
break;
}
$head = rg_git_load_ref($repo_path, $refname_or_hash);
rg_log('DEBUG_MERGE: head=' . $head);
if ($head === FALSE)
break;
if (!rg_git_ref_exists($repo_path, $refname_or_hash))
rg_log('DEBUG_MERGE: ref does not exists, probably is a hash');
// TODO: we cannot use the caching because refname_or_hash
// can be a refname which is a moving target.
if (0) {
if (!empty($repo_path)) {
// TODO: why do we use escape here?!
$key = 'git'
. '::' . sha1($repo_path)
. '::' . 'merge-base'
. '::' . escapeshellarg($head)
. '::' . escapeshellarg($b);
$r = rg_cache_get($key);
if ($r !== FALSE) {
$ret = $r;
break;
}
}
}
if (empty($repo_path))
$add = '';
else
$add = ' --git-dir=' . escapeshellarg($repo_path);
$cmd = RG_GIT_CMD
. $add
. ' merge-base'
. ' ' . escapeshellarg($refname_or_hash)
. ' ' . escapeshellarg($b);
$r = rg_exec($cmd, '', FALSE, FALSE, FALSE);
if ($r['ok'] != 1) {
rg_git_set_error('error on git merge-base ('
. $r['errmsg'] . ')');
break;
}
$ret = trim($r['data']);
if (0) {
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
* Returns FALSE on error, TRUE on success.
* 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 = RG_GIT_CMD
. ' --git-dir=' . escapeshellarg($repo_path)
. ' update-ref';
if (!empty($reason))
$cmd .= " -m " . escapeshellarg($reason);
$ref = rg_git_name2ref($ref);
if (empty($new))
$cmd .= " -d " . escapeshellarg($ref);
else
$cmd .= " " . escapeshellarg($ref)
. " " . escapeshellarg($new);
if (!empty($old))
$cmd .= " " . escapeshellarg($old);
$r = rg_exec($cmd, '', FALSE, FALSE, FALSE);
if ($r['ok'] != 1) {
rg_git_set_error(
'error on update-ref (' . $r['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 = RG_GIT_CMD . ' shortlog'
. ' --git-dir=' . escapeshellarg($repo_path)
. ' ' . escapeshellarg($a)
. '..' . escapeshellarg($b);
$r = rg_exec($cmd, '', FALSE, 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('git_ls_tree: repo_path=' . $repo_path
. ' tree=' . $tree . ' path=' . $path);
$ret = FALSE;
while (1) {
$r = rg_git_repo_is_empty($repo_path);
if ($r === -1)
break;
if ($r === 1) {
$ret = array();
break;
}
$op = " ";
if (empty($tree)) {
$op = " --full-tree";
$tree = " HEAD";
}
$cmd = RG_GIT_CMD . ' --git-dir=' . escapeshellarg($repo_path)
. ' ls-tree -z --long' . $op
. escapeshellarg($tree);
if (!empty($path))
$cmd .= ' ' . escapeshellarg($path);
$a = rg_exec($cmd, '', FALSE, FALSE, FALSE);
if ($a['ok'] != 1) {
rg_git_set_error("error on ls-tree (" . $a['errmsg'] . ")");
break;
}
if (empty($a['data']) && !empty($path)) {
rg_log_debug('a[data] is empty; a: ' . print_r($a, TRUE));
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("\0", $a['data']);
unset($a['data']); // manually free data
foreach ($output as $line) {
// last chunk is empty
if (empty($line))
break;
//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_debug('ls-tree: ' . print_r($ret, TRUE));
rg_log_exit();
rg_prof_end("git_ls_tree");
return $ret;
}
/*
* Extracts a maybe quoted file name from a string
* Returns the string extracting, updating also @s.
*/
function rg_git_parse_file_name(&$s)
{
/* we need to remove the space before '["]b/' */
if (strncmp($s, ' ', 1) == 0)
$s = substr($s, 1);
if (strncmp($s, '"', 1) == 0) {
$quoted = TRUE;
$s = substr($s, 3); /* skip '"a|b/' */
} else {
$quoted = FALSE;
$s = substr($s, 2); /* skip 'a|b/' */
}
$ret = '';
$rest = strlen($s);
while (1) {
if ($quoted === FALSE) {
if (empty($s))
break;
if (strncmp($s, ' b/', 3) == 0)
break;
if (strncmp($s, ' "b/', 4) == 0)
break;
$ret .= $s[0];
$s = substr($s, 1); $rest--;
continue;
}
/* From now on, $quoted is TRUE */
if (empty($s)) {
rg_log_debug('ret=' . $ret);
rg_git_set_error('unterminated quoted file name');
$ret = FALSE;
break;
}
$c = $s[0]; $nc = ord($c); $s = substr($s, 1); $rest--;
if ($nc == 0x5C) { /* '\' */
if ($rest == 0) {
rg_git_set_error('invalid diff line: unfinished escape');
$ret = FALSE;
break;
}
/* Decoding '\xxx' strings */
//rg_log_debug('s: ' . $s);
if ($rest >= 3) {
$all_digits = TRUE;
$nr = 0; $f = 64;
for ($i = 0; $i < 3; $i++) {
$z = ord($s[$i]);
if (($z < 0x30) || ($z > 0x37)) {
$all_digits = FALSE;
break;
}
$nr += ($z - 0x30) * $f;
$f /= 8;
}
if ($all_digits) {
$s = substr($s, 3); $rest -= 3;
$ret .= chr($nr);
continue;
}
}
$c = ord($s[0]); $s = substr($s, 1); $rest--;
switch ($c) {
case 0x61: $ret .= chr(0x07); break; /* \a */
case 0x62: $ret .= chr(0x08); break; /* \b */
case 0x74: $ret .= chr(0x09); break; /* \t */
case 0x6e: $ret .= chr(0x0a); break; /* \n */
case 0x76: $ret .= chr(0x0b); break; /* \v */
case 0x66: $ret .= chr(0x0c); break; /* \f */
case 0x72: $ret .= chr(0x0d); break; /* \r */
case 0x5c: $ret .= chr(0x5c); break; /* \\ */
case 0x22: $ret .= chr(0x22); break; /* " */
}
} else if ($nc == 0x22) { /* '"' */
break;
} else {
$ret .= $c;
}
}
return $ret;
}
/*
* Extracts the two file names from a 'diff --git a/x b/x' line
* Only one of a file may be quoted (example: diff --git a/a b c "b/\\a\\b").
* Return FALSE on error.
*/
function rg_git_split_diff_file_names($s)
{
global $rg_git_debug;
rg_log_enter('git_split_diff_file_names: s=' . $s);
$ret = array();
for ($i = 0; $i <= 1; $i++) {
$r = rg_git_parse_file_name($s);
if ($r === FALSE) {
$ret = FALSE;
break;
}
$ret[$i] = $r;
}
if ($rg_git_debug > 90)
rg_log_debug('git_split_diff_file_names: ret: '
. print_r($ret, TRUE));
rg_log_exit();
return $ret;
}
/*
* Transforms a diff into an array (ready for rg_git_diff)
* @out - will come populated (by rg_git_log_simple) with an array indexed by
* file name: flags, lines_add, lines_del, changes, oversize_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_enter('git_diff2array');
if ($rg_git_debug > 90) {
rg_log_debug('git_diff2array: diff: ' . $diff);
rg_log_debug('git_diff2array: out: ' . print_r($out, TRUE));
}
$ret = TRUE;
$lines = explode("\n", $diff);
//if ($rg_git_debug > 90) {
// rg_log_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);
$files = rg_git_split_diff_file_names($rest);
if ($files === FALSE) {
$ret = FALSE;
break;
}
$a['file_from'] = $files[0];
$a['file'] = $files[1];
$a['index'] = '';
$a['chunks'] = array();
$file = $a['file'];
if (!isset($out[$file])) {
rg_log_debug('out: ' . print_r($out, TRUE));
rg_log_debug('line=' . $line);
rg_git_set_error('internal error');
rg_internal_error('we have a diff for a'
. ' non-existing file (' . $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_log_debug('diff: ' . print_r($diff, TRUE));
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_log_exit();
rg_prof_end("git_diff2array");
return $ret;
}
/*
* Returns 1 if a range of commits contain at least a merge,
* 0 if not, -1 on error.
*/
function rg_git_log_has_merges($repo_path, $from, $to)
{
global $rg_git_debug;
rg_prof_start('git_log_has_merges');
rg_log_enter('git_log_has_merges: repo_path=' . $repo_path
. ' from=' . $from . ' to=' . $to);
$ret = -1;
while (1) {
$from_to = rg_git_from_to($from, $to);
$cmd = RG_GIT_CMD . ' --no-pager'
. ' --git-dir=' . escapeshellarg($repo_path)
. ' log'
. ' --merges --oneline --max-count=1';
if (!empty($from_to))
$cmd .= ' ' . escapeshellarg($from_to);
$a = rg_exec($cmd, '', FALSE, FALSE, FALSE);
if ($a['ok'] != 1) {
rg_internal_error("error on log (" . $a['errmsg'] . ")"
. " cmd=" . $cmd);
rg_git_set_error('could not generate log; try again later');
break;
}
$rg_git_debug && rg_log('a:' . rg_array2string($a));
if (empty($a['data'])) {
$ret = 0;
break;
}
$ret = 1;
break;
}
rg_log_exit();
rg_prof_end('git_log_has_merges');
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) {
$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 = RG_GIT_CMD . " --no-pager"
. " --git-dir=" . escapeshellarg($repo_path)
. ' -c core.quotePath=false' // this is about the diff (-z counts in numstat)
. " log"
. " --find-copies"
. " --find-renames"
. ' --find-copies-harder'
. " --no-merges"
. ' --numstat'
. " -z"
. $max_count
. $patches
. " --pretty=\"format:"
. $sep_start
. "hash:%H%x00\"\""
. "hash_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, FALSE);
if ($a['ok'] != 1) {
rg_log_debug('stderr: ' . $a['stderr']);
rg_internal_error("error on log (" . $a['errmsg'] . ")"
. " cmd=" . $cmd);
rg_git_set_error("could not generate log; try again later");
break;
}
if ($rg_git_debug > 95) {
rg_log_enter('DEBUG: OUTPUT OF GIT LOG START');
rg_log_ml($a['data']);
rg_log_exit();
}
$blocks = explode($sep_start, $a['data']);
unset($a['data']); // manually free memory
// because data starts with the separator, we remove it
unset($blocks[0]);
if ($rg_git_debug > 94) {
rg_log_enter('DEBUG: blocks:');
rg_log_ml(print_r($blocks, TRUE));
rg_log_exit();
}
$ret = array();
foreach ($blocks as $junk => $block) {
if ($rg_git_debug > 90)
rg_log_debug('block: ' . print_r($block, TRUE));
$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
if ($rg_git_debug > 90)
rg_log_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
if (isset($y['vars']['author date']))
$y['vars']['author date UTC'] = gmdate("Y-m-d H:i:s", $y['vars']['author date']);
if (isset($y['vars']['committer 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;
}
if ($rg_git_debug > 90)
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_debug('n_d: ' . print_r($n_d, TRUE));
// numstat
$numstat = explode("\0", trim($n_d[0]));
//rg_log_debug('numstat: ' . print_r($numstat, TRUE));
$tc = count($numstat);
while ($tc > 0) {
$a = explode("\t", array_shift($numstat), 3); $tc--;
//rg_log_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])
);
$changes = $y['files'][$f]['lines_add']
+ $y['files'][$f]['lines_del'];
$y['files'][$f]['changes'] = $changes;
// We will mark over sized diffs for later use
//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_debug('files: ' . print_r($y['files'], TRUE));
if ($also_patch === FALSE) {
$ret[] = $y;
continue;
}
if (!isset($n_d[1])) {
// Can happen if the diff is empty.
$ret = array();
break;
}
// now, patch, if present
$r = rg_git_diff2array($n_d[1], $y['files']);
if ($r === FALSE) {
$ret = FALSE;
break;
}
//rg_log_debug('diff2array: ' . print_r($y['files'], TRUE));
//rg_log_debug('y: ' . print_r($y, TRUE));
$ret[] = $y;
}
break;
}
if ($rg_git_debug > 90)
rg_log_debug('simple: ret: ' . print_r($ret, TRUE));
rg_log_exit();
rg_prof_end('git_log_simple');
return $ret;
}
/*
* Works on git_log (without patch) output to detect big diffs.
* Returns an array with all the info needed to prepare a new 'git log'.
* Will return an empty array if normal log can be called.
* It returns contiguous ranges of good commits and separate bad commits, with what files are good.
*/
function rg_git_log_detect_big_diff($stat)
{
global $rg_git_debug;
rg_log_enter('git_log_detect_big_diff');
if ($rg_git_debug > 90)
rg_log_debug('input(stat): ' . print_r($stat, TRUE));
$ret = array();
$at_least_one_bad = FALSE;
foreach ($stat as $junk => $per_commit) {
$hash = $per_commit['vars']['hash'];
$good_files = array();
$cur = 'good';
foreach ($per_commit['files'] as $fname => $i) {
if ($i['oversize_diff']) {
$cur = 'bad';
$at_least_one_bad = TRUE;
} else {
$good_files[] = $fname;
}
}
if (empty($ret)) {
$pos = 0;
} else {
$last = $ret[$pos]['type'];
if ($rg_git_debug > 90)
rg_log_debug('pos=' . $pos . ' last=' . $last
. ' cur=' . $cur . ' hash='
. substr($ret[$pos]['hash'], 0, 6)
. ' range=' . $ret[$pos]['range']);
if (strcmp($cur, 'good') == 0) {
$ret[$pos]['range']++;
$ret[$pos]['hash'] = $hash;
continue;
}
$pos++;
}
$ret[$pos] = array(
'hash' => $hash,
'range' => 1,
'type' => $cur
);
if (strcmp($cur, 'bad') == 0)
$ret[$pos]['good_files'] = $good_files;
}
// No need to parse the array
if ($at_least_one_bad === FALSE) {
rg_log_debug('No big diff detected => return empty array');
$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)
{
global $rg_git_debug;
rg_prof_start('git_log');
rg_log_enter('git_log: repo_path=' . $repo_path . ' from=' . $from
. ' to=' . $to .' max=' . $max . ' also_patch=' . ($also_patch ? 'Y' : 'N'));
$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);
if ($stat === FALSE) {
rg_log_debug('git_log_simple returned FALSE');
break;
}
// 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;
}
if ($rg_git_debug > 90)
rg_log_debug('log_simple returned: stat: ' . print_r($stat, TRUE));
$r = rg_git_log_detect_big_diff($stat);
if (empty($r)) { // = no big diff
rg_log_debug('No big diff, call git_log_simple and return');
$good_files = array(); // = all
$ret = rg_git_log_simple($repo_path, $max, $from, $to,
$also_patch, $good_files, $patch_limit);
break;
}
//if ($rg_git_debug > 90)
// rg_log_debug('git_log_detect_big_diff returned ' . count($r) . ' entries');
foreach ($r as $index => $i) {
if ($rg_git_debug > 90)
rg_log_debug('Generating log for index '
. $index . ': ' . print_r($i, TRUE));
if (strcmp($i['type'], 'good') == 0)
$_files = array();
else if (!empty($i['good_files']))
$_files = $i['good_files'];
else
continue;
// TODO: do we signal in web that a patch is big?
// TODO: it is not really ok to call 'git log' n times! How to optimize this?
// TODO: maybe join togheter a continuous range of good commits.
// TODO: it is true that this is called only if there are oversized commits.
$x = rg_git_log_simple($repo_path, $i['range'],
'' /*from*/, $i['hash'], $also_patch, $_files,
$patch_limit);
if ($x === FALSE) {
$stat = FALSE;
break;
}
//rg_log_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 (B = Big, G = good):
// We have: GGBBBBG
// We need B...G...B and others
*/
$ret = $stat;
break;
}
//if ($rg_git_debug > 90)
// rg_log_ml('DEBUG 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)
{
rg_prof_start("git_files");
rg_log_enter('git_files old=' . $old . ' new=' . $new);
$ret = FALSE;
while (1) {
if (strcmp($old, RG_GIT_ZERO) == 0)
$old = RG_GIT_EMPTY;
else if (strcmp($old, RG_GIT_ZERO_SHA256) == 0)
$old = RG_GIT_EMPTY_SHA256;
$cmd = RG_GIT_CMD . ' diff --name-only '
. ' ' . escapeshellarg($old)
. ' ' . escapeshellarg($new);
$a = rg_exec($cmd, '', FALSE, FALSE, FALSE);
if ($a['ok'] != 1) {
rg_git_set_error("error on git diff (" . $a['errmsg'] . ")");
rg_log_debug('stderr: ' . $a['stderr']);
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 hash; 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)
{
global $rg_git_debug;
rg_prof_start("git_diff");
rg_log_debug('git_diff: id=' . $id . ' 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) {
if ($rg_git_debug > 90)
rg_log_debug('finfo: ' . print_r($finfo, TRUE));
$v = rg_visible_string($finfo['file']);
$f = rg_xss_safe($v);
$ret .= '<table id="file-' . $id . '-' . sha1($finfo['file']) . '" 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_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>"
. rg_xss_safe($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)
{
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['misc'] = $a['refname'];
$history = array('ri' => array(), 'ui_login' => array());
$history['ri']['repo_id'] = $a['repo_id'];
$history['ui_login']['uid'] = $a['login_uid'];
if (strcmp($a['new_rev_type'], "tag") == 0) { // Annotated
if ((strcmp($a['old_rev'], RG_GIT_ZERO) == 0)
|| (strcmp($a['old_rev'], RG_GIT_ZERO_SHA256) == 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)
|| (strcmp($a['new_rev'], RG_GIT_ZERO_SHA256) == 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)
|| (strcmp($a['old_rev'], RG_GIT_ZERO_SHA256) == 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)
|| (strcmp($a['new_rev'], RG_GIT_ZERO_SHA256) == 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_login']['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)
{
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['misc'] = $a['refname'];
$history = array('ri' => array(), 'ui_login' => array());
$history['ri']['repo_id'] = $a['repo_id'];
$history['ui_login']['uid'] = $a['login_uid'];
if ((strcmp($a['new_rev'], RG_GIT_ZERO) == 0)
|| (strcmp($a['new_rev'], RG_GIT_ZERO_SHA256) == 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 (was ' . $a['old_rev'] . ')';
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)
|| (strcmp($a['old_rev'], RG_GIT_ZERO_SHA256) == 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_log_debug('merge_base=' . $merge_base);
rg_log_debug('merge_base != old_rev [' . $a['old_rev'] . ']');
rg_git_fatal($a['refname']
. "\nYou have no rights to do a non fast-forward push;"
. " Do a merge or re-base before pushing.");
}
}
// Check if user pushes a merge commit
$x = $_x;
$x['needed_rights'] = 'M';
if (rg_rights_allow($db, $x) !== TRUE) {
$r = rg_git_log_has_merges($a['repo_path'],
$a['old_rev'], $a['new_rev']);
if ($r == -1) {
rg_git_fatal($a['refname'] . "\n" . rg_git_error());
} else if ($r == 1) {
rg_git_fatal($a['refname']
. "\nYou have no rights to push merges."
. ' Do a rebase.');
}
}
// 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('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['source'] = 'git_update_branch';
$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'] = 'repo_event_push';
$ev['source'] = 'git_update_branch';
$ev['prio'] = 50;
$ev['ri'] = array(
'repo_id' => $a['repo_id'],
'repo_uid' => $a['repo_uid'],
'name' => $a['repo_name'],
'repo_username' => $a['repo_username'],
'url' => rg_base_url($db, '', '') . $a['login_url']
. '/' . rawurlencode($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)
|| (strcmp($a['old_rev'], RG_GIT_ZERO_SHA256) == 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'];
}
}
// TODO: move this to event to make push faster.
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)
{
rg_log('git_ref_valid: type=' . $type . ' ref=' . $ref);
if (strcmp($type, 'commit') == 0)
return TRUE;
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('DEBUG: current=' . $current);
$_l = array();
foreach ($refs as $o => $list) {
if (empty($list))
continue;
foreach ($list as $name) {
// TODO: escape ','!
$ename = str_replace('/', ',', $name);
$name = rg_xss_safe($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'
. '/' . rawurlencode($o)
. '/' . rawurlencode($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 if (strcmp($paras[0], 'commit') == 0) {
$ret['ref_type'] = 'commit';
$ret['ref_path'] = $paras[1];
$ret['ref_val'] = $paras[1]; // TODO: don't know why this is needed. accessing /source/log/commit needs this
$ret['ref_url'] = 'commit/' . rawurlencode($paras[1]); // TODO: same as above
return $ret;
} else {
return $ret;
}
array_shift($paras);
$v = empty($paras) ? '' : array_shift($paras);
$val = trim($v);
$ret['ref_url'] = rawurlencode($ret['ref_type'])
. '/' . rawurlencode($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('git_diff_tree: tree1=' . $tree1 . ' tree2=' . $tree2);
$ret = FALSE;
while (1) {
$cmd = RG_GIT_CMD . " diff-tree -r " . escapeshellarg($tree1)
. " " . escapeshellarg($tree2);
$a = rg_exec($cmd, '', FALSE, 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 = RG_GIT_CMD . ' show ' . escapeshellarg($treeish) . ':'
. escapeshellarg($file);
$a = rg_exec($cmd, '', FALSE, 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)
{
rg_log_enter('git_log2listing');
$ret = '';
while (1) {
if ($log === FALSE) {
$ret = rg_template('repo/err/not_init.html', $rg, TRUE /*xss*/);
break;
}
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'])
. "#hash-" . rg_xss_safe($i['vars']['hash']);
$rg['HTML:commit_table'] = rg_git_log_template($log,
'repo/log', $rg);
} else {
$rg['HTML:commit_table'] = '';
}
foreach ($log as $junk => &$i) {
//rg_log_debug('i=' . print_r($i, TRUE));
// Some info about commit
if (!empty($i['vars']['body']))
$i['vars']['HTML:x_body']
= '<div class="commit_body">'
. nl2br(rg_xss_safe(trim($i['vars']['body'])))
. '</div>';
else
$i['vars']['HTML:x_body'] = '';
$i['vars']['HTML:x_author date']
= gmdate("Y-m-d H:i", $i['vars']['author date']);
if (!empty($i['vars']['committer name']))
$i['vars']['x_committer name']
= $i['vars']['committer name'];
else
$i['vars']['HTML:x_committer name'] = '?';
$i['vars']['HTML:x_committer date']
= gmdate("Y-m-d H:i", $i['vars']['committer date']);
// stats
$r = rg_git_files_stats($i['vars']['hash'], $i['files'],
'repo/fstat');
if ($r === FALSE)
$i['HTML:x_stats'] = rg_template('repo/err/stats.html', $rg, TRUE /*xss*/);
else
$i['HTML:x_stats'] = $r;
// diff
//rg_log_debug('i[files]=' . print_r($i['files'], TRUE));
$r = rg_git_diff($i['vars']['hash'], $i['files'],
'repo/diff.html');
if ($r === FALSE)
$i['HTML:x_diff'] = rg_template('repo/err/diff.html', $rg, TRUE /*xss*/);
else
$i['HTML:x_diff'] = $r;
}
break;
}
$ret .= rg_template_table('repo/commits', $log, $rg);
rg_log_exit();
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 = RG_GIT_CMD . ' --git-dir=' . escapeshellarg($repo_path)
. ' archive --format=' . escapeshellarg($format)
. ' --output=' . escapeshellarg($archive_name)
. ' ' . escapeshellarg($treeish);
$a = rg_exec($cmd, '', FALSE, 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)
{
rg_prof_start('git_merge_tree');
rg_log_enter('git_merge_tree base='
. $base . ' a=' . $a . ' b=' . $b);
$ret = FALSE;
while (1) {
$r = rg_git_repo_is_empty($repo_path);
if ($r === -1)
break;
if ($r === 1) {
$hash = rg_git_repo_get_hash($repo_path);
if (empty($hash))
break;
if (strcmp($hash, 'sha1') == 0)
$a = RG_GIT_EMPTY;
else
$a = RG_GIT_EMPTY_SHA256;
}
$head = rg_git_load_ref($repo_path, $a);
if ($head === FALSE)
break;
if (!empty($repo_path)) {
$key = 'git'
. '::' . sha1($repo_path)
. '::' . 'merge-tree'
. '::' . $head . '::' . $b;
$r = rg_cache_get($key);
if ($r !== FALSE) {
$ret = $r;
break;
}
}
$cmd = RG_GIT_CMD . ' --git-dir=' . escapeshellarg($repo_path)
. ' merge-tree'
. ' ' . escapeshellarg($base)
. ' ' . escapeshellarg($head)
. ' ' . escapeshellarg($b);
$a = rg_exec($cmd, '', FALSE, FALSE, FALSE);
if ($a['ok'] != 1) {
rg_git_set_error('error on git merge-tree ('
. $a['errmsg'] . ')');
break;
}
if (!empty($repo_path)) {
$ret = trim($a['data']);
rg_cache_set($key, $ret, RG_SOCKET_NO_WAIT);
}
rg_log_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 merge 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 in a bare repo
* @ff - 0 = fast-forward not allowed, 1 = fast-forward allowed
* @msg - merge message
* Returns TRUE or FALSE
* Thanks to https://stackoverflow.com/questions/7984986/git-merging-branches-in-a-bare-repository
* TODO: we may prepend to @msg some more info (mr id etc.)
* TODO: it seems we need to do this in the caller!
*/
function rg_git_merge($repo_path, $ref_name, $new, $ff, $msg)
{
rg_prof_start('git_merge');
rg_log_enter('git_merge' . ' ref_name=' . $ref_name
. ' new=' . $new . ' ff=' . $ff . ' msg=[' . $msg . ']');
$ret = FALSE;
while (1) {
$r = rg_git_repo_is_empty($repo_path);
if ($r === -1)
break;
if ($r === 1) {
$hash = rg_git_repo_get_hash($repo_path);
if (empty($hash))
break;
if (strcmp($hash, 'sha1') == 0)
$ref_name_tmp = RG_GIT_EMPTY;
else
$ref_name_tmp = RG_GIT_EMPTY_SHA256;
} else {
$ref_name_tmp = $ref_name;
}
$r = rg_git_lock($repo_path, 60);
if ($r === FALSE)
break;
@unlink($repo_path . '/index');
if (file_exists($repo_path . '/index')) {
rg_git_set_error('cannot unlink index');
break;
}
$mb = rg_git_merge_base($repo_path, $ref_name_tmp, $new);
if ($mb === FALSE)
break;
rg_log_debug('merge-base=' . $mb);
$cur = rg_git_load_ref($repo_path, $ref_name_tmp);
if ($cur === FALSE)
break;
rg_log_debug('ref_name points to ' . $cur);
// If repo was empty, we are forced to do a ff
if ((strcmp($mb, RG_GIT_EMPTY) == 0)
|| (strcmp($mb, RG_GIT_EMPTY_SHA256) == 0)) {
rg_log_debug('merge base is empty, allow fast-forward.');
$ff = 1;
}
if (($ff == 1) && (strcmp($mb, $cur) == 0)) {
rg_log_debug('we can do a fast forward...');
$commit = $new;
} else {
rg_log_debug('we must not do a fast forward');
$e_mb = escapeshellarg($mb);
$e_ref_name_tmp = escapeshellarg($ref_name_tmp);
$e_new = escapeshellarg($new);
// TODO: this must go away - the cache will malfunction?
if (empty($repo_path))
$add = '';
else
$add = ' --git-dir=' . escapeshellarg($repo_path);
$cmd = RG_GIT_CMD
. $add . ' read-tree -i -m '
. $e_mb . ' ' . $e_ref_name_tmp . ' ' . $e_new;
$r = rg_exec($cmd, '', FALSE, FALSE, FALSE);
if ($r['ok'] != 1) {
rg_git_set_error('error on merge (read-tree) ('
. $r['errmsg'] . ')');
break;
}
$cmd = RG_GIT_CMD . $add . ' write-tree';
$r = rg_exec($cmd, '', FALSE, FALSE, FALSE);
if ($r['ok'] != 1) {
rg_git_set_error('error on merge (write-tree) ('
. $r['errmsg'] . ')');
break;
}
$tree = trim($r['data']);
$cmd = RG_GIT_CMD . $add . ' commit-tree '
. escapeshellarg($tree)
. ' -p ' . $e_ref_name_tmp . ' -p ' . $e_new
. ' -m ' . escapeshellarg($msg);
$r = rg_exec($cmd, '', FALSE, FALSE, FALSE);
if ($r['ok'] != 1) {
rg_git_set_error('error on merge (commit-tree) ('
. $r['errmsg'] . ')');
break;
}
$commit = trim($r['data']);
}
$r = rg_git_update_ref($repo_path, $ref_name,
'' /*old*/, $commit, $msg);
if ($r !== TRUE)
break;
$ret = TRUE;
break;
}
@unlink($repo_path . '/index');
rg_git_unlock($repo_path);
rg_log_exit();
rg_prof_end('git_merge');
return $ret;
}
/*
* Creates a merge request against a user repo to be sent by e-mail
*/
function rg_git_request_pull($repo_path, $start, $url, $end, $patch)
{
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 = RG_GIT_CMD . ' diff -M --stat --summary';
if ($patch)
$cmd .= ' --patch';
$cmd .= escapeshellarg($start) . '..' . escapeshellarg($end);
$r = rg_exec($cmd, '', FALSE, 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);
}