'checkPHP', ); static $listeners = array(); static function addCheck($name) { require_once "MTrack/CommitCheck/$name.php"; $cls = "MTrackCommitCheck_$name"; self::$listeners[] = new $cls; } var $repo; var $bridge; var $authUser; var $checks = array(); var $no_ticket; var $deferred; var $spent_by_tid_by_user; function __construct($ar) { foreach($ar as $k=>$v) { $this->$k = $v; } foreach($this->checks as $chk) { self::addCheck($chk); } } function checkVeto() { $args = func_get_args(); $method = array_shift($args); $reasons = array(); foreach (self::$listeners as $l) { $v = call_user_func_array(array($l, $method), $args); if ($v !== true) { if ($v === null || $v === false) { $reasons[] = sprintf("%s:%s() returned %s", get_class($l), $method, $v === null ? 'null' : 'false'); } elseif (is_array($v)) { foreach ($v as $m) { $reasons[] = $m; } } else { $reasons[] = $v; } } } if (count($reasons)) { require_once 'MTrack/Exception/Veto.php'; throw new MTrackVetoException($reasons); } } function parseCommitMessage($msg) { // Parse the commit message and look for commands; // returns each recognized command and its args in an array $close = array('resolves', 'resolved', 'close', 'closed', 'closes', 'fix', 'fixed', 'fixes'); $refs = array('addresses', 'references', 'referenced', 'refs', 'ref', 'see', 're'); $cmds = join('|', $close) . '|' . join('|', $refs); $timepat = ''; //'(?:\s*\((?:spent|sp)\s*(-?[0-9]*(?:\.[0-9]+)?)\s*(?:hours?|hrs)?\s*\))?'; $tktref = "(?:#|(?:(?:ticket|issue|bug):?\s*))([a-z]*[0-9]+)$timepat"; $pat = "(?P(?:$cmds))\s*(?P$tktref(?:(?:[, &]*|\s+and\s+)$tktref)*)"; $M = array(); $actions = array(); if (preg_match_all("/$pat/smi", $msg, $M, PREG_SET_ORDER)) { foreach ($M as $match) { if (in_array($match['action'], $close)) { $action = 'ref'; // 'close'; - commits need reviewing before they can close something. } else { $action = 'ref'; } $tickets = array(); $T = array(); if (preg_match_all("/$tktref/smi", $match['ticket'], $T, PREG_SET_ORDER)) { foreach ($T as $tmatch) { if (isset($tmatch[2])) { // [ action, ticket, spent ] $actions[] = array($action, $tmatch[1], $tmatch[2]); } else { // [ action, ticket ] $actions[] = array($action, $tmatch[1]); } } } } } return $actions; } function preCommit(IMTrackCommitHookBridge $bridge) { die("NOT SUPPORTED YET!"); //echo "Pre-commit"; $this->bridge = $bridge; MTrackACL::requireAllRights("repo:" . $this->repo->id, 'commit'); $files = $bridge->enumChangedOrModifiedFileNames(); $changes = $this->_getChanges($bridge); foreach ($changes as $c) { $log = $c->changelog; $actions = $this->parseCommitMessage($log); // check permissions on the tickets $tickets = array(); foreach ($actions as $act) { $tkt = $act[1]; $tickets[$tkt] = $tkt; } $reasons = array(); foreach ($tickets as $tkt) { if (strlen($tkt) == 32) { $T = MTrackIssue::loadById($tkt); } else { $T = MTrackIssue::loadByNSIdent($tkt); } if ($T === null) { $reasons[] = "#$tkt is not a valid ticket\n"; continue; } $accounted = false; if ($c->hash !== null) { list($accounted) = MTrackDB::q( 'select count(hash) from ticket_changeset_hashes where tid = ? and hash = ?', $T->tid, $c->hash)->fetchAll(PDO::FETCH_COLUMN, 0); if ($accounted) { continue; } } if (!MTrackACL::hasAllRights("ticket:$T->tid", "modify")) { $reasons[] = MTrackAuth::whoami() . " does not have permission to modify #$tkt\n"; } else if (!$T->isOpen()) { $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"; } } } if (count($reasons) > 0) { require_once 'MTrack/Exception/Veto.php'; throw new MTrackVetoException($reasons); } $this->checkVeto('vetoCommit', $log, $files, $actions, $this); } private function _getChanges(IMTrackCommitHookBridge $bridge) { $changes = array(); if ($bridge instanceof IMTrackCommitHookBridge2) { // this is HG only at present. $changes = $bridge->getChanges(); } else { require_once 'MTrack/CommitHookChangeEvent.php'; $c = new MTrackCommitHookChangeEvent; $c->rev = $bridge->getChangesetDescriptor(); $c->changelog = $bridge->getCommitMessage(); $c->changeby = $this->authUser->email; //??? $c->changeby_id = $this->authUser->id; //??? $c->branch = $bridge->branch; //print_r($bridge);exit; $c->ctime = isset($bridge->props['Date']) ? strtotime($bridge->props['Date']) : time(); $c->fileActions = $bridge->fileActions; $changes[] = $c; } return $changes; } function postCommit(IMTrackCommitHookBridge $bridge) { // this might be run on multiple commits (big push...) // in our system, we not only log commits that are against a // ticket, but also ones that are not.. $files = $bridge->enumChangedOrModifiedFileNames(); $fqfiles = array(); foreach ($files as $filename) { $fqfiles[] = $this->repo->shortname . '/' . $filename; } // build up overall picture of what needs to be applied to tickets $changes = $this->_getChanges($bridge); //print_R($changes); // Deferred by tid $deferred = array(); $T_by_tid = array(); $hashed = array(); // For correct attribution of spent time $spent_by_tid_by_user = array(); // Changes that didn't ref a ticket; we want to show something // on the timeline $no_ticket = array(); $me = $this->authUser; foreach ($changes as $c) { $tickets = array(); $log = $c->changelog; $actions = $this->parseCommitMessage($log); foreach ($actions as $act) { $what = $act[0]; $tkt = $act[1]; $tickets[$tkt][$what] = $what; if (isset($act[2])) { $tickets[$tkt]['spent'] += $act[2]; } } if (count($tickets) == 0) { $no_ticket[] = $c; continue; } // apply changes to tickets $T = false; foreach ($tickets as $tkt => $act) { // removed all the code that handles hashed ticked ids... //DB_DataObject::DebugLevel(1); $T = DB_DataObject::Factory('mtrack_ticket'); $T->project_id = $this->repo->project_id; if (!$T->get($tkt)) { continue; } break; $T_by_tid[$T->id] = $T; } if (!$T) { continue; } /* $accounted = false; if ($c->hash !== null) { if (isset($hashed[$T->tid][$c->hash])) { $accounted = true; } else { // see if we already have a reference list($accounted) = MTrackDB::q( 'select count(hash) from ticket_changeset_hashes where tid = ? and hash = ?', $T->tid, $c->hash)->fetchAll(PDO::FETCH_COLUMN, 0); if (!$accounted) { $hashed[$T->tid][$c->hash] = $c->hash; } } } if ($accounted) { $deferred[$T->tid]['comments'][] = "(In $c->rev) merged to [repo:" . $this->repo->getBrowseRootName() . "]"; continue; } */ $log = "(In " . $c->rev . ") "; /* .. we do not support commits on behalf of yet.. .. to fix this we need to change the auth code to .. really pick up auth data.. if ($c->changeby != $me) { $log .= " (on behalf of [user:$c->changeby]) "; } */ // for the stuff below we do not currently support multiple tickets.. $log .= $c->changelog; if (!isset($deferred[$T->id])) { $deferred[$T->id] = array( 'comments' => array(), 'changes' => array(), 'act' => array(), 'ticket' => $T ); } $deferred[$T->id]['comments'][] = $log; $deferred[$T->id]['changes'][] = $c; if (isset($act['spent']) && $c->changeby != $me) { $spent_by_tid_by_user[$T->id][$c->changeby_id][] = $act['spent']; unset($act['spent']); } $deferred[$T->id]['act'][] = $act; //??? $this->checkVeto('postCommit', $log, $fqfiles, $actions); } // defered is a list of actions... $this->no_ticket = $no_ticket; $this->deferred = $deferred; $this->spent_by_tid_by_user = $spent_by_tid_by_user; return true; /* $ret = array(); // print_r($deferred); foreach ($deferred as $tid => $info) { $T = $T_by_tid[$tid]; $log = join("\n\n", $info['comments']); $CS = MTrackChangeset::begin("ticket:" . $T->tid, $log); if (isset($hashed[$T->tid])) { foreach ($hashed[$T->tid] as $hash) { MTrackDB::q( 'insert into ticket_changeset_hashes(tid, hash) values (?, ?)', $T->tid, $hash); } } $T->addComment($log); if (isset($info['act'])) foreach ($info['act'] as $act) { if (isset($act['close'])) { $T->resolution = 'fixed'; $T->close(); } if (isset($act['spent'])) { $T->addEffort($act['spent']); } } $T->save($CS); $CS->commit(); } foreach ($spent_by_tid_by_user as $tid => $sdata) { // Load it fresh here, as there seems to be an issue with saving // a second set of changes on a pre-existing object $T = MTrackIssue::loadById($tid); foreach ($sdata as $user => $time) { MTrackAuth::su($user); $CS = MTrackChangeset::begin("ticket:" . $T->tid, "Tracking time from prior push"); MTrackAuth::drop(); foreach ($time as $spent) { $T->addEffort($spent); } $T->save($CS); $CS->commit(); } } $log = ''; foreach ($no_ticket as $c) { $log .= "(In " . $c->rev . ") "; if ($c->changeby != $me) { $log .= " (on behalf of [user:$c->changeby]) "; } $log .= $c->changelog . "\n\n"; } $CS = MTrackChangeset::begin("repo:" . $this->repo->id, $log); $CS->commit(); */ } }