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 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 }