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/Changeset.php';
13 class MTrack_CommitChecker {
15 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;
34 var $checks = array();
37 var $spent_by_tid_by_user;
40 function __construct($ar) {
41 foreach($ar as $k=>$v) {
44 foreach($this->checks as $chk) {
54 $args = func_get_args();
55 $method = array_shift($args);
58 foreach (self::$listeners as $l) {
59 $v = call_user_func_array(array($l, $method), $args);
61 if ($v === null || $v === false) {
62 $reasons[] = sprintf("%s:%s() returned %s",
63 get_class($l), $method, $v === null ? 'null' : 'false');
64 } elseif (is_array($v)) {
73 if (count($reasons)) {
74 require_once 'MTrack/Exception/Veto.php';
75 throw new MTrackVetoException($reasons);
79 function parseCommitMessage($msg)
81 // Parse the commit message and look for commands;
82 // returns each recognized command and its args in an array
84 $close = array('resolves', 'resolved', 'close', 'closed',
85 'closes', 'fix', 'fixed', 'fixes');
86 $refs = array('addresses', 'references', 'referenced',
87 'refs', 'ref', 'see', 're');
89 $cmds = join('|', $close) . '|' . join('|', $refs);
92 $timepat = ''; //'(?:\s*\((?:spent|sp)\s*(-?[0-9]*(?:\.[0-9]+)?)\s*(?:hours?|hrs)?\s*\))?';
93 $tktref = "(?:#|(?:(?:ticket|issue|bug):?\s*))([a-z]*[0-9]+)$timepat";
95 $pat = "(?P<action>(?:$cmds))\s*(?P<ticket>$tktref(?:(?:[, &]*|\s+and\s+)$tktref)*)";
101 if (preg_match_all("/$pat/smi", $msg, $M, PREG_SET_ORDER)) {
103 foreach ($M as $match) {
104 if (in_array($match['action'], $close)) {
105 $action = 'ref'; // 'close'; - commits need reviewing before they can close something.
111 if (preg_match_all("/$tktref/smi", $match['ticket'],
112 $T, PREG_SET_ORDER)) {
114 foreach ($T as $tmatch) {
115 if (isset($tmatch[2])) {
116 // [ action, ticket, spent ]
117 $actions[] = array($action, $tmatch[1], $tmatch[2]);
119 // [ action, ticket ]
120 $actions[] = array($action, $tmatch[1]);
130 function preCommit(IMTrackCommitHookBridge $bridge)
132 die("NOT SUPPORTED YET!");
135 $this->bridge = $bridge;
136 MTrackACL::requireAllRights("repo:" . $this->repo->id, 'commit');
139 $files = $bridge->enumChangedOrModifiedFileNames();
141 $changes = $this->_getChanges($bridge);
142 foreach ($changes as $c) {
143 $log = $c->changelog;
144 $actions = $this->parseCommitMessage($log);
146 // check permissions on the tickets
148 foreach ($actions as $act) {
150 $tickets[$tkt] = $tkt;
153 foreach ($tickets as $tkt) {
154 if (strlen($tkt) == 32) {
155 $T = MTrackIssue::loadById($tkt);
157 $T = MTrackIssue::loadByNSIdent($tkt);
161 $reasons[] = "#$tkt is not a valid ticket\n";
166 if ($c->hash !== null) {
167 list($accounted) = MTrackDB::q(
168 'select count(hash) from ticket_changeset_hashes
169 where tid = ? and hash = ?',
170 $T->tid, $c->hash)->fetchAll(PDO::FETCH_COLUMN, 0);
176 if (!MTrackACL::hasAllRights("ticket:$T->tid", "modify")) {
177 $reasons[] = MTrackAuth::whoami() . " does not have permission to modify #$tkt\n";
178 } else if (!$T->isOpen()) {
179 $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";
183 if (count($reasons) > 0) {
184 require_once 'MTrack/Exception/Veto.php';
185 throw new MTrackVetoException($reasons);
187 $this->checkVeto('vetoCommit', $log, $files, $actions, $this);
190 private function _getChanges(IMTrackCommitHookBridge $bridge)
193 if ($bridge instanceof IMTrackCommitHookBridge2) {
194 // this is HG only at present.
195 $changes = $bridge->getChanges();
197 require_once 'MTrack/CommitHookChangeEvent.php';
198 $c = new MTrackCommitHookChangeEvent;
199 $c->rev = $bridge->getChangesetDescriptor();
200 $c->changelog = $bridge->getCommitMessage();
201 $c->changeby = $this->authUser->email; //???
202 $c->changeby_id = $this->authUser->id; //???
203 $c->branch = $bridge->branch;
204 //print_r($bridge);exit;
205 $c->ctime = isset($bridge->props['Date']) ? strtotime($bridge->props['Date']) : time();
206 $c->fileActions = $bridge->fileActions;
212 function postCommit(IMTrackCommitHookBridge $bridge)
215 // this might be run on multiple commits (big push...)
218 // in our system, we not only log commits that are against a
219 // ticket, but also ones that are not..
222 $files = $bridge->enumChangedOrModifiedFileNames();
225 foreach ($files as $filename) {
226 $fqfiles[] = $this->repo->shortname . '/' . $filename;
229 // build up overall picture of what needs to be applied to tickets
230 $changes = $this->_getChanges($bridge);
241 // For correct attribution of spent time
242 $spent_by_tid_by_user = array();
244 // Changes that didn't ref a ticket; we want to show something
246 $no_ticket = array();
248 $me = $this->authUser;
253 foreach ($changes as $c) {
255 $log = $c->changelog;
257 $actions = $this->parseCommitMessage($log);
258 foreach ($actions as $act) {
261 $tickets[$tkt][$what] = $what;
262 if (isset($act[2])) {
263 $tickets[$tkt]['spent'] += $act[2];
266 if (count($tickets) == 0) {
272 // apply changes to tickets
274 foreach ($tickets as $tkt => $act) {
275 // removed all the code that handles hashed ticked ids...
276 //DB_DataObject::DebugLevel(1);
277 $T = DB_DataObject::Factory('mtrack_ticket');
278 $T->project_id = $this->repo->project_id;
279 if (!$T->get($tkt)) {
284 $T_by_tid[$T->id] = $T;
293 if ($c->hash !== null) {
294 if (isset($hashed[$T->tid][$c->hash])) {
297 // see if we already have a reference
300 list($accounted) = MTrackDB::q(
301 'select count(hash) from ticket_changeset_hashes
302 where tid = ? and hash = ?',
303 $T->tid, $c->hash)->fetchAll(PDO::FETCH_COLUMN, 0);
305 $hashed[$T->tid][$c->hash] = $c->hash;
311 $deferred[$T->tid]['comments'][] =
312 "(In $c->rev) merged to [repo:" .
313 $this->repo->getBrowseRootName() . "]";
319 $log = "(In " . $c->rev . ") ";
321 .. we do not support commits on behalf of yet..
322 .. to fix this we need to change the auth code to
323 .. really pick up auth data..
325 if ($c->changeby != $me) {
326 $log .= " (on behalf of [user:$c->changeby]) ";
330 // for the stuff below we do not currently support multiple tickets..
332 $log .= $c->changelog;
333 if (!isset($deferred[$T->id])) {
334 $deferred[$T->id] = array(
335 'comments' => array(),
336 'changes' => array(),
344 $deferred[$T->id]['comments'][] = $log;
345 $deferred[$T->id]['changes'][] = $c;
349 if (isset($act['spent']) && $c->changeby != $me) {
350 $spent_by_tid_by_user[$T->id][$c->changeby_id][] = $act['spent'];
351 unset($act['spent']);
353 $deferred[$T->id]['act'][] = $act;
356 $this->checkVeto('postCommit', $log, $fqfiles, $actions);
360 // defered is a list of actions...
361 $this->no_ticket = $no_ticket;
362 $this->deferred = $deferred;
363 $this->spent_by_tid_by_user = $spent_by_tid_by_user;
370 // print_r($deferred);
371 foreach ($deferred as $tid => $info) {
372 $T = $T_by_tid[$tid];
374 $log = join("\n\n", $info['comments']);
380 $CS = MTrackChangeset::begin("ticket:" . $T->tid, $log);
382 if (isset($hashed[$T->tid])) {
383 foreach ($hashed[$T->tid] as $hash) {
385 'insert into ticket_changeset_hashes(tid, hash) values (?, ?)',
390 $T->addComment($log);
391 if (isset($info['act'])) foreach ($info['act'] as $act) {
392 if (isset($act['close'])) {
393 $T->resolution = 'fixed';
396 if (isset($act['spent'])) {
397 $T->addEffort($act['spent']);
403 foreach ($spent_by_tid_by_user as $tid => $sdata) {
404 // Load it fresh here, as there seems to be an issue with saving
405 // a second set of changes on a pre-existing object
406 $T = MTrackIssue::loadById($tid);
407 foreach ($sdata as $user => $time) {
408 MTrackAuth::su($user);
409 $CS = MTrackChangeset::begin("ticket:" . $T->tid,
410 "Tracking time from prior push");
412 foreach ($time as $spent) {
413 $T->addEffort($spent);
420 foreach ($no_ticket as $c) {
421 $log .= "(In " . $c->rev . ") ";
422 if ($c->changeby != $me) {
423 $log .= " (on behalf of [user:$c->changeby]) ";
425 $log .= $c->changelog . "\n\n";
427 $CS = MTrackChangeset::begin("repo:" . $this->repo->id, $log);