MTrack/Repo.php
[web.mtrack] / MTrack / Repo.php
1 <?php
2 require_once 'MTrack/SCM.php';
3 require_once 'MTrack/DB.php';
4 require_once 'MTrack/Config.php';
5 require_once 'MTrack/Project.php';
6 require_once 'MTrack/SCMFileEvent.php';
7 require_once 'MTrack/ACL.php';
8 require_once 'MTrack/Changeset.php';
9 require_once 'MTrack/Wiki.php';
10
11
12 class MTrack_Repo extends MTrackSCM 
13 {
14     public $repoid = null;
15     public $shortname = null;
16     public $scmtype = null;
17     public $repopath = null;
18     public $browserurl = null;
19     public $browsertype = null;
20     public $description = null;
21     public $parent = '';
22     public $clonedfrom = null;
23     public $serverurl = null;
24     private $links_to_add = array();
25     private $links_to_remove = array();
26     private $links = null;
27     static $scms = array();
28  
29     static function loadType($a) {
30         
31         
32     }
33  
34     static function getAvailableSCMs()
35     {
36         $ret = array();
37         $ar = scandir(dirname(__FILE__).'/SCM');
38         
39         foreach($ar as $a) {
40             if (empty($a) || $a[0] == '.') {
41                 continue;
42             }
43             $fn = dirname(__FILE__).'/SCM/'.$a.'/Repo.php';
44             if (!file_exists($fn)) {
45                 continue;
46             }
47             $cls = 'MTrack_SCM_'.$a.'_Repo';
48             require_once $fn;
49             $ret[$a] = new $cls;
50             
51         }
52         return $ret;
53     }  
54     
55     static function factory($ar)
56     {
57         $fn = 'MTrack/SCM/'.$ar['scmtype'].'/Repo.php';
58         $cls = 'MTrack_SCM_'.$a.'_Repo';
59         require_once $fn;
60         
61         $ret = new $cls;
62         foreach($ar as $k=>$v) {
63             $ret->$k = $v;
64         }
65         return $ret;
66         
67     }
68     
69     /*static function loadById($id) {
70         list($row) = MTrackDB::q(
71           'select repoid, scmtype from repos where repoid = ?',
72           $id)->fetchAll();
73         if (isset($row[0])) {
74           $type = $row[1];
75           if (isset(self::$scms[$type])) {
76             $class = self::$scms[$type];
77             return new $class($row[0]);
78           }
79           throw new Exception("unsupported repo type $type");
80         }
81         return null;
82     }
83     static function loadByName($name) {
84         $bits = explode('/', $name);
85         if (count($bits) > 1 && $bits[0] == 'default') {
86           array_shift($bits);
87           $name = $bits[0];
88         }
89         if (count($bits) > 1) {
90           // wez/reponame -> per user repo 
91           $u = "user:$bits[0]";
92           $p = "project:$bits[0]";
93           $rows = MTrackDB::q(
94             'select repoid, scmtype from repos where shortname = ? and (parent = ? OR parent = ?)',
95             $bits[1], $u, $p)->fetchAll();
96         } else {
97           $rows = MTrackDB::q(
98             "select repoid, scmtype from repos where shortname = ? and parent =''",
99             $name)->fetchAll();
100         }
101         if (is_array($rows) && isset($rows[0])) {
102           $row = $rows[0];
103           if (isset($row[0])) {
104             $type = $row[1];
105             if (isset(self::$scms[$type])) {
106               $class = self::$scms[$type];
107               return new $class($row[0]);
108             }
109             throw new Exception("unsupported repo type $type");
110           }
111         }
112         return null;
113     }
114     static function loadByLocation($path) 
115     {
116           
117         // we have magic configuration - end users commit into SVN
118         // backend is really git... - so pre-commit hooks have to be from svn
119         list($row) = MTrackDB::q('select repoid, scmtype from repos where repopath = ?', $path)->fetchAll();
120         if (isset($row[0])) {
121           $type = $row[1];
122           if (isset(self::$scms[$type])) {
123             $class = self::$scms[$type];
124             return new $class($row[0]);
125           }
126           throw new Exception("unsupported repo type $type");
127         }
128         return null;
129       }
130   
131     static function loadByChangeSet($cs)
132     {
133         
134         static $re = array();
135         if (isset($re[$cs])) {
136             return $re[$cs];
137         }
138         //using (repoid)  ??
139         $q = MTrackDB::q("
140             select 
141                 r.shortname as repo, 
142                 p.shortname as proj 
143             from 
144                 repos r 
145             left join 
146                 project_repo_link l on r.repoid = l.repoid
147             left join 
148                 projects p on p.projid = r.projectid
149             where 
150                 (parent is null  or length(parent) = 0)
151             AND
152                 (
153                     ( ? like CONCAT(proj), '%') 
154                 OR
155                     ( ? like CONCAT(repo), '%')
156                 )                    
157                 ");
158         $ar = $q->fetchAll(PDO::FETCH_ASSOC);
159         if ($ar) {
160             $re[$cs] = self::loadByName($ar['repo']);
161             return $re[$cs];
162         } 
163         $re[$cs] = false;
164         return $re[$cs];
165     }
166     */
167     // methods 
168   
169     function __construct($id = null) {
170         if ($id !== null) {
171           list($row) = MTrackDB::q(
172                         'select * from repos where repoid = ?',
173                         $id)->fetchAll();
174           if (isset($row[0])) {
175             $this->repoid = $row['repoid'];
176             $this->shortname = $row['shortname'];
177             $this->scmtype = $row['scmtype'];
178             $this->repopath = $row['repopath'];
179             $this->browserurl = $row['browserurl'];
180             $this->browsertype = $row['browsertype'];
181             $this->description = $row['description'];
182             $this->parent = $row['parent'];
183             $this->clonedfrom = $row['clonedfrom'];
184             $this->serverurl = $row['serverurl'];
185             return;
186           }
187           throw new Exception("unable to find repo with id = $id");
188         }
189     }
190     
191     function reconcileRepoSettings() {
192         if (!isset(self::$scms[$this->scmtype])) {
193           throw new Exception("invalid scm type $this->scmtype");
194         }
195         $c = self::$scms[$this->scmtype];
196         $s = new $c;
197         $s->reconcileRepoSettings($this);
198     }
199    
200     function getSCMMetaData() {
201     return null;
202   }
203
204     function getServerURL() {
205     if ($this->serverurl) {
206       return $this->serverurl;
207     }
208     $url = MTrackConfig::get('repos', "$this->scmtype.serverurl");
209     if ($url) {
210       return $url . $this->getBrowseRootName();
211     }
212     return null;
213   }
214
215     function getCheckoutCommand() {
216     $url = $this->getServerURL();
217     if (strlen($url)) {
218       return $this->scmtype . ' clone ' . $this->getServerURL();
219     }
220     return null;
221   }
222
223     function canFork() {
224     return false;
225   }
226
227     function getWorkingCopy() {
228     throw new Exception("cannot getWorkingCopy from a generic repo object");
229   }
230
231     function deleteRepo(MTrackChangeset $CS) {
232     MTrackDB::q('delete from repos where repoid = ?', $this->repoid);
233     mtrack_rmdir($this->repopath);
234   }
235
236     function save(MTrackChangeset $CS) {
237         if (!isset(self::$scms[$this->scmtype])) {
238           throw new Exception("unsupported repo type " . $this->scmtype);
239         }
240     
241         if ($this->repoid) {
242           list($row) = MTrackDB::q(
243                         'select * from repos where repoid = ?',
244                         $this->repoid)->fetchAll();
245           $old = $row;
246           MTrackDB::q(
247               'update repos set shortname = ?, scmtype = ?, repopath = ?,
248                 browserurl = ?, browsertype = ?, description = ?,
249                 parent = ?, serverurl = ?, clonedfrom = ? where repoid = ?',
250               $this->shortname, $this->scmtype, $this->repopath,
251               $this->browserurl, $this->browsertype, $this->description,
252               $this->parent, $this->serverurl, $this->clonedfrom, $this->repoid);
253         } else {
254           $acl = null;
255     
256           if (!strlen($this->repopath)) {
257             if (!MTrackConfig::get('repos', 'allow_user_repo_creation')) {
258               throw new Exception("configuration does not allow repo creation");
259             }
260             $repodir = MTrackConfig::get('repos', 'basedir');
261             if ($repodir == null) {
262               $repodir = MTrackConfig::get('core', 'vardir') . '/repos';
263             }
264             if (!is_dir($repodir)) {
265               mkdir($repodir);
266             }
267     
268             if (!$this->parent) {
269               $owner = mtrack_canon_username(MTrackAuth::whoami());
270               $this->parent = 'user:' . $owner;
271             } else {
272               list($type, $owner) = explode(':', $this->parent, 2);
273               switch ($type) {
274                 case 'project':
275                   $P = MTrackProject::loadByName($owner);
276                   if (!$P) {
277                     throw new Exception("invalid project $owner");
278                   }
279                   MTrackACL::requireAllRights("project:$P->projid", 'modify');
280                   break;
281                 case 'user':
282                   if ($owner != mtrack_canon_username(MTrackAuth::whoami())) {
283                     throw new Exception("can't make a repo for another user");
284                   }
285                   break;
286                 default:
287                   throw new Exception("invalid parent ($this->parent)");
288               }
289             }
290             if (preg_match("/[^a-zA-Z0-9_.-]/", $owner)) {
291               throw new Exception("$owner must not contain special characters");
292             }
293             $this->repopath = $repodir . DIRECTORY_SEPARATOR . $owner;
294             if (!is_dir($this->repopath)) {
295               mkdir($this->repopath);
296             }
297             $this->repopath .= DIRECTORY_SEPARATOR . $this->shortname;
298     
299             /* default ACL is allow user all rights, block everybody else */
300             $acl = array(
301               array($owner, 'read', 1),
302               array($owner, 'modify', 1),
303               array($owner, 'delete', 1),
304               array($owner, 'checkout', 1),
305               array($owner, 'commit', 1),
306               array('*', 'read', 0),
307               array('*', 'modify', 0),
308               array('*', 'delete', 0),
309               array('*', 'checkout', 0),
310               array('*', 'commit', 0),
311             );
312           }
313     
314           MTrackDB::q('insert into repos (shortname, scmtype,
315               repopath, browserurl, browsertype, description, parent,
316               serverurl, clonedfrom)
317               values (?, ?, ?, ?, ?, ?, ?, ?, ?)',
318               $this->shortname, $this->scmtype, $this->repopath,
319               $this->browserurl, $this->browsertype, $this->description,
320               $this->parent, $this->serverurl, $this->clonedfrom);
321     
322           $this->repoid = MTrackDB::lastInsertId('repos', 'repoid');
323           $old = null;
324     
325           if ($acl !== null) {
326             MTrackACL::setACL("repo:$this->repoid", 0, $acl);
327             $me = mtrack_canon_username(MTrackAuth::whoami());
328             foreach (array('ticket', 'changeset') as $e) {
329               MTrackDB::q(
330                 'insert into watches (otype, oid, userid, medium, event, active) values (?, ?, ?, ?, ?, 1)',
331               'repo', $this->repoid, $me, 'email', $e);
332             }
333           }
334         }
335         $this->reconcileRepoSettings();
336         if (!$this->parent) {
337           /* for SSH access, populate a symlink from the repos basedir to the
338            * actual path for this repo */
339           $repodir = MTrackConfig::get('repos', 'basedir');
340           if ($repodir == null) {
341             $repodir = MTrackConfig::get('core', 'vardir') . '/repos';
342           }
343           if (!is_dir($repodir)) {
344             mkdir($repodir);
345           }
346           $repodir .= '/default';
347           if (!is_dir($repodir)) {
348             mkdir($repodir);
349           }
350           $repodir .= '/' . $this->shortname;
351           if (!file_exists($repodir)) {
352             symlink($this->repopath, $repodir);
353           } else if (is_link($repodir) && readlink($repodir) != $this->repopath) {
354             unlink($repodir);
355             symlink($this->repopath, $repodir);
356           }
357         }
358         $CS->add("repo:" . $this->repoid . ":shortname", $old['shortname'], $this->shortname);
359         $CS->add("repo:" . $this->repoid . ":scmtype", $old['scmtype'], $this->scmtype);
360         $CS->add("repo:" . $this->repoid . ":repopath", $old['repopath'], $this->repopath);
361         $CS->add("repo:" . $this->repoid . ":browserurl", $old['browserurl'], $this->browserurl);
362         $CS->add("repo:" . $this->repoid . ":browsertype", $old['browsertype'], $this->browsertype);
363         $CS->add("repo:" . $this->repoid . ":description", $old['description'], $this->description);
364         $CS->add("repo:" . $this->repoid . ":parent", $old['parent'], $this->parent);
365         $CS->add("repo:" . $this->repoid . ":clonedfrom", $old['clonedfrom'], $this->clonedfrom);
366         $CS->add("repo:" . $this->repoid . ":serverurl", $old['serverurl'], $this->serverurl);
367     
368         foreach ($this->links_to_add as $link) {
369           MTrackDB::q('insert into project_repo_link (projid, repoid, repopathregex) values (?, ?, ?)', $link[0], $this->repoid, $link[1]);
370         }
371         foreach ($this->links_to_remove as $linkid) {
372           MTrackDB::q('delete from project_repo_link where repoid = ? and linkid = ?', $this->repoid, $linkid);
373         }
374   }
375
376     function getLinks()
377   {
378     if ($this->links === null) {
379       $this->links = array();
380       foreach (MTrackDB::q('select linkid, projid, repopathregex
381           from project_repo_link where repoid = ? order by repopathregex',
382           $this->repoid)->fetchAll() as $row) {
383         $this->links[$row[0]] = array($row[1], $row[2]);
384       }
385     }
386     return $this->links;
387   }
388
389     function addLink($proj, $regex)
390   {
391     if ($proj instanceof MTrackProject) {
392       $this->links_to_add[] = array($proj->projid, $regex);
393     } else {
394       $this->links_to_add[] = array($proj, $regex);
395     }
396   }
397
398     function removeLink($linkid)
399   {
400     $this->links_to_remove[$linkid] = $linkid;
401   }
402
403     function getBranches() {}
404     function getTags() {}
405     function readdir($path, $object = null, $ident = null) {}
406     function file($path, $object = null, $ident = null) {}
407     function history($path, $limit = null, $object = null, $ident = null){}
408     function diff($path, $from = null, $to = null) {}
409     function getRelatedChanges($revision) {}
410
411     function projectFromPath($filename)
412     {
413     static $links = array();
414     if (!isset($links[$this->repoid]) || $links[$this->repoid] === null) {
415       $links[$this->repoid] = array();
416       foreach (MTrackDB::q(
417         'select projid, repopathregex from project_repo_link where repoid = ?',
418             $this->repoid) as $row) {
419         $re = str_replace('/', '\\/', $row[1]);
420         $links[$this->repoid][] = array($row[0], "/$re/");
421       }
422     }
423     if (is_array($filename)) {
424       $proj_incidence = array();
425       foreach ($filename as $file) {
426         $proj = $this->projectFromPath($file);
427         if ($proj === null) continue;
428         if (isset($proj_incidence[$proj])) {
429           $proj_incidence[$proj]++;
430         } else {
431           $proj_incidence[$proj] = 1;
432         }
433       }
434       $the_proj = null;
435       $the_proj_count = 0;
436       foreach ($proj_incidence as $proj => $count) {
437         if ($count > $the_proj_count) {
438           $the_proj_count = $count;
439           $the_proj = $proj;
440         }
441       }
442       return $the_proj;
443     }
444
445     if ($filename instanceof MTrackSCMFileEvent) {
446       $filename = $filename->name;
447     }
448
449     // walk through the regexes; take the longest match as definitive
450     $longest = null;
451     $longest_id = null;
452     if ($filename[0] != '/') {
453       $filename = '/' . $filename;
454     }
455     foreach ($links[$this->repoid] as $link) {
456       if (preg_match($link[1], $filename, $M)) {
457         if (strlen($M[0]) > strlen($longest)) {
458           $longest = $M[0];
459           $longest_id = $link[0];
460         }
461       }
462     }
463     return $longest_id;
464   }
465   
466     function historyWithChangelog($path, $limit = null, $object = null,   $ident = null) 
467     {
468       
469         $ents = $this->history($path, $limit, $object, $ident);
470         $data = new StdClass;
471         if (!count($ents)) {
472             $data->ent = null;
473             return $data;
474         }
475         $ent = $ents[0];
476         $data->ent = $ent;
477
478         // Determine project from the file list
479         $the_proj = $this->projectFromPath($ent->files);
480         if ($the_proj > 1) {
481           $proj = MTrackProject::loadById($the_proj);
482           $changelog = $proj->adjust_links($ent->changelog, true);
483         } else {
484           $changelog = $ent->changelog;
485         }
486         $data->changelog = $changelog;
487
488         //if (is_array($ent->files)) foreach ($ent->files as $file) {
489         //  $file->diff = mtrack_diff($repo->diff($file, $ent->rev));
490         //}
491       
492
493         return $data;
494     }
495     function historyWithChangelogAndDiff($path, $limit = null, $object = null,   $ident = null)
496     {
497         $ret = $this->historyWithChangelog($path, $limit, $object, $ident);
498         if (!$ret->ent) {
499             return $ret;
500         }
501         if (!is_array($ret->ent->files)) {
502             return $ret;
503         }
504         foreach ($ret->ent->files as $file) {
505             // where is mtrack_diff...
506             $file->diff = mtrack_diff($this->diff($file, $ret->ent->rev));
507         }
508       
509
510         return $data;
511     }
512     // rendering..
513     
514     function displayName()
515     {
516         // fixme - this code needs to be in here.. rather than in SCM?
517         return MTrackSCM::makeDisplayName($this);
518     }
519     function descriptionToHtml()
520     {
521         return  MTrack_Wiki::format_to_html($this->description);
522     }
523     
524     static function defaultRepo($cfg = null)
525     {
526         static $defrepo = null;
527         if ($defrepo !== null) {
528             return $defrepo; // already to it..
529         }
530         
531         $defrepo = $cfg;
532         if ($defrepo !== null) {
533             $defrepo = strpos($defrepo, '/') === false ?  'default/' . $defrepo :  $defrepo;
534             return $defrepo;
535         }
536         
537         $defrepo = '';
538         $q = MTrackDB::q( 'select parent, shortname from repos order by shortname');
539         foreach($q->fetchAll() as $row) {
540             $defrepo = MTrackSCM::makeDisplayName($row);
541             return $defrepo;
542         }
543         return '';
544         
545     }
546     
547 }