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     
25     
26     private $links_to_add = array();
27     private $links_to_remove = array();
28     private $links = null;
29     static $scms = array();
30     /**
31      *load class and create instance using array as properties
32      */
33     
34     static function factory($ar)
35     {
36         print_r($ar);
37         $type = ucfirst($ar['scmtype']);
38         $fn = 'MTrack/SCM/'.$type .'/Repo.php';
39         $cls = 'MTrack_SCM_'.$type .'_Repo';
40         require_once $fn;
41         
42         $ret = new $cls($ar);
43         
44         return $ret;
45         
46     }
47     
48  
49     static function getAvailableSCMs()
50     {
51         $ret = array();
52         $ar = scandir(dirname(__FILE__).'/SCM');
53         
54         foreach($ar as $a) {
55             if (empty($a) || $a[0] == '.') {
56                 continue;
57             }
58             $fn = dirname(__FILE__).'/SCM/'.$a.'/Repo.php';
59             if (!file_exists($fn)) {
60                 continue;
61             } 
62             $ret[$a] = MTrack_Repo::factory(array('scmtype'=> $a));
63             
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($ar = null) {
170         
171         if (!is_array($ar)) {
172             return; // can accept empty ctrs
173         }
174         foreach($ar as $k=>$v) {
175             $this->$k = $v;
176         }
177         
178     }
179     
180     function reconcileRepoSettings()
181     {
182         $c = self::Factory(array('scmtype'=>$this->scmtype));
183         $s->reconcileRepoSettings($this);
184     }
185     // these are needed just to implement the abstract interface..
186     function getSCMMetaData() {
187         return null;
188     }
189     
190     function getServerURL()
191     {
192         if ($this->serverurl) {
193             return $this->serverurl;
194         }
195         /*
196         $url = MTrackConfig::get('repos', "$this->scmtype.serverurl");
197         if ($url) {
198           return $url . $this->getBrowseRootName();
199         }
200         */
201         return null;
202     }
203
204     function getCheckoutCommand() {
205         $url = $this->getServerURL();
206         if (strlen($url)) {
207           return $this->scmtype . ' clone ' . $this->getServerURL();
208         }
209         return null;
210     }
211
212     function canFork() {
213     return false;
214   }
215
216     function getWorkingCopy() {
217     throw new Exception("cannot getWorkingCopy from a generic repo object");
218   }
219
220     function deleteRepo(MTrackChangeset $CS) {
221     MTrackDB::q('delete from repos where repoid = ?', $this->repoid);
222     mtrack_rmdir($this->repopath);
223   }
224
225     function save(MTrackChangeset $CS) {
226         if (!isset(self::$scms[$this->scmtype])) {
227           throw new Exception("unsupported repo type " . $this->scmtype);
228         }
229     
230         if ($this->repoid) {
231           list($row) = MTrackDB::q(
232                         'select * from repos where repoid = ?',
233                         $this->repoid)->fetchAll();
234           $old = $row;
235           MTrackDB::q(
236               'update repos set shortname = ?, scmtype = ?, repopath = ?,
237                 browserurl = ?, browsertype = ?, description = ?,
238                 parent = ?, serverurl = ?, clonedfrom = ? where repoid = ?',
239               $this->shortname, $this->scmtype, $this->repopath,
240               $this->browserurl, $this->browsertype, $this->description,
241               $this->parent, $this->serverurl, $this->clonedfrom, $this->repoid);
242         } else {
243           $acl = null;
244     
245           if (!strlen($this->repopath)) {
246             if (!MTrackConfig::get('repos', 'allow_user_repo_creation')) {
247               throw new Exception("configuration does not allow repo creation");
248             }
249             $repodir = MTrackConfig::get('repos', 'basedir');
250             if ($repodir == null) {
251               $repodir = MTrackConfig::get('core', 'vardir') . '/repos';
252             }
253             if (!is_dir($repodir)) {
254               mkdir($repodir);
255             }
256     
257             if (!$this->parent) {
258               $owner = mtrack_canon_username(MTrackAuth::whoami());
259               $this->parent = 'user:' . $owner;
260             } else {
261               list($type, $owner) = explode(':', $this->parent, 2);
262               switch ($type) {
263                 case 'project':
264                   $P = MTrackProject::loadByName($owner);
265                   if (!$P) {
266                     throw new Exception("invalid project $owner");
267                   }
268                   MTrackACL::requireAllRights("project:$P->projid", 'modify');
269                   break;
270                 case 'user':
271                   if ($owner != mtrack_canon_username(MTrackAuth::whoami())) {
272                     throw new Exception("can't make a repo for another user");
273                   }
274                   break;
275                 default:
276                   throw new Exception("invalid parent ($this->parent)");
277               }
278             }
279             if (preg_match("/[^a-zA-Z0-9_.-]/", $owner)) {
280               throw new Exception("$owner must not contain special characters");
281             }
282             $this->repopath = $repodir . DIRECTORY_SEPARATOR . $owner;
283             if (!is_dir($this->repopath)) {
284               mkdir($this->repopath);
285             }
286             $this->repopath .= DIRECTORY_SEPARATOR . $this->shortname;
287     
288             /* default ACL is allow user all rights, block everybody else */
289             $acl = array(
290               array($owner, 'read', 1),
291               array($owner, 'modify', 1),
292               array($owner, 'delete', 1),
293               array($owner, 'checkout', 1),
294               array($owner, 'commit', 1),
295               array('*', 'read', 0),
296               array('*', 'modify', 0),
297               array('*', 'delete', 0),
298               array('*', 'checkout', 0),
299               array('*', 'commit', 0),
300             );
301           }
302     
303           MTrackDB::q('insert into repos (shortname, scmtype,
304               repopath, browserurl, browsertype, description, parent,
305               serverurl, clonedfrom)
306               values (?, ?, ?, ?, ?, ?, ?, ?, ?)',
307               $this->shortname, $this->scmtype, $this->repopath,
308               $this->browserurl, $this->browsertype, $this->description,
309               $this->parent, $this->serverurl, $this->clonedfrom);
310     
311           $this->repoid = MTrackDB::lastInsertId('repos', 'repoid');
312           $old = null;
313     
314           if ($acl !== null) {
315             MTrackACL::setACL("repo:$this->repoid", 0, $acl);
316             $me = mtrack_canon_username(MTrackAuth::whoami());
317             foreach (array('ticket', 'changeset') as $e) {
318               MTrackDB::q(
319                 'insert into watches (otype, oid, userid, medium, event, active) values (?, ?, ?, ?, ?, 1)',
320               'repo', $this->repoid, $me, 'email', $e);
321             }
322           }
323         }
324         $this->reconcileRepoSettings();
325         if (!$this->parent) {
326           /* for SSH access, populate a symlink from the repos basedir to the
327            * actual path for this repo */
328           $repodir = MTrackConfig::get('repos', 'basedir');
329           if ($repodir == null) {
330             $repodir = MTrackConfig::get('core', 'vardir') . '/repos';
331           }
332           if (!is_dir($repodir)) {
333             mkdir($repodir);
334           }
335           $repodir .= '/default';
336           if (!is_dir($repodir)) {
337             mkdir($repodir);
338           }
339           $repodir .= '/' . $this->shortname;
340           if (!file_exists($repodir)) {
341             symlink($this->repopath, $repodir);
342           } else if (is_link($repodir) && readlink($repodir) != $this->repopath) {
343             unlink($repodir);
344             symlink($this->repopath, $repodir);
345           }
346         }
347         $CS->add("repo:" . $this->repoid . ":shortname", $old['shortname'], $this->shortname);
348         $CS->add("repo:" . $this->repoid . ":scmtype", $old['scmtype'], $this->scmtype);
349         $CS->add("repo:" . $this->repoid . ":repopath", $old['repopath'], $this->repopath);
350         $CS->add("repo:" . $this->repoid . ":browserurl", $old['browserurl'], $this->browserurl);
351         $CS->add("repo:" . $this->repoid . ":browsertype", $old['browsertype'], $this->browsertype);
352         $CS->add("repo:" . $this->repoid . ":description", $old['description'], $this->description);
353         $CS->add("repo:" . $this->repoid . ":parent", $old['parent'], $this->parent);
354         $CS->add("repo:" . $this->repoid . ":clonedfrom", $old['clonedfrom'], $this->clonedfrom);
355         $CS->add("repo:" . $this->repoid . ":serverurl", $old['serverurl'], $this->serverurl);
356     
357         foreach ($this->links_to_add as $link) {
358           MTrackDB::q('insert into project_repo_link (projid, repoid, repopathregex) values (?, ?, ?)', $link[0], $this->repoid, $link[1]);
359         }
360         foreach ($this->links_to_remove as $linkid) {
361           MTrackDB::q('delete from project_repo_link where repoid = ? and linkid = ?', $this->repoid, $linkid);
362         }
363   }
364
365     function getLinks()
366   {
367     if ($this->links === null) {
368       $this->links = array();
369       foreach (MTrackDB::q('select linkid, projid, repopathregex
370           from project_repo_link where repoid = ? order by repopathregex',
371           $this->repoid)->fetchAll() as $row) {
372         $this->links[$row[0]] = array($row[1], $row[2]);
373       }
374     }
375     return $this->links;
376   }
377
378     function addLink($proj, $regex)
379   {
380     if ($proj instanceof MTrackProject) {
381       $this->links_to_add[] = array($proj->projid, $regex);
382     } else {
383       $this->links_to_add[] = array($proj, $regex);
384     }
385   }
386
387     function removeLink($linkid)
388   {
389     $this->links_to_remove[$linkid] = $linkid;
390   }
391
392     function getBranches() {}
393     function getTags() {}
394     function readdir($path, $object = null, $ident = null) {}
395     function file($path, $object = null, $ident = null) {}
396     function history($path, $limit = null, $object = null, $ident = null){}
397     function diff($path, $from = null, $to = null) {}
398     function getRelatedChanges($revision) {}
399
400     function projectFromPath($filename)
401     {
402     static $links = array();
403     if (!isset($links[$this->repoid]) || $links[$this->repoid] === null) {
404       $links[$this->repoid] = array();
405       foreach (MTrackDB::q(
406         'select projid, repopathregex from project_repo_link where repoid = ?',
407             $this->repoid) as $row) {
408         $re = str_replace('/', '\\/', $row[1]);
409         $links[$this->repoid][] = array($row[0], "/$re/");
410       }
411     }
412     if (is_array($filename)) {
413       $proj_incidence = array();
414       foreach ($filename as $file) {
415         $proj = $this->projectFromPath($file);
416         if ($proj === null) continue;
417         if (isset($proj_incidence[$proj])) {
418           $proj_incidence[$proj]++;
419         } else {
420           $proj_incidence[$proj] = 1;
421         }
422       }
423       $the_proj = null;
424       $the_proj_count = 0;
425       foreach ($proj_incidence as $proj => $count) {
426         if ($count > $the_proj_count) {
427           $the_proj_count = $count;
428           $the_proj = $proj;
429         }
430       }
431       return $the_proj;
432     }
433
434     if ($filename instanceof MTrackSCMFileEvent) {
435       $filename = $filename->name;
436     }
437
438     // walk through the regexes; take the longest match as definitive
439     $longest = null;
440     $longest_id = null;
441     if ($filename[0] != '/') {
442       $filename = '/' . $filename;
443     }
444     foreach ($links[$this->repoid] as $link) {
445       if (preg_match($link[1], $filename, $M)) {
446         if (strlen($M[0]) > strlen($longest)) {
447           $longest = $M[0];
448           $longest_id = $link[0];
449         }
450       }
451     }
452     return $longest_id;
453   }
454   
455     function historyWithChangelog($path, $limit = null, $object = null,   $ident = null) 
456     {
457       
458         $ents = $this->history($path, $limit, $object, $ident);
459         $data = new StdClass;
460         if (!count($ents)) {
461             $data->ent = null;
462             return $data;
463         }
464         $ent = $ents[0];
465         $data->ent = $ent;
466
467         // Determine project from the file list
468         $the_proj = $this->projectFromPath($ent->files);
469         if ($the_proj > 1) {
470           $proj = MTrackProject::loadById($the_proj);
471           $changelog = $proj->adjust_links($ent->changelog, true);
472         } else {
473           $changelog = $ent->changelog;
474         }
475         $data->changelog = $changelog;
476
477         //if (is_array($ent->files)) foreach ($ent->files as $file) {
478         //  $file->diff = mtrack_diff($repo->diff($file, $ent->rev));
479         //}
480       
481
482         return $data;
483     }
484     function historyWithChangelogAndDiff($path, $limit = null, $object = null,   $ident = null)
485     {
486         $ret = $this->historyWithChangelog($path, $limit, $object, $ident);
487         if (!$ret->ent) {
488             return $ret;
489         }
490         if (!is_array($ret->ent->files)) {
491             return $ret;
492         }
493         foreach ($ret->ent->files as $file) {
494             // where is mtrack_diff...
495             $file->diff = mtrack_diff($this->diff($file, $ret->ent->rev));
496         }
497       
498
499         return $data;
500     }
501     // rendering..
502     
503     function displayName()
504     {
505         // fixme - this code needs to be in here.. rather than in SCM?
506         return MTrackSCM::makeDisplayName($this);
507     }
508     function descriptionToHtml()
509     {
510         return  MTrack_Wiki::format_to_html($this->description);
511     }
512     
513     static function defaultRepo($cfg = null)
514     {
515         static $defrepo = null;
516         if ($defrepo !== null) {
517             return $defrepo; // already to it..
518         }
519         
520         $defrepo = $cfg;
521         if ($defrepo !== null) {
522             $defrepo = strpos($defrepo, '/') === false ?  'default/' . $defrepo :  $defrepo;
523             return $defrepo;
524         }
525         
526         $defrepo = '';
527         $q = MTrackDB::q( 'select parent, shortname from repos order by shortname');
528         foreach($q->fetchAll() as $row) {
529             $defrepo = MTrackSCM::makeDisplayName($row);
530             return $defrepo;
531         }
532         return '';
533         
534     }
535     
536 }