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