3 require_once 'MTrack/Interface/CommitListener.php';
4 require_once 'MTrack/Interface/CommitHookBridge.php';
5 require_once 'MTrack/Interface/CommitHookBridge2.php';
7 require_once 'MTrack/Issue.php';
8 require_once 'MTrack/DB.php';
9 require_once 'MTrack/Changeset.php';
10 require_once 'MTrack/Config.php';
12 require_once 'MTrack/ACL.php';
13 require_once 'MTrack/Auth.php';
16 class MTrackCommitChecker {
17 static $fileChecks = array(
20 static $listeners = array();
22 static function addCheck($name)
24 require_once "MTrack/CommitCheck/$name.php";
25 $cls = "MTrackCommitCheck_$name";
26 self::$listeners[] = new $cls;
33 function __construct($repo) {
39 $args = func_get_args();
40 $method = array_shift($args);
43 foreach (self::$listeners as $l) {
44 $v = call_user_func_array(array($l, $method), $args);
46 if ($v === null || $v === false) {
47 $reasons[] = sprintf("%s:%s() returned %s",
48 get_class($l), $method, $v === null ? 'null' : 'false');
49 } elseif (is_array($v)) {
58 if (count($reasons)) {
59 require_once 'MTrack/Exception/Veto.php';
60 throw new MTrackVetoException($reasons);
64 function parseCommitMessage($msg)
66 // Parse the commit message and look for commands;
67 // returns each recognized command and its args in an array
69 $close = array('resolves', 'resolved', 'close', 'closed',
70 'closes', 'fix', 'fixed', 'fixes');
71 $refs = array('addresses', 'references', 'referenced',
72 'refs', 'ref', 'see', 're');
74 $cmds = join('|', $close) . '|' . join('|', $refs);
77 $timepat = ''; //'(?:\s*\((?:spent|sp)\s*(-?[0-9]*(?:\.[0-9]+)?)\s*(?:hours?|hrs)?\s*\))?';
78 $tktref = "(?:#|(?:(?:ticket|issue|bug):?\s*))([a-z]*[0-9]+)$timepat";
80 $pat = "(?P<action>(?:$cmds))\s*(?P<ticket>$tktref(?:(?:[, &]*|\s+and\s+)$tktref)*)";
86 if (preg_match_all("/$pat/smi", $msg, $M, PREG_SET_ORDER)) {
88 foreach ($M as $match) {
89 if (in_array($match['action'], $close)) {
90 $action = 'ref'; // 'close'; - commits need reviewing before they can close something.
96 if (preg_match_all("/$tktref/smi", $match['ticket'],
97 $T, PREG_SET_ORDER)) {
99 foreach ($T as $tmatch) {
100 if (isset($tmatch[2])) {
101 // [ action, ticket, spent ]
102 $actions[] = array($action, $tmatch[1], $tmatch[2]);
104 // [ action, ticket ]
105 $actions[] = array($action, $tmatch[1]);
115 function preCommit(IMTrackCommitHookBridge $bridge)
118 $this->bridge = $bridge;
119 MTrackACL::requireAllRights("repo:" . $this->repo->repoid, 'commit');
122 $files = $bridge->enumChangedOrModifiedFileNames();
124 $changes = $this->_getChanges($bridge);
125 foreach ($changes as $c) {
126 $log = $c->changelog;
127 $actions = $this->parseCommitMessage($log);
129 // check permissions on the tickets
131 foreach ($actions as $act) {
133 $tickets[$tkt] = $tkt;
136 foreach ($tickets as $tkt) {
137 if (strlen($tkt) == 32) {
138 $T = MTrackIssue::loadById($tkt);
140 $T = MTrackIssue::loadByNSIdent($tkt);
144 $reasons[] = "#$tkt is not a valid ticket\n";
149 if ($c->hash !== null) {
150 list($accounted) = MTrackDB::q(
151 'select count(hash) from ticket_changeset_hashes
152 where tid = ? and hash = ?',
153 $T->tid, $c->hash)->fetchAll(PDO::FETCH_COLUMN, 0);
159 if (!MTrackACL::hasAllRights("ticket:$T->tid", "modify")) {
160 $reasons[] = MTrackAuth::whoami() . " does not have permission to modify #$tkt\n";
161 } else if (!$T->isOpen()) {
162 $reasons[] = " ** #$tkt is already closed.\n ** You must either re-open it (if it has not already shipped)\n ** or open a new ticket to track this issue\n";
166 if (count($reasons) > 0) {
167 require_once 'MTrack/Exception/Veto.php';
168 throw new MTrackVetoException($reasons);
170 $this->checkVeto('vetoCommit', $log, $files, $actions, $this);
173 private function _getChanges(IMTrackCommitHookBridge $bridge)
176 if ($bridge instanceof IMTrackCommitHookBridge2) {
177 // this is HG only at present.
178 $changes = $bridge->getChanges();
180 require_once 'MTrack/CommitHookChangeEvent.php';
181 $c = new MTrackCommitHookChangeEvent;
182 $c->rev = $bridge->getChangesetDescriptor();
183 $c->changelog = $bridge->getCommitMessage();
184 $c->changeby = MTrackAuth::whoami();
191 function postCommit(IMTrackCommitHookBridge $bridge)
193 $files = $bridge->enumChangedOrModifiedFileNames();
196 foreach ($files as $filename) {
197 $fqfiles[] = $this->repo->shortname . '/' . $filename;
200 // build up overall picture of what needs to be applied to tickets
201 $changes = $this->_getChanges($bridge);
208 // For correct attribution of spent time
209 $spent_by_tid_by_user = array();
211 // Changes that didn't ref a ticket; we want to show something
213 $no_ticket = array();
215 $me = mtrack_canon_username(MTrackAuth::whoami());
217 foreach ($changes as $c) {
219 $log = $c->changelog;
221 $actions = $this->parseCommitMessage($log);
222 foreach ($actions as $act) {
225 $tickets[$tkt][$what] = $what;
226 if (isset($act[2])) {
227 $tickets[$tkt]['spent'] += $act[2];
230 if (count($tickets) == 0) {
234 // apply changes to tickets
235 foreach ($tickets as $tkt => $act) {
236 if (strlen($tkt) == 32 && isset($T_by_tid[$tkt])) {
237 $T = $T_by_tid[$tkt];
239 if (strlen($tkt) == 32) {
240 $T = MTrackIssue::loadById($tkt);
242 $T = MTrackIssue::loadByNSIdent($tkt);
244 $T_by_tid[$T->tid] = $T;
248 if ($c->hash !== null) {
249 if (isset($hashed[$T->tid][$c->hash])) {
252 list($accounted) = MTrackDB::q(
253 'select count(hash) from ticket_changeset_hashes
254 where tid = ? and hash = ?',
255 $T->tid, $c->hash)->fetchAll(PDO::FETCH_COLUMN, 0);
257 $hashed[$T->tid][$c->hash] = $c->hash;
263 $deferred[$T->tid]['comments'][] =
264 "(In $c->rev) merged to [repo:" .
265 $this->repo->getBrowseRootName() . "]";
268 $log = "(In " . $c->rev . ") ";
269 if ($c->changeby != $me) {
270 $log .= " (on behalf of [user:$c->changeby]) ";
272 $log .= $c->changelog;
273 $deferred[$T->tid]['comments'][] = $log;
274 if (isset($act['spent']) && $c->changeby != $me) {
275 $spent_by_tid_by_user[$T->tid][$c->changeby][] = $act['spent'];
276 unset($act['spent']);
278 $deferred[$T->tid]['act'][] = $act;
281 $this->checkVeto('postCommit', $log, $fqfiles, $actions);
283 // print_r($deferred);
284 foreach ($deferred as $tid => $info) {
285 $T = $T_by_tid[$tid];
287 $log = join("\n\n", $info['comments']);
289 $CS = MTrackChangeset::begin("ticket:" . $T->tid, $log);
291 if (isset($hashed[$T->tid])) {
292 foreach ($hashed[$T->tid] as $hash) {
294 'insert into ticket_changeset_hashes(tid, hash) values (?, ?)',
299 $T->addComment($log);
300 if (isset($info['act'])) foreach ($info['act'] as $act) {
301 if (isset($act['close'])) {
302 $T->resolution = 'fixed';
305 if (isset($act['spent'])) {
306 $T->addEffort($act['spent']);
312 foreach ($spent_by_tid_by_user as $tid => $sdata) {
313 // Load it fresh here, as there seems to be an issue with saving
314 // a second set of changes on a pre-existing object
315 $T = MTrackIssue::loadById($tid);
316 foreach ($sdata as $user => $time) {
317 MTrackAuth::su($user);
318 $CS = MTrackChangeset::begin("ticket:" . $T->tid,
319 "Tracking time from prior push");
321 foreach ($time as $spent) {
322 $T->addEffort($spent);
329 foreach ($no_ticket as $c) {
330 $log .= "(In " . $c->rev . ") ";
331 if ($c->changeby != $me) {
332 $log .= " (on behalf of [user:$c->changeby]) ";
334 $log .= $c->changelog . "\n\n";
336 $CS = MTrackChangeset::begin("repo:" . $this->repo->repoid, $log);