99829d88dbaf89e39332b249d0874fefee6712fa
[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             $c->ctime = time();
200             $changes[] = $c;
201         }
202         return $changes;
203     }
204
205     function postCommit(IMTrackCommitHookBridge $bridge)
206     {
207        
208         // this might be run on multiple commits (big push...)
209         
210         
211         $files = $bridge->enumChangedOrModifiedFileNames();
212         
213         $fqfiles = array();
214         foreach ($files as $filename) {
215             $fqfiles[] = $this->repo->shortname . '/' . $filename;
216         }
217
218         // build up overall picture of what needs to be applied to tickets
219         $changes = $this->_getChanges($bridge);
220         
221         
222         print_R($changes);
223         
224
225         // Deferred by tid
226         $deferred = array();
227         $T_by_tid = array();
228         $hashed = array();
229
230         // For correct attribution of spent time
231         $spent_by_tid_by_user = array();
232
233         // Changes that didn't ref a ticket; we want to show something
234         // on the timeline
235         $no_ticket = array();
236         
237         $me = $this->authUser;
238
239         foreach ($changes as $c) {
240             $tickets = array();
241             $log = $c->changelog;
242
243             $actions = $this->parseCommitMessage($log);
244             foreach ($actions as $act) {
245                 $what = $act[0];
246                 $tkt = $act[1];
247                 $tickets[$tkt][$what] = $what;
248                 if (isset($act[2])) {
249                   $tickets[$tkt]['spent'] += $act[2];
250                 }
251             }
252             if (count($tickets) == 0) {
253                 $no_ticket[] = $c;
254                 continue;
255             }
256             
257             // apply changes to tickets
258             foreach ($tickets as $tkt => $act) {
259                 // removed all the code that handles hashed ticked ids...
260                 $T = clone($this->ticketProvider);
261                 if (!$T->get($tkt)) {
262                     continue;
263                 }
264                 
265                 $T_by_tid[$T->id] = $T;
266             }
267             /*
268             $accounted = false;
269             
270             if ($c->hash !== null) {
271                 if (isset($hashed[$T->tid][$c->hash])) {
272                     $accounted = true;
273                 } else {
274                     // see if we already have a reference
275                     
276                     
277                     list($accounted) = MTrackDB::q(
278                           'select count(hash) from ticket_changeset_hashes
279                       where tid = ? and hash = ?',
280                   $T->tid, $c->hash)->fetchAll(PDO::FETCH_COLUMN, 0);
281                 if (!$accounted) {
282                   $hashed[$T->tid][$c->hash] = $c->hash;
283                 }
284               }
285             }
286
287             if ($accounted) {
288               $deferred[$T->tid]['comments'][] =
289                 "(In $c->rev) merged to [repo:" . 
290                   $this->repo->getBrowseRootName() . "]";
291               continue;
292             }
293             
294             */
295             
296             $log = "(In " . $c->rev . ") ";
297                 /*
298                  .. we do not support commits on behalf of yet..
299                  .. to fix this we need to change the auth code to
300                  .. really pick up auth data..
301                  
302                 if ($c->changeby != $me) {
303                   $log .= " (on behalf of [user:$c->changeby]) ";
304                 }
305                 */
306             
307             $log .= $c->changelog;
308             
309             $deferred[$T->id]['comments'][] = $log;
310             
311             if (isset($act['spent']) && $c->changeby != $me) {
312                 $spent_by_tid_by_user[$T->id][$c->changeby_id][] = $act['spent'];
313                 unset($act['spent']);
314             }
315             $deferred[$T->tid]['act'][] = $act;
316  
317             //??? 
318             $this->checkVeto('postCommit', $log, $fqfiles, $actions);
319         }
320         
321         // defered is a list of actions...
322         
323         $this->deferred = $deferred;
324         $this->spent_by_tid_by_user = $spent_by_tid_by_user;
325         
326         
327         return  true;
328     
329     /*    
330         $ret = array();
331        // print_r($deferred);
332         foreach ($deferred as $tid => $info) {
333             $T = $T_by_tid[$tid];
334
335             $log = join("\n\n", $info['comments']);
336             
337             
338           
339           
340           
341           $CS = MTrackChangeset::begin("ticket:" . $T->tid, $log);
342
343           if (isset($hashed[$T->tid])) {
344             foreach ($hashed[$T->tid] as $hash) {
345               MTrackDB::q(
346                 'insert into ticket_changeset_hashes(tid, hash) values (?, ?)',
347                 $T->tid, $hash);
348             }
349           }
350
351           $T->addComment($log);
352           if (isset($info['act'])) foreach ($info['act'] as $act) {
353             if (isset($act['close'])) {
354               $T->resolution = 'fixed';
355               $T->close();
356             }
357             if (isset($act['spent'])) {
358               $T->addEffort($act['spent']);
359             }
360           }
361           $T->save($CS);
362           $CS->commit();
363         }
364         foreach ($spent_by_tid_by_user as $tid => $sdata) {
365           // Load it fresh here, as there seems to be an issue with saving
366           // a second set of changes on a pre-existing object
367           $T = MTrackIssue::loadById($tid);
368           foreach ($sdata as $user => $time) {
369             MTrackAuth::su($user);
370             $CS = MTrackChangeset::begin("ticket:" . $T->tid,
371               "Tracking time from prior push");
372             MTrackAuth::drop();
373             foreach ($time as $spent) {
374               $T->addEffort($spent);
375             }
376             $T->save($CS);
377             $CS->commit();
378           }
379         }
380         $log = '';
381         foreach ($no_ticket as $c) {
382           $log .= "(In " . $c->rev . ") ";
383           if ($c->changeby != $me) {
384             $log .= " (on behalf of [user:$c->changeby]) ";
385           }
386           $log .= $c->changelog . "\n\n";
387         }
388         $CS = MTrackChangeset::begin("repo:" . $this->repo->id, $log);
389         $CS->commit();
390         
391         */
392     }
393
394   
395 }