<?php
// Merge requests
require_once($INC . "/util.inc.php");
require_once($INC . "/sql.inc.php");
require_once($INC . '/user.inc.php');
require_once($INC . "/events.inc.php");
$rg_mr_env_q = getenv("ROCKETGIT_MR_QUEUE");
if (empty($rg_mr_env_q))
$rg_mr_queue = $rg_state_dir . "/q_merge_requests";
else
$rg_mr_queue = $rg_mr_env_q;
$rg_mr_error = "";
function rg_mr_set_error($str)
{
global $rg_mr_error;
$rg_mr_error = $str;
rg_log($str);
}
function rg_mr_error()
{
global $rg_mr_error;
return $rg_mr_error;
}
/*
* Event functions
*/
$rg_mr_functions = array(
'mr_event_new' => 'rg_mr_event_new',
'mr_event_add_to_db' => 'rg_mr_event_add_to_db',
'mr_merge' => 'rg_mr_event_merge'
);
rg_event_register_functions($rg_mr_functions);
/*
* This is called when the user press "Merge" button
*/
function rg_mr_event_merge($db, $ev)
{
$ret = array();
// TODO: we should send the error messages only at last try! Anywhere! Really?!
// TODO: care! we should not send the mail before doing the merge!
// TODO: check also all the other events.
// do the merge
$r = rg_git_merge($ev['repo_path'], $ev['merge_against'],
$ev['mri']['new_rev'], $ev['merge_ff'], $ev['merge_msg']);
if ($r === FALSE) {
rg_log('Merge failed: ' . rg_git_error());
// TODO: notify ui['uid'] that the merge failed
$ev['mail'] = array();
$ev['mail']['subject'] = 'Merge failed';
rg_mail_template('repo/mail/merge_failed', $ev);
return $ret;
}
$r = rg_mr_merge_set_status($db, $ev['ri']['repo_id'],
$ev['mri']['id'], time());
if ($r === FALSE)
return FALSE;
// notify the requester (if not anonymous)
// TODO: notify the watchers of the project?
// TODO: notify the users who commented on the merge request?
// TODO: notify users who watch the merge request?
return $ret;
}
/*
* This is called when a new merge request is done
*/
function rg_mr_event_new($db, $ev)
{
$ret = array();
// Insert it into database
$ret[] = array_merge($ev,
array('category' => 'mr_event_add_to_db', 'prio' => 100));
// TODO: Notify admins of the repo
// TODO: Call a webhook
return $ret;
}
/*
* Add a merge request file to database
*/
function rg_mr_event_add_to_db($db, $a)
{
rg_prof_start('mr_event_add_to_db');
rg_log_enter('mr_add_to_db: a: ' . rg_array2string($a));
$now = time();
$ret = FALSE;
$rollback = 0;
while (1) {
if (rg_sql_begin($db) !== TRUE) {
rg_mr_set_error('start transaction failed');
break;
}
$rollback = 1;
$id = rg_repo_next_id($db, 'mr', $a['repo_id']);
if ($id === FALSE) {
rg_mr_set_error('cannot get next id');
break;
}
// TODO: git may fail to update the reference after this hook;
// the mr code should check if the update was done.
$mr = 'refs/mr/' . $id;
$reason = $a['ui']['username'] . ' pushed a merge request'
. ' for ref ' . $a['refname']
. ' into ' . $mr;
$r = rg_git_update_ref($a['repo_path'], $mr, '', $a['new_rev'], $reason);
if ($r !== TRUE) {
rg_mr_set_error('cannot update-ref: ' . rg_git_error());
break;
}
$a['id'] = $id;
$a['who'] = $a['ui']['uid'];
$sql = 'INSERT INTO merge_requests (repo_id, id, itime'
. ', namespace, refname, old_rev, new_rev, done, ip'
. ', who)'
. ' VALUES (@@repo_id@@, @@id@@, @@itime@@'
. ', @@namespace@@, @@refname@@, @@old_rev@@'
. ', @@new_rev@@, 0, @@ip@@, @@who@@)';
$res = rg_sql_query_params($db, $sql, $a);
if ($res === FALSE) {
rg_mr_set_error('cannot insert merge request');
break;
}
rg_sql_free_result($res);
if (rg_sql_commit($db) !== TRUE) {
rg_mr_set_error('cannot commit');
break;
}
$rollback = 0;
$ret = array();
break;
}
if ($rollback)
rg_sql_rollback($db);
rg_log_exit();
rg_prof_end('mr_event_add_to_db');
return $ret;
}
/*
* Loads info from a merge request file
*/
function rg_mr_queue_load_file($file)
{
$ret = array();
$ret['ok'] = 0;
$c = @file_get_contents($file);
if ($c === FALSE) {
rg_mr_set_error("cannot load a merge request from $file (" . rg_php_err() . ")");
return $ret;
}
$tokens = explode(" ", trim($c));
foreach ($tokens as $token) {
$p = explode("=", $token);
$ret[$p[0]] = $p[1];
}
$ret['ok'] = 1;
return $ret;
}
/*
* Process merge requests queue
*/
function rg_mr_queue_process($db)
{
global $rg_mr_queue;
rg_prof_start("mr_queue_process");
$ret = TRUE;
$dir = @opendir($rg_mr_queue);
if ($dir === FALSE) {
rg_mr_set_error("cannot open dir $rg_mr_queue (" . rg_php_err() . ")!");
return FALSE;
}
while (($file = readdir($dir))) {
if (strncmp($file, "mr-", 3) != 0)
continue;
$path = $rg_mr_queue . "/" . $file;
rg_log("Loading merge request from $path...");
$d = rg_mr_queue_load_file($path);
if ($d['ok'] != 1) {
$ret = FALSE;
break;
}
$r = rg_mr_create($db, $d['repo_id'], $d['namespace'],
$d['old_rev'], $d['new_rev'], $d['refname'], $d['ip']);
if ($r != TRUE) {
rename($path, $rg_mr_queue . "/BAD-" . $file);
} else {
if (@unlink($path) !== TRUE)
rg_log("Warn: Cannot unlink file $path!");
// TODO: Verify it exists in database
}
}
closedir($dir);
rg_prof_end("mr_queue_process");
return $ret;
}
/*
* Helper to cosmetic mr data
*/
function rg_mr_cosmetic($db, &$row)
{
$row['date_utc'] = gmdate("Y-m-d H:i", $row['itime']);
$row['old_rev_short'] = substr($row['old_rev'], 0, RG_GIT_HASH_LEN);
$row['new_rev_short'] = substr($row['new_rev'], 0, RG_GIT_HASH_LEN);
if (!isset($row['who_nice'])) {
if ($row['who'] == 0) {
$row['who_nice'] = 'anonymous';
} else {
$ui = rg_user_info($db, $row['who'], '', '');
if ($ui['exists'] == 1)
$row['who_nice'] = $ui['username'];
else
$row['who_nice'] = 'n/a';
}
}
$row['done_nice'] = gmdate("Y-m-d H:i", $row['done']);
}
/*
* Loads merge requests
* @type - 'pending' or 'closed'
* @limit = 0 => no limit
*/
function rg_mr_load($db, $repo_id, $type, $limit)
{
rg_log("mr_load: repo_id=$repo_id type=$type limit=$limit");
if (strcmp($type, 'pending') == 0)
$add = ' AND done = 0';
else
$add = ' AND done > 1000'; // first values are for different status
$params = array('repo_id' => $repo_id);
$sql = 'SELECT * FROM merge_requests'
. ' WHERE repo_id = @@repo_id@@'
. $add
. ' ORDER BY itime';
if ($limit > 0)
$sql .= ' LIMIT ' . $limit;
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
rg_mr_set_error("Cannot load merge requests (" . rg_sql_error() . ")");
return FALSE;
}
$ret = array();
while (($row = rg_sql_fetch_array($res))) {
rg_mr_cosmetic($db, $row);
$ret[] = $row;
}
rg_sql_free_result($res);
return $ret;
}
/*
* Loads a merge request
*/
function rg_mr_load_one($db, $repo_id, $id)
{
rg_prof_start('mr_load_one');
rg_log('mr_load_one: repo_id=' . $repo_id . ' id=' . $id);
$ret = FALSE;
while (1) {
$params = array('repo_id' => $repo_id, 'id' => $id);
$sql = 'SELECT * FROM merge_requests'
. ' WHERE repo_id = @@repo_id@@'
. ' AND id = @@id@@';
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
rg_mr_set_error('cannot load the merge request');
break;
}
$rows = rg_sql_num_rows($res);
if ($rows)
$ret = rg_sql_fetch_array($res);
rg_sql_free_result($res);
if ($rows == 0) {
rg_mr_set_error('mr not found');
break;
}
rg_mr_cosmetic($db, $ret);
break;
}
rg_prof_end('mr_load_one');
return $ret;
}
/*
* Sets the status ('done' filed of a merge request
* @status - 0 - not merged, > 10 = timestamp when the merge took place
*/
function rg_mr_merge_set_status($db, $repo_id, $id, $status)
{
$ret = FALSE;
while (1) {
$params = array('repo_id' => $repo_id,
'id' => $id,
'status' => $status
);
$sql = 'UPDATE merge_requests'
. ' SET done = @@status@@'
. ' WHERE repo_id = @@repo_id@@'
. ' AND id = @@id@@';
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
rg_mr_set_error('cannot update status (' . rg_sql_error());
break;
}
rg_sql_free_result($res);
$ret = TRUE;
break;
}
return $ret;
}
/*
* Loads data for graphs
* @unit - interval on which a sum is made
*/
function rg_mr_data($db, $type, $start, $end, $unit, $mode)
{
$params = array('start' => $start, 'end' => $end);
switch ($type) {
case 'create_mr':
$q = 'SELECT 1 AS value, itime FROM merge_requests'
. ' WHERE itime >= @@start@@ AND itime <= @@end@@';
break;
default:
rg_internal_error('invalid type');
return FALSE;
}
$ret = rg_graph_query($db, $start, $end, $unit, $mode, $q, $params, '');
if ($ret === FALSE)
rg_mr_set_error(rg_graph_error());
return $ret;
}
/*
* High level merge request function
*/
function rg_mr_high_level($db, &$rg, $paras)
{
rg_prof_start('mr_high_level');
rg_log_enter('mr_high_level');
$ret = '';
while (1) {
if ($rg['ri']['git_dir_done'] == 0) {
$ret .= rg_template('repo/err/no_git_dir.html',
$rg, TRUE /*xss*/);
break;
}
// TODO: mrs.html is empty!
$ret .= rg_template('repo/mrs.html', $rg, TRUE /*xss*/);
$op = array_shift($paras);
if (empty($op)) {
$op = 'pending';
} else {
$mr = sprintf('%u', $op);
if ($mr > 0)
$op = '';
}
rg_log('DEBUG: op=' . $op);
$rg['menu']['mr'][$op] = 1;
$rg['HTML:menu_repo_level2'] =
rg_template('repo/mr/menu.html', $rg, TRUE /*xss*/);
if ((strcmp($op, 'pending') == 0) || (strcmp($op, 'closed') == 0)) {
$rg['mr']['op'] = $op;
$r = rg_mr_load($db, $rg['ri']['repo_id'], $op,
0 /*no limit*/);
if ($r === FALSE) {
$ret .= 'error getting merge request list ('
. rg_mr_error() . ')';
} else {
$ret .= rg_template_table('repo/mr/list', $r, $rg);
}
break;
} else if (strcmp($op, 'create') == 0) {
$ret .= rg_warning('not yet implemented'); // TODO
break;
}
$mri = rg_mr_load_one($db, $rg['ri']['repo_id'], $mr);
if ($mri === FALSE) {
$ret .= 'error loading merge request'
. ' (' . rg_mr_error() . ')';
break;
}
$mri['merge_in_progress'] = 0;
$mri['already_merged'] = 0;
$against = rg_git_short($mri['refname']);
$mr_op = array_shift($paras);
rg_log('DEBUG: mr_op=' . $mr_op);
if (strcmp($mr_op, 'merge') == 0) {
if ($rg['can_admin'] !== 1) {
$ret .= rg_warning('Not allowed!');
break;
}
if (!rg_valid_referer()) {
$ret .= rg_warning('Invalid referer; try again.');
break;
}
if (!rg_token_valid($db, $rg, 'mr_merge', FALSE)) {
$ret .= rg_warning('Invalid token; try again.');
break;
}
$event = array(
'category' => 'mr_merge',
'prio' => 30,
'ri' => $rg['ri'],
'ui' => $rg['login_ui'],
'repo_path' => $rg['repo_path'],
'mri' => $mri,
'merge_against' => $against,
'merge_ff' => rg_var_uint('merge_ff'),
'merge_msg' => rg_var_str('merge_msg')
);
$r = rg_event_add($db, $event);
if ($r !== TRUE) {
$ret .= rg_warning('Cannot add event; try again later.');
break;
}
rg_event_signal_daemon('', 0);
$mri['merge_in_progress'] = 1;
$mri['HTML:body'] = rg_template(
'repo/mr/merge_in_progress.html',
$rg, TRUE /*xss*/);
} else {
$mri['HTML:body'] = '';
if ($mri['done'] > 10) { // already merged
$mri['merge_in_progress'] = 1;
$mri['already_merged'] = 1;
$mri['done_nice'] =
gmdate('Y-m-d H:i', $mri['done']) . ' UTC';
$mri['can_merge_without_conflicts'] = 0;
} else {
$r = rg_git_merge_without_conflict($rg['repo_path'],
$against, $mri['new_rev']);
if ($r === FALSE) {
$ret .= rg_warning('Error testing if merge will work'
. ' (' . rg_git_error() . ').');
break;
}
rg_log('DEBUG: can merge without conflict:' . ($r == 1 ? 'yes' : 'no'));
$mri['can_merge_without_conflicts'] = $r;
if ($mri['can_merge_without_conflicts'] == 0) {
$base = rg_git_merge_base($rg['repo_path'],
$against, $mri['new_rev']);
if ($base === FALSE) {
$ret .= rg_warning('Error: finding base: '
. rg_git_error() . '.');
break;
}
$mri['HTML:status'] = rg_git_merge_tree_html(
$rg['repo_path'], $base, $against,
$mri['new_rev']);
rg_log_ml('DEBUG: status: ' . print_r($mri['HTML:status'], TRUE));
$mri['HTML:body'] .= rg_template(
'repo/mr/conflicts.html', $mri, TRUE /*xss*/);
}
}
$patch_limit = rg_git_patch_limit($db);
$_log = rg_git_log($rg['repo_path'], 0, $mri['old_rev'],
$mri['new_rev'], TRUE /*also_patch*/, $patch_limit);
if ($_log === FALSE) {
$ret .= rg_warning('Error generating patch.');
break;
}
$rg['HTML:commit_labels'] = '';
$mri['HTML:body'] .=
rg_git_log2listing($_log, $rg, TRUE);
if ($mri['can_merge_without_conflicts'] == 1)
$rg['rg_form_token'] = rg_token_get($db, $rg, 'mr_merge');
}
//rg_log_ml('DEBUG: mri: ' . print_r($mri, TRUE));
$rg['mr'] = $mri;
$hints = array();
$hints[]['HTML:hint'] = rg_template('hints/repo/merge.html',
$rg, TRUE /*xss*/);
$rg['mr']['HTML:hints'] = rg_template_table('hints/list',
$hints, $rg);
$ret .= rg_template('repo/mr/page.html', $rg,
TRUE/*xss*/);
break;
}
rg_log_exit();
rg_prof_end('mr_high_level');
return $ret;
}
?>