<?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 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? // TODO: notify the users who commented on the pull request? // TODO: notify users who watch the pull request? return $ret; } /* * This is called when a new pull 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) { global $php_errormsg; $ret = array(); $ret['ok'] = 0; $c = @file_get_contents($file); if ($c === FALSE) { rg_mr_set_error("cannot load a merge request from $file ($php_errormsg)"); 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 $php_errormsg; 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 ($php_errormsg)!"); 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, 7); $row['new_rev_short'] = substr($row['new_rev'], 0, 7); 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); $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'); return FALSE; } $row = rg_sql_fetch_array($res); rg_mr_cosmetic($db, $row); rg_sql_free_result($res); rg_prof_end('mr_load_one'); return $row; } /* * 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; } /* * 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/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'); 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: cmwc=' . $r); $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*/); } } $_log = rg_git_log($rg['repo_path'], 0, $mri['old_rev'], $mri['new_rev'], TRUE); 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; } ?>