MTrack/CommitChecker.php
[web.mtrack] / MTrack / CommitChecker.php
1 <?php 
2
3 require_once 'MTrack/Interface/CommitListener.php'; 
4 require_once 'MTrack/Interface/CommitHookBridge.php'; 
5 require_once 'MTrack/Interface/CommitHookBridge2.php'; 
6
7 //require_once 'MTrack/Issue.php'; 
8  //require_once 'MTrack/Changeset.php'; 
9   
10
11  
12
13 class MTrack_CommitChecker {
14     
15     static $fileChecks = array(
16         'php' => 'checkPHP',
17     );
18     
19     
20     static $listeners = array();
21    
22     static function addCheck($name)
23     {
24         require_once "MTrack/CommitCheck/$name.php";
25         $cls = "MTrackCommitCheck_$name";
26         self::$listeners[] = new $cls;
27     }
28      
29     
30     var $repo;
31     var $bridge;
32     var $authUser;
33     
34     var $checks = array();
35     
36     function __construct($ar) {
37         foreach($ar as $k=>$v) {
38             $this->$k = $v;
39         }
40         foreach($this->checks as $chk) {
41             self::addCheck($chk);
42         }
43         
44         
45     }
46     
47
48     function checkVeto()
49     {
50         $args = func_get_args();
51         $method = array_shift($args);
52         $reasons = array();
53
54         foreach (self::$listeners as $l) {
55           $v = call_user_func_array(array($l, $method), $args);
56           if ($v !== true) {
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)) {
61               foreach ($v as $m) {
62                 $reasons[] = $m;
63               }
64             } else {
65               $reasons[] = $v;
66             }
67           }
68         }
69         if (count($reasons)) {
70             require_once 'MTrack/Exception/Veto.php';
71             throw new MTrackVetoException($reasons);
72         }
73     }
74
75     function parseCommitMessage($msg) 
76     {
77         // Parse the commit message and look for commands;
78         // returns each recognized command and its args in an array
79
80         $close = array('resolves', 'resolved', 'close', 'closed',
81                        'closes', 'fix', 'fixed', 'fixes');
82         $refs = array('addresses', 'references', 'referenced',
83                       'refs', 'ref', 'see', 're');
84
85         $cmds = join('|', $close) . '|' . join('|', $refs);
86         
87         
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";
90
91         $pat = "(?P<action>(?:$cmds))\s*(?P<ticket>$tktref(?:(?:[, &]*|\s+and\s+)$tktref)*)";
92
93          
94         $M = array();
95         $actions = array();
96
97         if (preg_match_all("/$pat/smi", $msg, $M, PREG_SET_ORDER)) {
98              
99           foreach ($M as $match) {
100             if (in_array($match['action'], $close)) {
101               $action = 'ref'; // 'close'; - commits need reviewing before they can close something.
102             } else {
103               $action = 'ref';
104             }
105             $tickets = array();
106             $T = array();
107             if (preg_match_all("/$tktref/smi", $match['ticket'],
108                 $T, PREG_SET_ORDER)) {
109
110               foreach ($T as $tmatch) {
111                 if (isset($tmatch[2])) {
112                   // [ action, ticket, spent ]
113                   $actions[] = array($action, $tmatch[1], $tmatch[2]);
114                 } else {
115                   // [ action, ticket ]
116                   $actions[] = array($action, $tmatch[1]);
117                 }
118               }
119             }
120           }
121         }
122        
123         return $actions;
124     }
125
126     function preCommit(IMTrackCommitHookBridge $bridge) 
127     {
128         die("NOT SUPPORTED YET!");
129        
130         //echo "Pre-commit";
131         $this->bridge = $bridge;
132         MTrackACL::requireAllRights("repo:" . $this->repo->id, 'commit');
133         
134         
135         $files = $bridge->enumChangedOrModifiedFileNames();
136          
137         $changes = $this->_getChanges($bridge);
138         foreach ($changes as $c) {
139             $log = $c->changelog;
140             $actions = $this->parseCommitMessage($log);
141
142               // check permissions on the tickets
143             $tickets = array();
144             foreach ($actions as $act) {
145                 $tkt = $act[1];
146                 $tickets[$tkt] = $tkt;
147             }
148             $reasons = array();
149             foreach ($tickets as $tkt) {
150                 if (strlen($tkt) == 32) {
151                   $T = MTrackIssue::loadById($tkt);
152                 } else {
153                   $T = MTrackIssue::loadByNSIdent($tkt);
154                 }
155
156                 if ($T === null) {
157                   $reasons[] = "#$tkt is not a valid ticket\n";
158                   continue;
159                 }
160
161                 $accounted = false;
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);
167                   if ($accounted) {
168                     continue;
169                   }
170                 }
171
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";
176                 }
177             }
178         }
179         if (count($reasons) > 0) {
180             require_once 'MTrack/Exception/Veto.php';
181           throw new MTrackVetoException($reasons);
182         }
183         $this->checkVeto('vetoCommit', $log, $files, $actions, $this);
184     }
185
186     private function _getChanges(IMTrackCommitHookBridge $bridge)
187     {
188         $changes = array();
189         if ($bridge instanceof IMTrackCommitHookBridge2) {
190             // this is HG only at present.
191           $changes = $bridge->getChanges();
192         } else {
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             //print_r($bridge);exit;
200             $c->ctime       = isset($bridge->props['Date']) ? strtotime($bridge->props['Date']) : time();
201             $c->fileActions = $bridge->fileActions;
202             $changes[] = $c;
203         }
204         return $changes;
205     }
206
207     function postCommit(IMTrackCommitHookBridge $bridge)
208     {
209        
210         // this might be run on multiple commits (big push...)
211         
212         
213         // in our system, we not only log commits that are against a
214         // ticket, but also ones that are not..
215         
216         
217         $files = $bridge->enumChangedOrModifiedFileNames();
218         
219         $fqfiles = array();
220         foreach ($files as $filename) {
221             $fqfiles[] = $this->repo->shortname . '/' . $filename;
222         }
223
224         // build up overall picture of what needs to be applied to tickets
225         $changes = $this->_getChanges($bridge);
226         
227         
228         //print_R($changes);
229         
230
231         // Deferred by tid
232         $deferred = array();
233         $T_by_tid = array();
234         $hashed = array();
235
236         // For correct attribution of spent time
237         $spent_by_tid_by_user = array();
238
239         // Changes that didn't ref a ticket; we want to show something
240         // on the timeline
241         $no_ticket = array();
242         
243         $me = $this->authUser;
244
245
246
247
248         foreach ($changes as $c) {
249             $tickets = array();
250             $log = $c->changelog;
251
252             $actions = $this->parseCommitMessage($log);
253             foreach ($actions as $act) {
254                 $what = $act[0];
255                 $tkt = $act[1];
256                 $tickets[$tkt][$what] = $what;
257                 if (isset($act[2])) {
258                   $tickets[$tkt]['spent'] += $act[2];
259                 }
260             }
261             if (count($tickets) == 0) {
262                 $no_ticket[] = $c;
263                 
264                 continue;
265             }
266             
267             // apply changes to tickets
268             $T = false;
269             foreach ($tickets as $tkt => $act) {
270                 // removed all the code that handles hashed ticked ids...
271                 //DB_DataObject::DebugLevel(1);
272                 $T = DB_DataObject::Factory('mtrack_ticket');
273                 $T->project_id = $this->repo->project_id;
274                 if (!$T->get($tkt)) {
275                     continue;
276                 }
277                 break;
278                 
279                 $T_by_tid[$T->id] = $T;
280             }
281             
282             if (!$T) {
283                 continue;
284             }
285             /*
286             $accounted = false;
287             
288             if ($c->hash !== null) {
289                 if (isset($hashed[$T->tid][$c->hash])) {
290                     $accounted = true;
291                 } else {
292                     // see if we already have a reference
293                     
294                     
295                     list($accounted) = MTrackDB::q(
296                           'select count(hash) from ticket_changeset_hashes
297                       where tid = ? and hash = ?',
298                   $T->tid, $c->hash)->fetchAll(PDO::FETCH_COLUMN, 0);
299                 if (!$accounted) {
300                   $hashed[$T->tid][$c->hash] = $c->hash;
301                 }
302               }
303             }
304
305             if ($accounted) {
306               $deferred[$T->tid]['comments'][] =
307                 "(In $c->rev) merged to [repo:" . 
308                   $this->repo->getBrowseRootName() . "]";
309               continue;
310             }
311             
312             */
313             
314             $log = "(In " . $c->rev . ") ";
315                 /*
316                  .. we do not support commits on behalf of yet..
317                  .. to fix this we need to change the auth code to
318                  .. really pick up auth data..
319                  
320                 if ($c->changeby != $me) {
321                   $log .= " (on behalf of [user:$c->changeby]) ";
322                 }
323                 */
324                 
325             // for the stuff below we do not currently support multiple tickets..
326             
327             $log .= $c->changelog;
328             if (!isset($deferred[$T->id])) {
329                 $deferred[$T->id] = array(
330                     'comments' => array(),
331                     'changes' => array(),
332                     'act' => array(),
333                     'ticket' => $T
334                     
335                 );
336             }
337             
338             
339             $deferred[$T->id]['comments'][] = $log;
340             $deferred[$T->id]['changes'][] = $c;
341             
342             
343             
344             if (isset($act['spent']) && $c->changeby != $me) {
345                 $spent_by_tid_by_user[$T->id][$c->changeby_id][] = $act['spent'];
346                 unset($act['spent']);
347             }
348             $deferred[$T->id]['act'][] = $act;
349  
350             //??? 
351             $this->checkVeto('postCommit', $log, $fqfiles, $actions);
352         }
353         
354         
355         // defered is a list of actions...
356         $this->no_ticket = $no_ticket;
357         $this->deferred = $deferred;
358         $this->spent_by_tid_by_user = $spent_by_tid_by_user;
359         
360         
361         return  true;
362     
363     /*    
364         $ret = array();
365        // print_r($deferred);
366         foreach ($deferred as $tid => $info) {
367             $T = $T_by_tid[$tid];
368
369             $log = join("\n\n", $info['comments']);
370             
371             
372           
373           
374           
375           $CS = MTrackChangeset::begin("ticket:" . $T->tid, $log);
376
377           if (isset($hashed[$T->tid])) {
378             foreach ($hashed[$T->tid] as $hash) {
379               MTrackDB::q(
380                 'insert into ticket_changeset_hashes(tid, hash) values (?, ?)',
381                 $T->tid, $hash);
382             }
383           }
384
385           $T->addComment($log);
386           if (isset($info['act'])) foreach ($info['act'] as $act) {
387             if (isset($act['close'])) {
388               $T->resolution = 'fixed';
389               $T->close();
390             }
391             if (isset($act['spent'])) {
392               $T->addEffort($act['spent']);
393             }
394           }
395           $T->save($CS);
396           $CS->commit();
397         }
398         foreach ($spent_by_tid_by_user as $tid => $sdata) {
399           // Load it fresh here, as there seems to be an issue with saving
400           // a second set of changes on a pre-existing object
401           $T = MTrackIssue::loadById($tid);
402           foreach ($sdata as $user => $time) {
403             MTrackAuth::su($user);
404             $CS = MTrackChangeset::begin("ticket:" . $T->tid,
405               "Tracking time from prior push");
406             MTrackAuth::drop();
407             foreach ($time as $spent) {
408               $T->addEffort($spent);
409             }
410             $T->save($CS);
411             $CS->commit();
412           }
413         }
414         $log = '';
415         foreach ($no_ticket as $c) {
416           $log .= "(In " . $c->rev . ") ";
417           if ($c->changeby != $me) {
418             $log .= " (on behalf of [user:$c->changeby]) ";
419           }
420           $log .= $c->changelog . "\n\n";
421         }
422         $CS = MTrackChangeset::begin("repo:" . $this->repo->id, $log);
423         $CS->commit();
424         
425         */
426     }
427
428   
429 }