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