1 <?php # vim:ts=2:sw=2:et:
2 /* For licensing and copyright terms, see the file named LICENSE */
5 /** Revision or changeset identifier for this particular file */
8 /** commit message associated with this revision */
11 /** who committed this revision */
14 /** when this revision was committed */
17 /** files affected in this event; may be null, but otherwise
18 * will be an array of MTrackSCMFileEvent */
22 class MTrackSCMFileEvent {
23 /** Name of affected file */
25 /** Change status indicator */
28 /** when used in a string context, just return the filename.
29 * This simplifies explicit object vs. string interpretation
30 * throughout the SCM layer */
31 function __toString() {
36 class MTrackSCMAnnotation {
37 /** Revision of changeset identifier for when line was changed */
40 /** who made the change */
43 /** the content from that line of the file.
44 * This is null unless $include_line_content was set to true when annotate()
49 abstract class MTrackSCMFile {
50 /** reference to the associated MTrackSCM object */
53 /** full path to file, with a leading slash (which represents
54 * the root of its respective repo */
57 /** if true, this file represents a directory */
58 public $is_dir = false;
63 function __construct(MTrackSCM $repo, $name, $rev, $is_dir = false)
68 $this->is_dir = $is_dir;
71 /** Returns an MTrackSCMEvent corresponding to this revision of
73 abstract public function getChangeEvent();
75 /** Returns a stream representing the contents of the file at
77 abstract public function cat();
79 /** Returns an array of MTrackSCMAnnotation objects that correspond to
80 * each line of file content, annotating when the line was last
81 * changed. The array is keyed by line number, 1-based. */
82 abstract public function annotate($include_line_content = false);
85 abstract class MTrackSCMWorkingCopy {
88 /** returns the root dir of the working copy */
93 /** add a file to the working copy */
94 abstract function addFile($path);
95 /** removes a file from the working copy */
96 abstract function delFile($path);
97 /** commit changes that are pending in the working copy */
98 abstract function commit(MTrackChangeset $CS);
99 /** get an MTrackSCMFile representation of a file */
100 abstract function getFile($path);
102 /** enumerates files in a path in the working copy */
103 function enumFiles($path)
105 return scandir($this->dir . DIRECTORY_SEPARATOR . $path);
108 /** determines if a file exists in the working copy */
109 function file_exists($path)
111 return file_exists($this->dir . DIRECTORY_SEPARATOR . $path);
114 function __destruct()
116 if (strlen($this->dir) > 1) {
117 mtrack_rmdir($this->dir);
122 abstract class MTrackSCM {
123 static $repos = array();
125 static function factory(&$repopath) {
126 /* [ / owner type rest ] */
127 $bits = explode('/', $repopath, 4);
128 if (count($bits) < 3) {
129 throw new Exception("Invalid repo $repopath");
132 list($owner, $type) = $bits;
133 $repo = "$owner/$type";
135 $r = MTrackRepo::loadByName($repo);
137 throw new Exception("invalid repo $repo");
139 $repopath = isset($bits[2]) ? $bits[2] : '';
143 /** Returns an array keyed by possible branch names.
144 * The data associated with the branches is implementation
146 * If the SCM does not have a concept of first-class branch
147 * objects, this function returns null */
148 abstract public function getBranches();
150 /** Returns an array keyed by possible tag names.
151 * The data associated with the tags is implementation
153 * If the SCM does not have a concept of first-class tag
154 * objects, this function returns null */
155 abstract public function getTags();
157 /** Enumerates the files/dirs that are present in the specified
158 * location of the repository that match the specified revision,
159 * branch or tag information. If no revision, branch or tag is
160 * specified, then the appropriate default is assumed.
162 * The second and third parameters are optional; the second
163 * parameter is one of 'rev', 'branch', or 'tag', and if specifed
164 * the third parameter must be the corresponding revision, branch
167 * The return value is an array of MTrackSCMFile objects present
168 * at that location/revision of the repository.
170 abstract public function readdir($path, $object = null, $ident = null);
172 /** Queries information on a specific file in the repository.
174 * Parameters are as for readdir() above.
176 * This function returns a single MTrackSCMFile for the location
179 abstract public function file($path, $object = null, $ident = null);
181 /** Queries history for a particular location in the repo.
183 * Parameters are as for readdir() above, except that path can be
184 * left unspecified to query the history for the entire repo.
186 * The limit parameter limits the number of entries returned; it it is
187 * a number, it specifies the number of events, otherwise it is assumed
188 * to be a date in the past; only events since that date will be returned.
190 * Returns an array of MTrackSCMEvent objects.
192 abstract public function history($path, $limit = null, $object = null,
195 /** Obtain the diff text representing a change to a file.
197 * You may optionally provide one or two revisions as context.
199 * If no revisions are passed in, then the change associated
200 * with the location will be assumed.
202 * If one revision is passed, then the change associated with
203 * that event will be assumed.
205 * If two revisions are passed, then the difference between
206 * the two events will be assumed.
208 abstract public function diff($path, $from = null, $to = null);
210 /** Determine the next and previous revisions for a given
213 * Returns an array: the 0th element is an array of prior revisions,
214 * and the 1st element is an array of successor revisions.
216 * There will usually be one prior and one successor revision for a
217 * given change, but some SCMs will return multiples in the case of
220 abstract public function getRelatedChanges($revision);
222 /** Returns a working copy object for the repo
224 * The intended purpose is to support wiki page modifications, and
225 * as such, is not meant to be an especially efficient means to do so.
227 abstract public function getWorkingCopy();
229 /** Returns the default 'root' location in the repository.
230 * For SCMs that have a concept of branches, this is the empty string.
231 * For SCMs like SVN, this is the trunk dir */
232 public function getDefaultRoot() {
236 /** Returns meta information about the SCM type; this is used in the
237 * UI and tooling to let the user know their options.
239 * Returns an array with the following keys:
240 * 'name' => 'Mercurial', // human displayable name
241 * 'tools' => array('hg'), // list of tools to find during setup
243 abstract public function getSCMMetaData();
245 /* takes an MTrackSCM as a parameter because in some bootstrapping
246 * cases, we're actually MTrackRepo and not the end-class.
247 * MTrackRepo calls the end-class method and passes itself in for
249 public function reconcileRepoSettings(MTrackSCM $r) {
251 "Creating/updating a repo of type $this->scmtype is not implemented");
254 static function makeBreadcrumbs($pi) {
261 $crumbs = explode('/', $pi);
266 static function makeDisplayName($data) {
269 if (is_object($data)) {
270 $parent = $data->parent;
271 $name = $data->shortname;
272 } else if (is_array($data)) {
273 $parent = $data['parent'];
274 $name = $data['shortname'];
277 list($type, $owner) = explode(':', $parent);
278 return "$owner/$name";
280 return "default/$name";
283 public function getBrowseRootName() {
284 return self::makeDisplayName($this);
287 public function resolveRevision($rev, $object, $ident) {
291 if ($object === null) {
299 $branches = $this->getBranches();
300 $rev = isset($branches[$ident]) ? $branches[$ident] : null;
303 $tags = $this->getTags();
304 $rev = isset($tags[$ident]) ? $tags[$ident] : null;
309 "don't know which revision to use ($rev,$object,$ident)");
314 MTrackACL::registerAncestry('repo', 'Browser');
315 MTrackWatch::registerEventTypes('repo', array(
316 'ticket' => 'Tickets',
317 'changeset' => 'Code changes'
320 class MTrackRepo extends MTrackSCM {
321 public $repoid = null;
322 public $shortname = null;
323 public $scmtype = null;
324 public $repopath = null;
325 public $browserurl = null;
326 public $browsertype = null;
327 public $description = null;
329 public $clonedfrom = null;
330 public $serverurl = null;
331 private $links_to_add = array();
332 private $links_to_remove = array();
333 private $links = null;
334 static $scms = array();
336 static function registerSCM($scmtype, $classname) {
337 self::$scms[$scmtype] = $classname;
339 static function getAvailableSCMs() {
341 foreach (self::$scms as $t => $classname) {
348 public function reconcileRepoSettings() {
349 if (!isset(self::$scms[$this->scmtype])) {
350 throw new Exception("invalid scm type $this->scmtype");
352 $c = self::$scms[$this->scmtype];
354 $s->reconcileRepoSettings($this);
357 public function getSCMMetaData() {
361 static function loadById($id) {
362 list($row) = MTrackDB::q(
363 'select repoid, scmtype from repos where repoid = ?',
365 if (isset($row[0])) {
367 if (isset(self::$scms[$type])) {
368 $class = self::$scms[$type];
369 return new $class($row[0]);
371 throw new Exception("unsupported repo type $type");
376 static function loadByName($name) {
377 $bits = explode('/', $name);
378 if (count($bits) > 1 && $bits[0] == 'default') {
382 if (count($bits) > 1) {
383 /* wez/reponame -> per user repo */
384 $u = "user:$bits[0]";
385 $p = "project:$bits[0]";
387 'select repoid, scmtype from repos where shortname = ? and (parent = ? OR parent = ?)',
388 $bits[1], $u, $p)->fetchAll();
391 "select repoid, scmtype from repos where shortname = ? and parent =''",
394 if (is_array($rows) && isset($rows[0])) {
396 if (isset($row[0])) {
398 if (isset(self::$scms[$type])) {
399 $class = self::$scms[$type];
400 return new $class($row[0]);
402 throw new Exception("unsupported repo type $type");
408 function getServerURL() {
409 if ($this->serverurl) {
410 return $this->serverurl;
412 $url = MTrackConfig::get('repos', "$this->scmtype.serverurl");
414 return $url . $this->getBrowseRootName();
419 function getCheckoutCommand() {
420 $url = $this->getServerURL();
422 return $this->scmtype . ' clone ' . $this->getServerURL();
431 static function loadByLocation($path) {
432 list($row) = MTrackDB::q('select repoid, scmtype from repos where repopath = ?', $path)->fetchAll();
433 if (isset($row[0])) {
435 if (isset(self::$scms[$type])) {
436 $class = self::$scms[$type];
437 return new $class($row[0]);
439 throw new Exception("unsupported repo type $type");
444 public function getWorkingCopy() {
445 throw new Exception("cannot getWorkingCopy from a generic repo object");
448 function __construct($id = null) {
450 list($row) = MTrackDB::q(
451 'select * from repos where repoid = ?',
453 if (isset($row[0])) {
454 $this->repoid = $row['repoid'];
455 $this->shortname = $row['shortname'];
456 $this->scmtype = $row['scmtype'];
457 $this->repopath = $row['repopath'];
458 $this->browserurl = $row['browserurl'];
459 $this->browsertype = $row['browsertype'];
460 $this->description = $row['description'];
461 $this->parent = $row['parent'];
462 $this->clonedfrom = $row['clonedfrom'];
463 $this->serverurl = $row['serverurl'];
466 throw new Exception("unable to find repo with id = $id");
470 function deleteRepo(MTrackChangeset $CS) {
471 MTrackDB::q('delete from repos where repoid = ?', $this->repoid);
472 mtrack_rmdir($this->repopath);
475 function save(MTrackChangeset $CS) {
476 if (!isset(self::$scms[$this->scmtype])) {
477 throw new Exception("unsupported repo type " . $this->scmtype);
481 list($row) = MTrackDB::q(
482 'select * from repos where repoid = ?',
483 $this->repoid)->fetchAll();
486 'update repos set shortname = ?, scmtype = ?, repopath = ?,
487 browserurl = ?, browsertype = ?, description = ?,
488 parent = ?, serverurl = ?, clonedfrom = ? where repoid = ?',
489 $this->shortname, $this->scmtype, $this->repopath,
490 $this->browserurl, $this->browsertype, $this->description,
491 $this->parent, $this->serverurl, $this->clonedfrom, $this->repoid);
495 if (!strlen($this->repopath)) {
496 if (!MTrackConfig::get('repos', 'allow_user_repo_creation')) {
497 throw new Exception("configuration does not allow repo creation");
499 $repodir = MTrackConfig::get('repos', 'basedir');
500 if ($repodir == null) {
501 $repodir = MTrackConfig::get('core', 'vardir') . '/repos';
503 if (!is_dir($repodir)) {
507 if (!$this->parent) {
508 $owner = mtrack_canon_username(MTrackAuth::whoami());
509 $this->parent = 'user:' . $owner;
511 list($type, $owner) = explode(':', $this->parent, 2);
514 $P = MTrackProject::loadByName($owner);
516 throw new Exception("invalid project $owner");
518 MTrackACL::requireAllRights("project:$P->projid", 'modify');
521 if ($owner != mtrack_canon_username(MTrackAuth::whoami())) {
522 throw new Exception("can't make a repo for another user");
526 throw new Exception("invalid parent ($this->parent)");
529 if (preg_match("/[^a-zA-Z0-9_.-]/", $owner)) {
530 throw new Exception("$owner must not contain special characters");
532 $this->repopath = $repodir . DIRECTORY_SEPARATOR . $owner;
533 if (!is_dir($this->repopath)) {
534 mkdir($this->repopath);
536 $this->repopath .= DIRECTORY_SEPARATOR . $this->shortname;
538 /* default ACL is allow user all rights, block everybody else */
540 array($owner, 'read', 1),
541 array($owner, 'modify', 1),
542 array($owner, 'delete', 1),
543 array($owner, 'checkout', 1),
544 array($owner, 'commit', 1),
545 array('*', 'read', 0),
546 array('*', 'modify', 0),
547 array('*', 'delete', 0),
548 array('*', 'checkout', 0),
549 array('*', 'commit', 0),
553 MTrackDB::q('insert into repos (shortname, scmtype,
554 repopath, browserurl, browsertype, description, parent,
555 serverurl, clonedfrom)
556 values (?, ?, ?, ?, ?, ?, ?, ?, ?)',
557 $this->shortname, $this->scmtype, $this->repopath,
558 $this->browserurl, $this->browsertype, $this->description,
559 $this->parent, $this->serverurl, $this->clonedfrom);
561 $this->repoid = MTrackDB::lastInsertId('repos', 'repoid');
565 MTrackACL::setACL("repo:$this->repoid", 0, $acl);
566 $me = mtrack_canon_username(MTrackAuth::whoami());
567 foreach (array('ticket', 'changeset') as $e) {
569 'insert into watches (otype, oid, userid, medium, event, active) values (?, ?, ?, ?, ?, 1)',
570 'repo', $this->repoid, $me, 'email', $e);
574 $this->reconcileRepoSettings();
575 if (!$this->parent) {
576 /* for SSH access, populate a symlink from the repos basedir to the
577 * actual path for this repo */
578 $repodir = MTrackConfig::get('repos', 'basedir');
579 if ($repodir == null) {
580 $repodir = MTrackConfig::get('core', 'vardir') . '/repos';
582 if (!is_dir($repodir)) {
585 $repodir .= '/default';
586 if (!is_dir($repodir)) {
589 $repodir .= '/' . $this->shortname;
590 if (!file_exists($repodir)) {
591 symlink($this->repopath, $repodir);
592 } else if (is_link($repodir) && readlink($repodir) != $this->repopath) {
594 symlink($this->repopath, $repodir);
597 $CS->add("repo:" . $this->repoid . ":shortname", $old['shortname'], $this->shortname);
598 $CS->add("repo:" . $this->repoid . ":scmtype", $old['scmtype'], $this->scmtype);
599 $CS->add("repo:" . $this->repoid . ":repopath", $old['repopath'], $this->repopath);
600 $CS->add("repo:" . $this->repoid . ":browserurl", $old['browserurl'], $this->browserurl);
601 $CS->add("repo:" . $this->repoid . ":browsertype", $old['browsertype'], $this->browsertype);
602 $CS->add("repo:" . $this->repoid . ":description", $old['description'], $this->description);
603 $CS->add("repo:" . $this->repoid . ":parent", $old['parent'], $this->parent);
604 $CS->add("repo:" . $this->repoid . ":clonedfrom", $old['clonedfrom'], $this->clonedfrom);
605 $CS->add("repo:" . $this->repoid . ":serverurl", $old['serverurl'], $this->serverurl);
607 foreach ($this->links_to_add as $link) {
608 MTrackDB::q('insert into project_repo_link (projid, repoid, repopathregex) values (?, ?, ?)', $link[0], $this->repoid, $link[1]);
610 foreach ($this->links_to_remove as $linkid) {
611 MTrackDB::q('delete from project_repo_link where repoid = ? and linkid = ?', $this->repoid, $linkid);
617 if ($this->links === null) {
618 $this->links = array();
619 foreach (MTrackDB::q('select linkid, projid, repopathregex
620 from project_repo_link where repoid = ? order by repopathregex',
621 $this->repoid)->fetchAll() as $row) {
622 $this->links[$row[0]] = array($row[1], $row[2]);
628 function addLink($proj, $regex)
630 if ($proj instanceof MTrackProject) {
631 $this->links_to_add[] = array($proj->projid, $regex);
633 $this->links_to_add[] = array($proj, $regex);
637 function removeLink($linkid)
639 $this->links_to_remove[$linkid] = $linkid;
642 public function getBranches() {}
643 public function getTags() {}
644 public function readdir($path, $object = null, $ident = null) {}
645 public function file($path, $object = null, $ident = null) {}
646 public function history($path, $limit = null, $object = null, $ident = null){}
647 public function diff($path, $from = null, $to = null) {}
648 public function getRelatedChanges($revision) {}
650 function projectFromPath($filename) {
651 static $links = array();
652 if (!isset($links[$this->repoid]) || $links[$this->repoid] === null) {
653 $links[$this->repoid] = array();
654 foreach (MTrackDB::q(
655 'select projid, repopathregex from project_repo_link where repoid = ?',
656 $this->repoid) as $row) {
657 $re = str_replace('/', '\\/', $row[1]);
658 $links[$this->repoid][] = array($row[0], "/$re/");
661 if (is_array($filename)) {
662 $proj_incidence = array();
663 foreach ($filename as $file) {
664 $proj = $this->projectFromPath($file);
665 if ($proj === null) continue;
666 if (isset($proj_incidence[$proj])) {
667 $proj_incidence[$proj]++;
669 $proj_incidence[$proj] = 1;
674 foreach ($proj_incidence as $proj => $count) {
675 if ($count > $the_proj_count) {
676 $the_proj_count = $count;
683 if ($filename instanceof MTrackSCMFileEvent) {
684 $filename = $filename->name;
687 // walk through the regexes; take the longest match as definitive
690 if ($filename[0] != '/') {
691 $filename = '/' . $filename;
693 foreach ($links[$this->repoid] as $link) {
694 if (preg_match($link[1], $filename, $M)) {
695 if (strlen($M[0]) > strlen($longest)) {
697 $longest_id = $link[0];