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