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();
36 function __construct($ar) {
37 foreach($ar as $k=>$v) {
40 foreach($this->checks as $chk) {
50 $args = func_get_args();
51 $method = array_shift($args);
54 foreach (self::$listeners as $l) {
55 $v = call_user_func_array(array($l, $method), $args);
57 if ($v === null || $v === false) {
58 $reasons[] = sprintf("%s:%s() returned %s",
59 get_class($l), $method, $v === null ? 'null' : 'false');
60 } elseif (is_array($v)) {
69 if (count($reasons)) {
70 require_once 'MTrack/Exception/Veto.php';
71 throw new MTrackVetoException($reasons);
75 function parseCommitMessage($msg)
77 // Parse the commit message and look for commands;
78 // returns each recognized command and its args in an array
80 $close = array('resolves', 'resolved', 'close', 'closed',
81 'closes', 'fix', 'fixed', 'fixes');
82 $refs = array('addresses', 'references', 'referenced',
83 'refs', 'ref', 'see', 're');
85 $cmds = join('|', $close) . '|' . join('|', $refs);
88 $timepat = ''; //'(?:\s*\((?:spent|sp)\s*(-?[0-9]*(?:\.[0-9]+)?)\s*(?:hours?|hrs)?\s*\))?';
89 $tktref = "(?:#|(?:(?:ticket|issue|bug):?\s*))([a-z]*[0-9]+)$timepat";
91 $pat = "(?P<action>(?:$cmds))\s*(?P<ticket>$tktref(?:(?:[, &]*|\s+and\s+)$tktref)*)";
97 if (preg_match_all("/$pat/smi", $msg, $M, PREG_SET_ORDER)) {
99 foreach ($M as $match) {
100 if (in_array($match['action'], $close)) {
101 $action = 'ref'; // 'close'; - commits need reviewing before they can close something.
107 if (preg_match_all("/$tktref/smi", $match['ticket'],
108 $T, PREG_SET_ORDER)) {
110 foreach ($T as $tmatch) {
111 if (isset($tmatch[2])) {
112 // [ action, ticket, spent ]
113 $actions[] = array($action, $tmatch[1], $tmatch[2]);
115 // [ action, ticket ]
116 $actions[] = array($action, $tmatch[1]);
126 function preCommit(IMTrackCommitHookBridge $bridge)
128 die("NOT SUPPORTED YET!");
131 $this->bridge = $bridge;
132 MTrackACL::requireAllRights("repo:" . $this->repo->id, 'commit');
135 $files = $bridge->enumChangedOrModifiedFileNames();
137 $changes = $this->_getChanges($bridge);
138 foreach ($changes as $c) {
139 $log = $c->changelog;
140 $actions = $this->parseCommitMessage($log);
142 // check permissions on the tickets
144 foreach ($actions as $act) {
146 $tickets[$tkt] = $tkt;
149 foreach ($tickets as $tkt) {
150 if (strlen($tkt) == 32) {
151 $T = MTrackIssue::loadById($tkt);
153 $T = MTrackIssue::loadByNSIdent($tkt);
157 $reasons[] = "#$tkt is not a valid ticket\n";
162 if ($c->hash !== null) {
163 list($accounted) = MTrackDB::q(
164 'select count(hash) from ticket_changeset_hashes
165 where tid = ? and hash = ?',
166 $T->tid, $c->hash)->fetchAll(PDO::FETCH_COLUMN, 0);
172 if (!MTrackACL::hasAllRights("ticket:$T->tid", "modify")) {
173 $reasons[] = MTrackAuth::whoami() . " does not have permission to modify #$tkt\n";
174 } else if (!$T->isOpen()) {
175 $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";
179 if (count($reasons) > 0) {
180 require_once 'MTrack/Exception/Veto.php';
181 throw new MTrackVetoException($reasons);
183 $this->checkVeto('vetoCommit', $log, $files, $actions, $this);
186 private function _getChanges(IMTrackCommitHookBridge $bridge)
189 if ($bridge instanceof IMTrackCommitHookBridge2) {
190 // this is HG only at present.
191 $changes = $bridge->getChanges();
193 require_once 'MTrack/CommitHookChangeEvent.php';
194 $c = new MTrackCommitHookChangeEvent;
195 $c->rev = $bridge->getChangesetDescriptor();
196 $c->changelog = $bridge->getCommitMessage();
197 $c->changeby = $this->authUser->email; //???
198 $c->changeby_id = $this->authUser->id; //???
199 $c->branch = $bridge->branch;
200 //print_r($bridge);exit;
201 $c->ctime = isset($bridge->props['Date']) ? strtotime($bridge->props['Date']) : time();
202 $c->fileActions = $bridge->fileActions;
208 function postCommit(IMTrackCommitHookBridge $bridge)
211 // this might be run on multiple commits (big push...)
214 // in our system, we not only log commits that are against a
215 // ticket, but also ones that are not..
218 $files = $bridge->enumChangedOrModifiedFileNames();
221 foreach ($files as $filename) {
222 $fqfiles[] = $this->repo->shortname . '/' . $filename;
225 // build up overall picture of what needs to be applied to tickets
226 $changes = $this->_getChanges($bridge);
237 // For correct attribution of spent time
238 $spent_by_tid_by_user = array();
240 // Changes that didn't ref a ticket; we want to show something
242 $no_ticket = array();
244 $me = $this->authUser;
249 foreach ($changes as $c) {
251 $log = $c->changelog;
253 $actions = $this->parseCommitMessage($log);
254 foreach ($actions as $act) {
257 $tickets[$tkt][$what] = $what;
258 if (isset($act[2])) {
259 $tickets[$tkt]['spent'] += $act[2];
262 if (count($tickets) == 0) {
268 // apply changes to tickets
270 foreach ($tickets as $tkt => $act) {
271 // removed all the code that handles hashed ticked ids...
272 //DB_DataObject::DebugLevel(1);
273 $T = DB_DataObject::Factory('mtrack_ticket');
274 $T->project_id = $this->repo->project_id;
275 if (!$T->get($tkt)) {
280 $T_by_tid[$T->id] = $T;
289 if ($c->hash !== null) {
290 if (isset($hashed[$T->tid][$c->hash])) {
293 // see if we already have a reference
296 list($accounted) = MTrackDB::q(
297 'select count(hash) from ticket_changeset_hashes
298 where tid = ? and hash = ?',
299 $T->tid, $c->hash)->fetchAll(PDO::FETCH_COLUMN, 0);
301 $hashed[$T->tid][$c->hash] = $c->hash;
307 $deferred[$T->tid]['comments'][] =
308 "(In $c->rev) merged to [repo:" .
309 $this->repo->getBrowseRootName() . "]";
315 $log = "(In " . $c->rev . ") ";
317 .. we do not support commits on behalf of yet..
318 .. to fix this we need to change the auth code to
319 .. really pick up auth data..
321 if ($c->changeby != $me) {
322 $log .= " (on behalf of [user:$c->changeby]) ";
326 // for the stuff below we do not currently support multiple tickets..
328 $log .= $c->changelog;
329 if (!isset($deferred[$T->id])) {
330 $deferred[$T->id] = array(
331 'comments' => array(),
332 'changes' => array(),
340 $deferred[$T->id]['comments'][] = $log;
341 $deferred[$T->id]['changes'][] = $c;
345 if (isset($act['spent']) && $c->changeby != $me) {
346 $spent_by_tid_by_user[$T->id][$c->changeby_id][] = $act['spent'];
347 unset($act['spent']);
349 $deferred[$T->id]['act'][] = $act;
352 $this->checkVeto('postCommit', $log, $fqfiles, $actions);
356 // defered is a list of actions...
357 $this->no_ticket = $no_ticket;
358 $this->deferred = $deferred;
359 $this->spent_by_tid_by_user = $spent_by_tid_by_user;
366 // print_r($deferred);
367 foreach ($deferred as $tid => $info) {
368 $T = $T_by_tid[$tid];
370 $log = join("\n\n", $info['comments']);
376 $CS = MTrackChangeset::begin("ticket:" . $T->tid, $log);
378 if (isset($hashed[$T->tid])) {
379 foreach ($hashed[$T->tid] as $hash) {
381 'insert into ticket_changeset_hashes(tid, hash) values (?, ?)',
386 $T->addComment($log);
387 if (isset($info['act'])) foreach ($info['act'] as $act) {
388 if (isset($act['close'])) {
389 $T->resolution = 'fixed';
392 if (isset($act['spent'])) {
393 $T->addEffort($act['spent']);
399 foreach ($spent_by_tid_by_user as $tid => $sdata) {
400 // Load it fresh here, as there seems to be an issue with saving
401 // a second set of changes on a pre-existing object
402 $T = MTrackIssue::loadById($tid);
403 foreach ($sdata as $user => $time) {
404 MTrackAuth::su($user);
405 $CS = MTrackChangeset::begin("ticket:" . $T->tid,
406 "Tracking time from prior push");
408 foreach ($time as $spent) {
409 $T->addEffort($spent);
416 foreach ($no_ticket as $c) {
417 $log .= "(In " . $c->rev . ") ";
418 if ($c->changeby != $me) {
419 $log .= " (on behalf of [user:$c->changeby]) ";
421 $log .= $c->changelog . "\n\n";
423 $CS = MTrackChangeset::begin("repo:" . $this->repo->id, $log);