<?php require_once($INC . "/util.inc.php"); require_once($INC . "/log.inc.php"); require_once($INC . "/prof.inc.php"); require_once($INC . "/events.inc.php"); $rg_git_patch_limit_default = 5000; $rg_git_debug = 0; $rg_git_zero = "0000000000000000000000000000000000000000"; $rg_git_empty = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; define('GIT_LINK_MASK', intval(base_convert('160000', 8, 10))); 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_ml('DEBUG: scan: ' . print_r($scan, TRUE)); return 0; } /* * 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) { global $rg_git_zero; global $rg_git_empty; if (empty($from) && empty($to)) { rg_log('from/to empty'); $from_to = ''; } else if (empty($from)) { rg_log('from empty'); $from_to = $to; } else if (strcmp($from, $rg_git_zero) == 0) { rg_log('from zero'); $from_to = $rg_git_empty . '..' . $to; } else if (empty($to)) { rg_log('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) { 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] (" . rg_php_err() . ")"); break; } } // TODO: What to do if the creation fails? if (!is_dir($dst . "/rocketgit")) { $dst2 = $dst . '.tmp'; $cmd = RG_GIT_CMD . ' init --bare ' . escapeshellarg($dst2); $a = rg_exec($cmd, '', FALSE, 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 (" . 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) { 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 = 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) { 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 = 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) { 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 (FALSE on error) * TODO: Unit testing */ function rg_git_merge_base($repo_path, $refname_or_hash, $b) { global $rg_git_zero; global $rg_git_empty; 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.'); $ret = $rg_git_empty; 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("rg_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_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_ml("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_ml('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_ml("DEBUG: git_diff2array: diff: " . $diff); rg_log_ml("DEBUG: git_diff2array: out: " . print_r($out, TRUE)); } $ret = TRUE; $lines = explode("\n", $diff); //if ($rg_git_debug > 90) { // rg_log_ml("DEBUG: lines: " . print_r($lines, TRUE)); foreach ($lines as $line) { if ($rg_git_debug > 90) rg_log("DEBUG: line=$line"); // format: diff --git a/a b/a if (strncmp($line, "diff --git ", 11) == 0) { $a = array(); $a['flags'] = ''; $a['old_mode'] = ''; $a['mode'] = ''; $a['similarity'] = ''; $a['dissimilarity'] = ''; $rest = substr($line, 11); $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_ml('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_ml('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:' . print_r($a, TRUE)); 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 . "sha1:%H%x00\"\"" . "sha1_short:%h%x00\"\"" . "tree:%T%x00\"\"" . "tree_short:%t%x00\"\"" . "parents:%P%x00\"\"" . "parents_short:%p%x00\"\"" . "author name:%aN%x00\"\"" . "author email:%aE%x00\"\"" . "author date:%at%x00\"\"" . "committer name:%cN%x00\"\"" . "committer email:%ce%x00\"\"" . "committer date:%ct%x00\"\"" . "encoding:%e%x00\"\"" . "ref_names:%d%x00\"\"" . "sign_key:%GK%x00\"\"" . "subject:%s%x00\"\"" . "body:%b%x00\"\"" . "notes:%N%x00\"\"" . $sep_end . "\""; if (!empty($from_to)) $cmd .= ' ' . escapeshellarg($from_to); if (!empty($files)) { $cmd .= ' --'; foreach ($files as $f) $cmd .= ' ' . escapeshellarg($f); } $a = rg_exec($cmd, '', FALSE, FALSE, 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; } 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_ml('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_ml('DEBUG: parts: ' . print_r($parts, TRUE)); // vars $y['vars']['lines_add'] = 0; $y['vars']['lines_del'] = 0; $x = explode ("\0", $parts[0]); unset($parts[0]); // manually free data $count = count($x) - 1; // last is empty for ($i = 0; $i < $count; $i++) { $_t = explode(':', $x[$i], 2); if (isset($_t[1])) { $y['vars'][$_t[0]] = $_t[1]; } else if (empty($_t[0])) { // do nothing } else { //rg_log("DEBUG: Var [" . $_t[0] . "] has no value!"); } } // Some additions 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_ml('DEBUG: n_d: ' . print_r($n_d, TRUE)); // numstat $numstat = explode("\0", trim($n_d[0])); //rg_log_ml('DEBUG: numstat: ' . print_r($numstat, TRUE)); $tc = count($numstat); while ($tc > 0) { $a = explode("\t", array_shift($numstat), 3); $tc--; //rg_log_ml('DEBUG: a: ' . print_r($a, TRUE)); // We may have an empty commit if (count($a) == 1) break; if (count($a) != 3) { rg_internal_error('invalid numstat: c=' . count($a) . ' numstat: ' . print_r($n_d[0], TRUE) . '.'); break; } $f = $a[2]; if (empty($f)) { // it is a rename or copy if ($tc < 2) { rg_internal_error('a rename or copy with invalid fields'); rg_internal_error('numstat: ' . print_r($n_d[0], TRUE)); break; } // throw away the source (not needed) array_shift($numstat); $f = array_shift($numstat); $tc -= 2; } $y['files'][$f] = array( 'file' => $f, 'chunks' => array(), 'flags' => '', 'lines_add' => intval($a[0]), 'lines_del' => intval($a[1]) ); $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_ml('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_ml('DEBUG: diff2array: ' . print_r($y['files'], TRUE)); //rg_log_ml('DEBUG: y: ' . print_r($y, TRUE)); $ret[] = $y; } break; } if ($rg_git_debug > 90) rg_log_ml('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 'git log'. * Will return an empty array if normal log should be called. */ function rg_git_log_detect_big_diff($stat, $from) { rg_log_enter('git_log_detect_big_diff from=' . $from); //rg_log_ml('DEBUG: stat: ' . print_r($stat, TRUE)); $ret = array(); $pos = 0; $last_was_good = 2; // 2 = not good or bad $ret[0] = array('from' => $from); $at_least_one_bad = FALSE; foreach ($stat as $junk => $per_commit) { $hash = $per_commit['vars']['sha1']; $good_files = array(); $we_have_bad_files = FALSE; foreach ($per_commit['files'] as $fname => $i) { if ($i['oversize_diff']) { $we_have_bad_files = TRUE; $at_least_one_bad = TRUE; } else { $good_files[] = $fname; } } if ($we_have_bad_files === FALSE) $good_files = array(); // = all //rg_log_enter('DEBUG: hash ' . $hash); //rg_log('DEBUG: we_have_bad_files=' // . ($we_have_bad_files ? 'TRUE' : 'FALSE')); //rg_log('DEBUG: last_was_good=' . $last_was_good); //rg_log_ml('DEBUG: good_files: ' . print_r($good_files, TRUE)); if ($we_have_bad_files) { if ($last_was_good < 2) { rg_log('last_was_good == 0/1'); $ret[$pos]['from'] = $hash; $ret[$pos]['from_to'] = rg_git_from_to($ret[$pos]['from'], $ret[$pos]['to']); $pos++; } $ret[$pos] = array( 'type' => 'bad', 'from' => '', // we do not need it 'to' => $hash, 'good_files' => $good_files ); $ret[$pos]['from_to'] = rg_git_from_to($ret[$pos]['from'], $ret[$pos]['to']); $last_was_good = 0; } else { // we have no bad files if ($last_was_good == 1) { rg_log('last_was_good == 1; do nothing'); } else { if ($last_was_good == 0) { rg_log('last_was_good == 0'); $ret[$pos]['from'] = $hash; $ret[$pos]['from_to'] = rg_git_from_to($ret[$pos]['from'], $ret[$pos]['to']); $pos++; } else { rg_log('last_was_good == 2'); } $ret[$pos]['type'] = 'good'; $ret[$pos]['to'] = $hash; } $last_was_good = 1; } //rg_log_exit(); } // We may not had the chance to set 'from' if (!isset($ret[$pos]['from'])) { $ret[$pos]['from'] = $from; $ret[$pos]['from_to'] = rg_git_from_to($ret[$pos]['from'], $ret[$pos]['to']); } //rg_log_ml('DEBUG final (after detect big diff): ' . print_r($ret, TRUE)); // No need to parse the array if ($at_least_one_bad === FALSE) { rg_log('DEBUG: No big diff detected.'); $ret = array(); } rg_log('DEBUG: Big diff detected.'); 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); $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) 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_ml('DEBUG: stat: ' . print_r($stat, TRUE)); if (empty($from)) { $from = $stat[0]['vars']['sha1']; if ($rg_git_debug > 50) rg_log('DEBUG: overwriting from with ' . $from); } $r = rg_git_log_detect_big_diff($stat, $from); if (empty($r)) { // = no big diff $good_files = array(); // = all $ret = rg_git_log_simple($repo_path, $max, $from, $to, $also_patch, $good_files, $patch_limit); break; } foreach ($r as $i) { rg_log_ml('DEBUG: Generating log for ' . print_r($i, TRUE)); if (strcmp($i['type'], 'good') == 0) $_files = array(); else if (!empty($i['good_files'])) $_files = $i['good_files']; else continue; $x = rg_git_log_simple($repo_path, $max, $i['from'], $i['to'], $also_patch, $_files, $patch_limit); if ($x === FALSE) { $stat = FALSE; break; } //rg_log_ml('DEBUG: x: ' . print_r($x, TRUE)); // Overwrite $stat with the latest info foreach ($x as $hash => $per_hash) { foreach ($per_hash['files'] as $f => $per_file) $stat[$hash]['files'][$f] = $per_file; } unset($x); // TODO: sort files } /* // TODO: now, seems ok, but we need a functinal test // with more combinations: // We have: GGBBBBG // We need B...G...B and others */ $ret = $stat; break; } //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) { 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 = 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'] . ")"); break; } $ret = explode("\n", trim($a['data'])); unset($a['data']); // manually free data break; } rg_log_exit(); rg_prof_end("git_files"); return $ret; } /* * Nice diff per file * Outputs the result of replacing variables in a template with real variables * @id - uniq id, most of the time the commit sha1; used to differentiate * between same files in different commits. * @a - output of rg_git_diff2array[index]['files'] * TODO: Switch to rg_template_table? */ function rg_git_diff($id, $a, $template_file) { 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_ml("DEBUG: finfo: " . print_r($finfo, TRUE)); $v = rg_visible_string($finfo['file']); $f = rg_xss_safe($v); $ret .= '<a name="file-' . $id . '-' . sha1($finfo['file']) . '"></a>' . "\n"; $ret .= "<table class=\"chunk\">\n"; $ret .= "<tr style=\"border: 1px; background: #dddddd\"><td colspan=\"4\">"; if ($finfo['oversize_diff'] == 1) $ret .= rg_template('repo/diff_too_big.html', $finfo, TRUE /*xss*/); else if (strstr($finfo['flags'], "N")) $ret .= "File <b>$f</b> added"; else if (strstr($finfo['flags'], "D")) $ret .= "File <b>$f</b> deleted"; else if (strstr($finfo['flags'], "C")) $ret .= 'File <b>' . $f . '</b> copied from file ' . '<b>' . rg_xss_safe($finfo['file_from']) . '</b>'; else if (strstr($finfo['flags'], "R")) $ret .= 'File <b>' . $f . '</b> renamed from ' . '<b>' . rg_xss_safe($finfo['file_from']) . '</b>'; else $ret .= "File <b>$f</b> changed"; if (!empty($finfo['similarity'])) $ret .= " (similarity " . rg_xss_safe($finfo['similarity']) . ")"; if (!empty($finfo['dissimilarity'])) $ret .= " (dissimilarity " . rg_xss_safe($finfo['dissimilarity']) . ")"; if (!empty($finfo['mode'])) { $ret .= " (mode: " . rg_xss_safe($finfo['mode']); if (!empty($finfo['old_mode'])) $ret .= " -> " . rg_xss_safe($finfo['old_mode']); $ret .= ")"; } if (!empty($finfo['index'])) $ret .= " (index " . rg_xss_safe($finfo['index']) . ")"; // TODO: Before stats we must show commit hash, author etc. (source/log/commit/xxxxxx) // TODO: what about committer and time and rest? $ret .= "</td></tr>\n"; $empty_line = ""; foreach ($finfo['chunks'] as $chunk => $ci) { //rg_log_ml("DEBUG: ci: " . print_r($ci, TRUE)); $ret .= $empty_line; $empty_line = "<tr style=\"border: 1px\"><td colspan=\"4\"> </td></tr>\n"; if (!empty($ci['section'])) { $ret .= "<tr>\n"; $ret .= " <td class=\"numbers\">...</td>\n"; $ret .= " <td class=\"numbers\">...</td>\n"; $ret .= " <td style=\"background: #dddddd\" colspan=\"2\"><i>" . 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) { 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 (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) { // 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('DEBUG: Checking repo_path rights'); $r = rg_git_files($a['old_rev'], $a['new_rev']); if ($r === FALSE) rg_git_fatal($a['refname'] . "\nInternal error, try again later\n"); $x = $_x; $x['type'] = 'repo_path'; foreach ($r as $file) { $x['needed_rights'] = 'P'; $x['misc'] = $file; if (rg_rights_allow($db, $x) !== TRUE) { rg_git_fatal($a['refname'] . "\nNo rights to push file [$file]\n"); } $x['needed_rights'] = 'W'; if (rg_rights_allow($db, $x) !== TRUE) { $w = rg_git_whitespace_ok($a['old_rev'], $a['new_rev']); if ($w !== TRUE) { rg_git_fatal($a['refname'] . "\nYou have no rights to push bad whitespace on path [$file]:" . "\n" . $w); } } } rg_log_exit(); $x = $_x; $x['type'] = 'repo_refs'; $x['needed_rights'] = 'P'; $x['misc'] = $a['refname']; if (rg_rights_allow($db, $x) !== TRUE) { rg_log("DEBUG: Push is not allowed, let's see the anon one"); $x['needed_rights'] = 'H'; if (rg_rights_allow($db, $x) !== TRUE) { $_z = array(); $msg = rg_template("msg/push_not_allowed.txt", $_z, FALSE /*xss*/); rg_git_fatal($a['refname']. "\n" . $msg); } // anonymous push - create a merge request $ev = $a; $ev['category'] = 'mr_event_new'; $ev['prio'] = 100; $r = rg_event_add($db, $ev); if ($r !== TRUE) rg_git_fatal($a['refname'] . ": " . rg_event_error()); rg_event_signal_daemon('', 0); $_x = array(); $msg = rg_template("msg/push_merge_request.txt", $_x, FALSE /*xss*/); rg_git_info('', $a['refname'] . ':' . "\n" . $msg); $history['history_category'] = REPO_CAT_GIT_BRANCH_ANON_PUSH; $history['history_message'] = 'Anonymous push to ref ' . $a['refname'] . ' into namespace ' . $a['namespace']; } else { rg_log("DEBUG: We are allowed to push."); $ev = $a; $ev['category'] = 'repo_event_push'; $ev['prio'] = 50; $ev['ri'] = array( 'repo_id' => $a['repo_id'], 'name' => $a['repo_name'], 'url' => rg_base_url() . $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) { $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: 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]; return $ret; } else { return $ret; } array_shift($paras); $val = trim(array_shift($paras)); $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("rg_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']) . "#sha1-" . rg_xss_safe($i['vars']['sha1']); $rg['HTML:commit_table'] = rg_git_log_template($log, 'repo/log', $rg); } else { $rg['HTML:commit_table'] = ''; } foreach ($log as $junk => &$i) { //rg_log_ml('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']['sha1'], $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_ml("DEBUG: i[files]=" . print_r($i['files'], TRUE)); $r = rg_git_diff($i['vars']['sha1'], $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) { 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) { $r = rg_git_repo_is_empty($repo_path); if ($r === -1) break; if ($r === 1) $a = $rg_git_empty; $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_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 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) { global $rg_git_zero; global $rg_git_empty; 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) $ref_name_tmp = $rg_git_empty; 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) { 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) { 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 = 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); } ?>