php8
[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->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;
203             $changes[] = $c;
204         }
205         return $changes;
206     }
207
208     function postCommit(IMTrackCommitHookBridge $bridge)
209     {
210        
211         // this might be run on multiple commits (big push...)
212         
213         
214         // in our system, we not only log commits that are against a
215         // ticket, but also ones that are not..
216         
217         
218         $files = $bridge->enumChangedOrModifiedFileNames();
219         
220         $fqfiles = array();
221         foreach ($files as $filename) {
222             $fqfiles[] = $this->repo->shortname . '/' . $filename;
223         }
224
225         // build up overall picture of what needs to be applied to tickets
226         $changes = $this->_getChanges($bridge);
227         
228         
229         //print_R($changes);
230         
231
232         // Deferred by tid
233         $deferred = array();
234         $T_by_tid = array();
235         $hashed = array();
236
237         // For correct attribution of spent time
238         $spent_by_tid_by_user = array();
239
240         // Changes that didn't ref a ticket; we want to show something
241         // on the timeline
242         $no_ticket = array();
243         
244         $me = $this->authUser;
245
246
247
248
249         foreach ($changes as $c) {
250             $tickets = array();
251             $log = $c->changelog;
252
253             $actions = $this->parseCommitMessage($log);
254             foreach ($actions as $act) {
255                 $what = $act[0];
256                 $tkt = $act[1];
257                 $tickets[$tkt][$what] = $what;
258                 if (isset($act[2])) {
259                   $tickets[$tkt]['spent'] += $act[2];
260                 }
261             }
262             if (count($tickets) == 0) {
263                 $no_ticket[] = $c;
264                 
265                 continue;
266             }
267             
268             // apply changes to tickets
269             $T = false;
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)) {
276                     continue;
277                 }
278                 break;
279                 
280                 $T_by_tid[$T->id] = $T;
281             }
282             
283             if (!$T) {
284                 continue;
285             }
286             /*
287             $accounted = false;
288             
289             if ($c->hash !== null) {
290                 if (isset($hashed[$T->tid][$c->hash])) {
291                     $accounted = true;
292                 } else {
293                     // see if we already have a reference
294                     
295                     
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);
300                 if (!$accounted) {
301                   $hashed[$T->tid][$c->hash] = $c->hash;
302                 }
303               }
304             }
305
306             if ($accounted) {
307               $deferred[$T->tid]['comments'][] =
308                 "(In $c->rev) merged to [repo:" . 
309                   $this->repo->getBrowseRootName() . "]";
310               continue;
311             }
312             
313             */
314             
315             $log = "(In " . $c->rev . ") ";
316                 /*
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..
320                  
321                 if ($c->changeby != $me) {
322                   $log .= " (on behalf of [user:$c->changeby]) ";
323                 }
324                 */
325                 
326             // for the stuff below we do not currently support multiple tickets..
327             
328             $log .= $c->changelog;
329             if (!isset($deferred[$T->id])) {
330                 $deferred[$T->id] = array(
331                     'comments' => array(),
332                     'changes' => array(),
333                     'act' => array(),
334                     'ticket' => $T
335                     
336                 );
337             }
338             
339             
340             $deferred[$T->id]['comments'][] = $log;
341             $deferred[$T->id]['changes'][] = $c;
342             
343             
344             
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']);
348             }
349             $deferred[$T->id]['act'][] = $act;
350  
351             //??? 
352             $this->checkVeto('postCommit', $log, $fqfiles, $actions);
353         }
354         
355         
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;
360         
361         
362         return  true;
363     
364     /*    
365         $ret = array();
366        // print_r($deferred);
367         foreach ($deferred as $tid => $info) {
368             $T = $T_by_tid[$tid];
369
370             $log = join("\n\n", $info['comments']);
371             
372             
373           
374           
375           
376           $CS = MTrackChangeset::begin("ticket:" . $T->tid, $log);
377
378           if (isset($hashed[$T->tid])) {
379             foreach ($hashed[$T->tid] as $hash) {
380               MTrackDB::q(
381                 'insert into ticket_changeset_hashes(tid, hash) values (?, ?)',
382                 $T->tid, $hash);
383             }
384           }
385
386           $T->addComment($log);
387           if (isset($info['act'])) foreach ($info['act'] as $act) {
388             if (isset($act['close'])) {
389               $T->resolution = 'fixed';
390               $T->close();
391             }
392             if (isset($act['spent'])) {
393               $T->addEffort($act['spent']);
394             }
395           }
396           $T->save($CS);
397           $CS->commit();
398         }
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");
407             MTrackAuth::drop();
408             foreach ($time as $spent) {
409               $T->addEffort($spent);
410             }
411             $T->save($CS);
412             $CS->commit();
413           }
414         }
415         $log = '';
416         foreach ($no_ticket as $c) {
417           $log .= "(In " . $c->rev . ") ";
418           if ($c->changeby != $me) {
419             $log .= " (on behalf of [user:$c->changeby]) ";
420           }
421           $log .= $c->changelog . "\n\n";
422         }
423         $CS = MTrackChangeset::begin("repo:" . $this->repo->id, $log);
424         $CS->commit();
425         
426         */
427     }
428
429   
430 }