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