PageRenderTime 63ms CodeModel.GetById 32ms RepoModel.GetById 0ms app.codeStats 0ms

/inc/scm.php

https://bitbucket.org/yoander/mtrack
PHP | 1127 lines | 860 code | 103 blank | 164 comment | 124 complexity | 07d2e2bacb540d6b9b8602a203a5665f MD5 | raw file
Possible License(s): BSD-3-Clause, Apache-2.0
  1. <?php # vim:ts=2:sw=2:et:
  2. /* For licensing and copyright terms, see the file named LICENSE */
  3. class MTrackSCMEvent {
  4. /** Revision or changeset identifier for this particular file */
  5. public $rev;
  6. /** commit message associated with this revision */
  7. public $changelog;
  8. /** who committed this revision */
  9. public $changeby;
  10. /** when this revision was committed */
  11. public $ctime;
  12. /** files affected in this event; may be null, but otherwise
  13. * will be an array of MTrackSCMFileEvent */
  14. public $files;
  15. }
  16. class MTrackSCMFileEvent {
  17. /** Name of affected file */
  18. public $name;
  19. /** Change status indicator */
  20. public $status;
  21. /** when used in a string context, just return the filename.
  22. * This simplifies explicit object vs. string interpretation
  23. * throughout the SCM layer */
  24. function __toString() {
  25. return $this->name;
  26. }
  27. }
  28. class MTrackSCMAnnotation {
  29. /** Revision of changeset identifier for when line was changed */
  30. public $rev;
  31. /** who made the change */
  32. public $changeby;
  33. /** the content from that line of the file.
  34. * This is null unless $include_line_content was set to true when annotate()
  35. * was called */
  36. public $line;
  37. }
  38. abstract class MTrackSCMFile {
  39. /** reference to the associated MTrackSCM object */
  40. public $repo;
  41. /** full path to file, with a leading slash (which represents
  42. * the root of its respective repo */
  43. public $name;
  44. /** if true, this file represents a directory */
  45. public $is_dir = false;
  46. /** revision */
  47. public $rev;
  48. function __construct(MTrackSCM $repo, $name, $rev, $is_dir = false)
  49. {
  50. $this->repo = $repo;
  51. $this->name = $name;
  52. $this->rev = $rev;
  53. $this->is_dir = $is_dir;
  54. }
  55. /** Returns an MTrackSCMEvent corresponding to this revision of
  56. * the file */
  57. abstract public function getChangeEvent();
  58. /** Returns a stream representing the contents of the file at
  59. * this revision */
  60. abstract public function cat();
  61. /** Returns an array of MTrackSCMAnnotation objects that correspond to
  62. * each line of file content, annotating when the line was last
  63. * changed. The array is keyed by line number, 1-based. */
  64. abstract public function annotate($include_line_content = false);
  65. }
  66. abstract class MTrackSCMWorkingCopy {
  67. public $dir;
  68. /** returns the root dir of the working copy */
  69. function getDir() {
  70. return $this->dir;
  71. }
  72. /** add a file to the working copy */
  73. abstract function addFile($path);
  74. /** removes a file from the working copy */
  75. abstract function delFile($path);
  76. /** commit changes that are pending in the working copy */
  77. abstract function commit(MTrackChangeset $CS);
  78. /** get an MTrackSCMFile representation of a file */
  79. abstract function getFile($path);
  80. /** enumerates files in a path in the working copy */
  81. function enumFiles($path)
  82. {
  83. return scandir($this->dir . DIRECTORY_SEPARATOR . $path);
  84. }
  85. /** determines if a file exists in the working copy */
  86. function file_exists($path)
  87. {
  88. return file_exists($this->dir . DIRECTORY_SEPARATOR . $path);
  89. }
  90. function __destruct()
  91. {
  92. if (strlen($this->dir) > 1) {
  93. mtrack_rmdir($this->dir);
  94. }
  95. }
  96. }
  97. abstract class MTrackSCM {
  98. static $repos = array();
  99. static function factory(&$repopath) {
  100. /* [ / owner type rest ] */
  101. $bits = explode('/', $repopath, 4);
  102. if (count($bits) < 3) {
  103. throw new Exception("Invalid repo $repopath");
  104. }
  105. array_shift($bits);
  106. list($owner, $type) = $bits;
  107. $repo = "$owner/$type";
  108. $r = MTrackRepo::loadByName($repo);
  109. if (!$r) {
  110. throw new Exception("invalid repo $repo");
  111. }
  112. $repopath = isset($bits[2]) ? $bits[2] : '';
  113. return $r;
  114. }
  115. /** Returns an array keyed by possible branch names.
  116. * The data associated with the branches is implementation
  117. * defined.
  118. * If the SCM does not have a concept of first-class branch
  119. * objects, this function returns null */
  120. abstract public function getBranches();
  121. /** Returns an array keyed by possible tag names.
  122. * The data associated with the tags is implementation
  123. * defined.
  124. * If the SCM does not have a concept of first-class tag
  125. * objects, this function returns null */
  126. abstract public function getTags();
  127. /** Enumerates the files/dirs that are present in the specified
  128. * location of the repository that match the specified revision,
  129. * branch or tag information. If no revision, branch or tag is
  130. * specified, then the appropriate default is assumed.
  131. *
  132. * The second and third parameters are optional; the second
  133. * parameter is one of 'rev', 'branch', or 'tag', and if specifed
  134. * the third parameter must be the corresponding revision, branch
  135. * or tag identifier.
  136. *
  137. * The return value is an array of MTrackSCMFile objects present
  138. * at that location/revision of the repository.
  139. */
  140. abstract public function readdir($path, $object = null, $ident = null);
  141. /** Queries information on a specific file in the repository.
  142. *
  143. * Parameters are as for readdir() above.
  144. *
  145. * This function returns a single MTrackSCMFile for the location
  146. * in question.
  147. */
  148. abstract public function file($path, $object = null, $ident = null);
  149. /** Queries history for a particular location in the repo.
  150. *
  151. * Parameters are as for readdir() above, except that path can be
  152. * left unspecified to query the history for the entire repo.
  153. *
  154. * The limit parameter limits the number of entries returned; it it is
  155. * a number, it specifies the number of events, otherwise it is assumed
  156. * to be a date in the past; only events since that date will be returned.
  157. *
  158. * Returns an array of MTrackSCMEvent objects.
  159. */
  160. abstract public function history($path, $limit = null, $object = null,
  161. $ident = null);
  162. /** Obtain the diff text representing a change to a file.
  163. *
  164. * You may optionally provide one or two revisions as context.
  165. *
  166. * If no revisions are passed in, then the change associated
  167. * with the location will be assumed.
  168. *
  169. * If one revision is passed, then the change associated with
  170. * that event will be assumed.
  171. *
  172. * If two revisions are passed, then the difference between
  173. * the two events will be assumed.
  174. */
  175. abstract public function diff($path, $from = null, $to = null);
  176. /** Determine the next and previous revisions for a given
  177. * changeset.
  178. *
  179. * Returns an array: the 0th element is an array of prior revisions,
  180. * and the 1st element is an array of successor revisions.
  181. *
  182. * There will usually be one prior and one successor revision for a
  183. * given change, but some SCMs will return multiples in the case of
  184. * merges.
  185. */
  186. abstract public function getRelatedChanges($revision);
  187. /** Returns a working copy object for the repo
  188. *
  189. * The intended purpose is to support wiki page modifications, and
  190. * as such, is not meant to be an especially efficient means to do so.
  191. */
  192. abstract public function getWorkingCopy();
  193. /** Returns the default 'root' location in the repository.
  194. * For SCMs that have a concept of branches, this is the empty string.
  195. * For SCMs like SVN, this is the trunk dir */
  196. public function getDefaultRoot() {
  197. return '';
  198. }
  199. /** Returns meta information about the SCM type; this is used in the
  200. * UI and tooling to let the user know their options.
  201. *
  202. * Returns an array with the following keys:
  203. * 'name' => 'Mercurial', // human displayable name
  204. * 'tools' => array('hg'), // list of tools to find during setup
  205. */
  206. abstract public function getSCMMetaData();
  207. /* takes an MTrackSCM as a parameter because in some bootstrapping
  208. * cases, we're actually MTrackRepo and not the end-class.
  209. * MTrackRepo calls the end-class method and passes itself in for
  210. * context */
  211. public function reconcileRepoSettings(MTrackSCM $r = null) {
  212. throw new Exception(
  213. "Creating/updating a repo of type $this->scmtype is not implemented");
  214. }
  215. static function makeBreadcrumbs($pi) {
  216. if (!strlen($pi)) {
  217. $pi = '/';
  218. }
  219. if ($pi == '/') {
  220. $crumbs = array('');
  221. } else {
  222. $crumbs = explode('/', $pi);
  223. }
  224. return $crumbs;
  225. }
  226. static function makeDisplayName($data) {
  227. $parent = '';
  228. $name = '';
  229. if (is_object($data)) {
  230. $parent = $data->parent;
  231. $name = $data->shortname;
  232. } else if (is_array($data)) {
  233. $parent = $data['parent'];
  234. $name = $data['shortname'];
  235. }
  236. if ($parent) {
  237. list($type, $owner) = explode(':', $parent);
  238. return "$owner/$name";
  239. }
  240. return "default/$name";
  241. }
  242. public function getBrowseRootName() {
  243. return self::makeDisplayName($this);
  244. }
  245. public function resolveRevision($rev, $object, $ident) {
  246. if ($rev !== null) {
  247. return $rev;
  248. }
  249. if ($object === null) {
  250. return null;
  251. }
  252. switch ($object) {
  253. case 'rev':
  254. $rev = $ident;
  255. break;
  256. case 'branch':
  257. $branches = $this->getBranches();
  258. $rev = isset($branches[$ident]) ? $branches[$ident] : null;
  259. break;
  260. case 'tag':
  261. $tags = $this->getTags();
  262. $rev = isset($tags[$ident]) ? $tags[$ident] : null;
  263. break;
  264. }
  265. if ($rev === null) {
  266. throw new Exception(
  267. "don't know which revision to use ($rev,$object,$ident)");
  268. }
  269. return $rev;
  270. }
  271. }
  272. MTrackACL::registerAncestry('repo', 'Browser');
  273. MTrackWatch::registerEventTypes('repo', array(
  274. 'ticket' => 'Tickets',
  275. 'changeset' => 'Code changes'
  276. ));
  277. class MTrackRepo extends MTrackSCM {
  278. public $repoid = null;
  279. public $shortname = null;
  280. public $scmtype = null;
  281. public $repopath = null;
  282. public $browserurl = null;
  283. public $browsertype = null;
  284. public $description = null;
  285. public $parent = '';
  286. public $clonedfrom = null;
  287. public $serverurl = null;
  288. private $links_to_add = array();
  289. private $links_to_remove = array();
  290. private $links = null;
  291. static $scms = array();
  292. static function registerSCM($scmtype, $classname) {
  293. self::$scms[$scmtype] = $classname;
  294. }
  295. static function getAvailableSCMs() {
  296. $ret = array();
  297. foreach (self::$scms as $t => $classname) {
  298. $o = new $classname;
  299. $ret[$t] = $o;
  300. }
  301. return $ret;
  302. }
  303. public function reconcileRepoSettings(MTrackSCM $ignored = null) {
  304. if (!isset(self::$scms[$this->scmtype])) {
  305. throw new Exception("invalid scm type $this->scmtype");
  306. }
  307. $c = self::$scms[$this->scmtype];
  308. $s = new $c;
  309. $s->reconcileRepoSettings($this);
  310. }
  311. public function getSCMMetaData() {
  312. return null;
  313. }
  314. static function loadById($id) {
  315. list($row) = MTrackDB::q(
  316. 'select repoid, scmtype from repos where repoid = ?',
  317. $id)->fetchAll();
  318. if (isset($row[0])) {
  319. $type = $row[1];
  320. if (isset(self::$scms[$type])) {
  321. $class = self::$scms[$type];
  322. return new $class($row[0]);
  323. }
  324. throw new Exception("unsupported repo type $type");
  325. }
  326. return null;
  327. }
  328. static function loadByName($name) {
  329. $bits = explode('/', $name);
  330. if (count($bits) > 1 && $bits[0] == 'default') {
  331. array_shift($bits);
  332. $name = $bits[0];
  333. }
  334. if (count($bits) > 1) {
  335. /* wez/reponame -> per user repo */
  336. $u = "user:$bits[0]";
  337. $p = "project:$bits[0]";
  338. $rows = MTrackDB::q(
  339. 'select repoid, scmtype from repos where shortname = ? and (parent = ? OR parent = ?)',
  340. $bits[1], $u, $p)->fetchAll();
  341. } else {
  342. $rows = MTrackDB::q(
  343. "select repoid, scmtype from repos where shortname = ? and parent =''",
  344. $name)->fetchAll();
  345. }
  346. if (is_array($rows) && isset($rows[0])) {
  347. $row = $rows[0];
  348. if (isset($row[0])) {
  349. $type = $row[1];
  350. if (isset(self::$scms[$type])) {
  351. $class = self::$scms[$type];
  352. return new $class($row[0]);
  353. }
  354. throw new Exception("unsupported repo type $type");
  355. }
  356. }
  357. return null;
  358. }
  359. function getServerURL() {
  360. if ($this->serverurl) {
  361. return $this->serverurl;
  362. }
  363. $url = MTrackConfig::get('repos', "$this->scmtype.serverurl");
  364. if ($url) {
  365. return $url . $this->getBrowseRootName();
  366. }
  367. return null;
  368. }
  369. function getCheckoutCommand() {
  370. $url = $this->getServerURL();
  371. if (strlen($url)) {
  372. return $this->scmtype . ' clone ' . $this->getServerURL();
  373. }
  374. return null;
  375. }
  376. function canFork() {
  377. return false;
  378. }
  379. static function loadByLocation($path) {
  380. list($row) = MTrackDB::q('select repoid, scmtype from repos where repopath = ?', $path)->fetchAll();
  381. if (isset($row[0])) {
  382. $type = $row[1];
  383. if (isset(self::$scms[$type])) {
  384. $class = self::$scms[$type];
  385. return new $class($row[0]);
  386. }
  387. throw new Exception("unsupported repo type $type");
  388. }
  389. return null;
  390. }
  391. public function getWorkingCopy() {
  392. throw new Exception("cannot getWorkingCopy from a generic repo object");
  393. }
  394. function __construct($id = null) {
  395. if ($id !== null) {
  396. list($row) = MTrackDB::q(
  397. 'select * from repos where repoid = ?',
  398. $id)->fetchAll();
  399. if (isset($row[0])) {
  400. $this->repoid = $row['repoid'];
  401. $this->shortname = $row['shortname'];
  402. $this->scmtype = $row['scmtype'];
  403. $this->repopath = $row['repopath'];
  404. $this->browserurl = $row['browserurl'];
  405. $this->browsertype = $row['browsertype'];
  406. $this->description = $row['description'];
  407. $this->parent = $row['parent'];
  408. $this->clonedfrom = $row['clonedfrom'];
  409. $this->serverurl = $row['serverurl'];
  410. return;
  411. }
  412. throw new Exception("unable to find repo with id = $id");
  413. }
  414. }
  415. function deleteRepo(MTrackChangeset $CS) {
  416. MTrackDB::q('delete from repos where repoid = ?', $this->repoid);
  417. mtrack_rmdir($this->repopath);
  418. }
  419. static function get_repos_dir() {
  420. $repodir = MTrackConfig::get('repos', 'basedir');
  421. if ($repodir == null) {
  422. $repodir = MTrackConfig::get('core', 'vardir') . '/repos';
  423. }
  424. if (!is_dir($repodir)) {
  425. mkdir($repodir);
  426. }
  427. return $repodir;
  428. }
  429. function save(MTrackChangeset $CS) {
  430. if (!isset(self::$scms[$this->scmtype])) {
  431. throw new Exception("unsupported repo type " . $this->scmtype);
  432. }
  433. if (preg_match("/[^a-zA-Z0-9_.-]/", $this->shortname)) {
  434. throw new Exception("repo name must not contain special characters");
  435. }
  436. if ($this->repoid) {
  437. list($row) = MTrackDB::q(
  438. 'select * from repos where repoid = ?',
  439. $this->repoid)->fetchAll();
  440. $old = $row;
  441. MTrackDB::q(
  442. 'update repos set shortname = ?, scmtype = ?, repopath = ?,
  443. browserurl = ?, browsertype = ?, description = ?,
  444. parent = ?, serverurl = ?, clonedfrom = ? where repoid = ?',
  445. $this->shortname, $this->scmtype, $this->repopath,
  446. $this->browserurl, $this->browsertype, $this->description,
  447. $this->parent, $this->serverurl, $this->clonedfrom, $this->repoid);
  448. } else {
  449. $acl = null;
  450. if (!strlen($this->repopath)) {
  451. if (!MTrackConfig::get('repos', 'allow_user_repo_creation')) {
  452. throw new Exception("configuration does not allow repo creation");
  453. }
  454. $repodir = self::get_repos_dir();
  455. if (!$this->parent) {
  456. $owner = mtrack_canon_username(MTrackAuth::whoami());
  457. $this->parent = 'user:' . $owner;
  458. } else {
  459. list($type, $owner) = explode(':', $this->parent, 2);
  460. switch ($type) {
  461. case 'project':
  462. $P = MTrackProject::loadByName($owner);
  463. if (!$P) {
  464. throw new Exception("invalid project $owner");
  465. }
  466. MTrackACL::requireAllRights("project:$P->projid", 'modify');
  467. break;
  468. case 'user':
  469. if ($owner != mtrack_canon_username(MTrackAuth::whoami())) {
  470. throw new Exception("can't make a repo for another user");
  471. }
  472. break;
  473. default:
  474. throw new Exception("invalid parent ($this->parent)");
  475. }
  476. }
  477. if (preg_match("/[^a-zA-Z0-9_.-]/", $owner)) {
  478. throw new Exception("$owner must not contain special characters");
  479. }
  480. $this->repopath = $repodir . DIRECTORY_SEPARATOR . $owner;
  481. if (!is_dir($this->repopath)) {
  482. mkdir($this->repopath);
  483. }
  484. $this->repopath .= DIRECTORY_SEPARATOR . $this->shortname;
  485. /* default ACL is allow user all rights, block everybody else */
  486. $acl = array(
  487. array($owner, 'read', 1),
  488. array($owner, 'modify', 1),
  489. array($owner, 'delete', 1),
  490. array($owner, 'checkout', 1),
  491. array($owner, 'commit', 1),
  492. array('*', 'read', 0),
  493. array('*', 'modify', 0),
  494. array('*', 'delete', 0),
  495. array('*', 'checkout', 0),
  496. array('*', 'commit', 0),
  497. );
  498. }
  499. MTrackDB::q('insert into repos (shortname, scmtype,
  500. repopath, browserurl, browsertype, description, parent,
  501. serverurl, clonedfrom)
  502. values (?, ?, ?, ?, ?, ?, ?, ?, ?)',
  503. $this->shortname, $this->scmtype, $this->repopath,
  504. $this->browserurl, $this->browsertype, $this->description,
  505. $this->parent, $this->serverurl, $this->clonedfrom);
  506. $this->repoid = MTrackDB::lastInsertId('repos', 'repoid');
  507. $old = null;
  508. if ($acl !== null) {
  509. MTrackACL::setACL("repo:$this->repoid", 0, $acl);
  510. $me = mtrack_canon_username(MTrackAuth::whoami());
  511. foreach (array('ticket', 'changeset') as $e) {
  512. MTrackDB::q(
  513. 'insert into watches (otype, oid, userid, medium, event, active) values (?, ?, ?, ?, ?, 1)',
  514. 'repo', $this->repoid, $me, 'email', $e);
  515. }
  516. }
  517. }
  518. $this->reconcileRepoSettings();
  519. if (!$this->parent) {
  520. /* for SSH access, populate a symlink from the repos basedir to the
  521. * actual path for this repo */
  522. $repodir = self::get_repos_dir();
  523. $repodir .= '/default';
  524. if (!is_dir($repodir)) {
  525. mkdir($repodir);
  526. }
  527. $repodir .= '/' . $this->shortname;
  528. if (!file_exists($repodir)) {
  529. symlink($this->repopath, $repodir);
  530. } else if (is_link($repodir) && readlink($repodir) != $this->repopath) {
  531. unlink($repodir);
  532. symlink($this->repopath, $repodir);
  533. }
  534. }
  535. $CS->add("repo:" . $this->repoid . ":shortname", $old['shortname'], $this->shortname);
  536. $CS->add("repo:" . $this->repoid . ":scmtype", $old['scmtype'], $this->scmtype);
  537. $CS->add("repo:" . $this->repoid . ":repopath", $old['repopath'], $this->repopath);
  538. $CS->add("repo:" . $this->repoid . ":browserurl", $old['browserurl'], $this->browserurl);
  539. $CS->add("repo:" . $this->repoid . ":browsertype", $old['browsertype'], $this->browsertype);
  540. $CS->add("repo:" . $this->repoid . ":description", $old['description'], $this->description);
  541. $CS->add("repo:" . $this->repoid . ":parent", $old['parent'], $this->parent);
  542. $CS->add("repo:" . $this->repoid . ":clonedfrom", $old['clonedfrom'], $this->clonedfrom);
  543. $CS->add("repo:" . $this->repoid . ":serverurl", $old['serverurl'], $this->serverurl);
  544. foreach ($this->links_to_add as $link) {
  545. MTrackDB::q('insert into project_repo_link (projid, repoid, repopathregex) values (?, ?, ?)', $link[0], $this->repoid, $link[1]);
  546. }
  547. foreach ($this->links_to_remove as $linkid) {
  548. MTrackDB::q('delete from project_repo_link where repoid = ? and linkid = ?', $this->repoid, $linkid);
  549. }
  550. $this->links = null;
  551. }
  552. function getLinks()
  553. {
  554. if ($this->links === null) {
  555. $this->links = array();
  556. foreach (MTrackDB::q('select linkid, projid, repopathregex
  557. from project_repo_link where repoid = ? order by repopathregex',
  558. $this->repoid)->fetchAll() as $row) {
  559. $this->links[$row[0]] = array($row[1], $row[2]);
  560. }
  561. }
  562. return $this->links;
  563. }
  564. function addLink($proj, $regex)
  565. {
  566. if ($proj instanceof MTrackProject) {
  567. $this->links_to_add[] = array($proj->projid, $regex);
  568. } else {
  569. $this->links_to_add[] = array($proj, $regex);
  570. }
  571. }
  572. function removeLink($linkid)
  573. {
  574. $this->links_to_remove[$linkid] = $linkid;
  575. }
  576. public function getBranches() {}
  577. public function getTags() {}
  578. public function readdir($path, $object = null, $ident = null) {}
  579. public function file($path, $object = null, $ident = null) {}
  580. public function history($path, $limit = null, $object = null, $ident = null){}
  581. public function diff($path, $from = null, $to = null) {}
  582. public function getRelatedChanges($revision) {}
  583. function projectFromPath($filename) {
  584. static $links = array();
  585. static $projects_by_name = array();
  586. if (!isset($links[$this->repoid]) || $links[$this->repoid] === null) {
  587. $links[$this->repoid] = array();
  588. foreach (MTrackDB::q(
  589. 'select projid, repopathregex from project_repo_link where repoid = ?',
  590. $this->repoid) as $row) {
  591. $re = str_replace('/', '\\/', $row[1]);
  592. $links[$this->repoid][] = array($row[0], "/$re/");
  593. }
  594. }
  595. if (is_array($filename)) {
  596. $proj_incidence = array();
  597. foreach ($filename as $file) {
  598. $proj = $this->projectFromPath($file);
  599. if ($proj === null) continue;
  600. if (isset($proj_incidence[$proj])) {
  601. $proj_incidence[$proj]++;
  602. } else {
  603. $proj_incidence[$proj] = 1;
  604. }
  605. }
  606. $the_proj = null;
  607. $the_proj_count = 0;
  608. foreach ($proj_incidence as $proj => $count) {
  609. if ($count > $the_proj_count) {
  610. $the_proj_count = $count;
  611. $the_proj = $proj;
  612. }
  613. }
  614. return $the_proj;
  615. }
  616. if ($filename instanceof MTrackSCMFileEvent) {
  617. $filename = $filename->name;
  618. }
  619. // walk through the regexes; take the longest match as definitive
  620. $longest = null;
  621. $longest_id = null;
  622. if ($filename[0] != '/') {
  623. $filename = '/' . $filename;
  624. }
  625. foreach ($links[$this->repoid] as $link) {
  626. if (preg_match($link[1], $filename, $M)) {
  627. if (strlen($M[0]) > strlen($longest)) {
  628. $longest = $M[0];
  629. $longest_id = $link[0];
  630. }
  631. }
  632. }
  633. if ($longest_id === null) {
  634. /* no match found; if this repo is project-owned, then we assume
  635. * that that project is the match */
  636. if (preg_match("/^project:(.*)$/", $this->parent, $M)) {
  637. $pname = $M[1];
  638. if (!isset($projects_by_name[$pname])) {
  639. $P = MTrackProject::loadByName($pname);
  640. $projects_by_name[$pname] = $P;
  641. } else {
  642. $P = $projects_by_name[$pname];
  643. }
  644. return $P->projid;
  645. }
  646. }
  647. return $longest_id;
  648. }
  649. static function rest_return(MTrackRepo $r) {
  650. $o = MTrackAPI::makeObj($r, 'repoid');
  651. $o->checkout_command = $r->getCheckoutCommand();
  652. $o->description_html = MTrackWiki::format_to_html($o->description);
  653. $o->browsepath = $r->getBrowseRootName();
  654. $o->links = array();
  655. foreach ($r->getLinks() as $lid => $data) {
  656. list($pid, $regex) = $data;
  657. $l = new stdclass;
  658. $l->id = $lid;
  659. $l->project = $pid;
  660. $l->regex = $regex;
  661. $o->links[] = $l;
  662. }
  663. if (MTrackACL::hasAllRights("repo:$r->repoid", "modify")) {
  664. $o->perms = MTrackACL::computeACLObject("repo:$r->repoid");
  665. }
  666. if ($r->canFork() && MTrackACL::hasAllRights('Browser', 'fork')
  667. && MTrackConfig::get('repos', 'allow_user_repo_creation')) {
  668. $o->canFork = true;
  669. } else {
  670. $o->canFork = false;
  671. }
  672. if ($r->parent &&
  673. MTrackACL::hasAllRights("repo:$r->repoid", "delete")) {
  674. $o->canDelete = true;
  675. } else {
  676. $o->canDelete = false;
  677. }
  678. return $o;
  679. }
  680. static function rest_apply(MTrackChangeset $CS, MTrackRepo $r, $in) {
  681. if (isset($in->description)) {
  682. $r->description = $in->description;
  683. }
  684. /* parse and apply links.
  685. * [{id: 1, regex: "path", project: 123}]
  686. */
  687. if (isset($in->links)) {
  688. $current = $r->getLinks();
  689. $seen = array();
  690. foreach ($in->links as $link) {
  691. /* updating an existing link? */
  692. if (isset($link->id)) {
  693. $seen[$link->id] = $link->id;
  694. if (isset($current[$link->id])) {
  695. /* already exists; are we changing it? */
  696. list($pid, $regex) = $current[$link->id];
  697. if ($pid != $link->project || $regex != $link->regex) {
  698. $r->removeLink($link->id);
  699. if (strlen($link->regex)) {
  700. $r->addLink($link->project, $link->regex);
  701. }
  702. }
  703. continue;
  704. }
  705. }
  706. /* adding */
  707. if (strlen($link->regex)) {
  708. $r->addLink($link->project, $link->regex);
  709. }
  710. }
  711. /* anything in current that is not in seen is removed */
  712. foreach ($current as $lid => $data) {
  713. if (isset($seen[$lid])) continue;
  714. $r->removeLink($lid);
  715. }
  716. }
  717. $r->save($CS);
  718. $CS->setObject("repo:$r->repoid");
  719. if (isset($in->perms) && isset($in->perms->acl)) {
  720. MTrackACL::setACL("repo:$r->repoid", 0, $in->perms->acl);
  721. }
  722. }
  723. /* /repo/properties -> lists or creates repos
  724. */
  725. static function rest_repo_list($method, $uri, $captures) {
  726. MTrackAPI::checkAllowed($method, 'GET', 'POST');
  727. if ($method == 'GET') {
  728. MTrackACL::requireAllRights('Browser', 'read');
  729. $res = array();
  730. foreach (self::getReposList() as $r) {
  731. if (!MTrackACL::hasAnyRights("repo:$repo->repoid", 'read')) {
  732. continue;
  733. }
  734. $res[] = self::rest_return(self::loadById($r->repoid));
  735. }
  736. return $res;
  737. }
  738. MTrackACL::requireAnyRights('Browser', array('create', 'fork'));
  739. $repo = new MTrackRepo;
  740. /* we're creating a new guy here.
  741. * We should validate that the current user has rights to
  742. * use the specified parent ($owner) or access to the source
  743. * repo (clonedfrom) */
  744. /* FIXME: also respect allow_user_repo_creation */
  745. /* If $owner != $me, then $owner can be any project that
  746. * I have 'modify' rights for */
  747. $in = MTrackAPI::getPayload();
  748. if (!is_object($in)) {
  749. MTrackAPI::error(400, "expected json payload");
  750. }
  751. if (!isset($in->shortname) || strlen(trim($in->shortname)) == 0) {
  752. MTrackAPI::error(400, "invalid name", $in->shortname);
  753. }
  754. $in->shortname = trim($in->shortname);
  755. if (preg_match("/[^a-zA-Z0-9_.-]/", $in->shortname)) {
  756. MTrackAPI::error(400, "name contains illegal characters", $in->shortname);
  757. }
  758. if (MTrackACL::hasAnyRights('Browser', 'create')) {
  759. /* I can create anything I damned well please */
  760. } else {
  761. /* I can only put things in my own namespace */
  762. $me = mtrack_canon_username(MTrackAuth::whoami());
  763. if ($in->parent != "user:$me") {
  764. MTrackAPI::error(400, "owner must match my user",
  765. $in->parent, "user:$me");
  766. }
  767. }
  768. if (isset($in->clonedfrom)) {
  769. MTrackACL::requireAllRights('Browser', 'fork');
  770. MTrackACL::requireAllRights("repo:$in->clonedfrom", 'read');
  771. $S = MTrackRepo::loadById($in->clonedfrom);
  772. if (!$S->canFork()) {
  773. MTrackAPI::error(400, "cannot fork repo", $S->shortname, $S->scmtype);
  774. }
  775. if (!isset($in->description)) {
  776. $in->description = $S->description;
  777. }
  778. $repo->scmtype = $S->scmtype;
  779. $repo->clonedfrom = $S->repoid;
  780. } else {
  781. if (!isset($in->scmtype)) {
  782. MTrackAPI::error(400, "missing scmtype");
  783. }
  784. $repo->scmtype = $in->scmtype;
  785. }
  786. if (isset($in->parent)) {
  787. $repo->parent = $in->parent;
  788. }
  789. $repo->shortname = $in->shortname;
  790. $CS = MTrackChangeset::begin("repo:X", "Create repo $in->shortname");
  791. self::rest_apply($CS, $repo, $in);
  792. $CS->commit();
  793. MTrackACL::requireAllRights("repo:$repo->repoid", 'read');
  794. return self::rest_return($repo);
  795. }
  796. /* /repo/properties/123 -> details of repo
  797. */
  798. static function rest_props($method, $uri, $captures) {
  799. MTrackAPI::checkAllowed($method, 'GET', 'PUT', 'DELETE');
  800. MTrackACL::requireAllRights('Browser', 'read');
  801. $rid = $captures['rid'];
  802. $repo = self::loadById($rid);
  803. if (!$repo) {
  804. MTrackAPI::error(404, "invalid repo", $rid);
  805. }
  806. if ($method == 'DELETE') {
  807. MTrackACL::requireAllRights("repo:$rid", 'delete');
  808. $CS = MTrackChangeset::begin("repo:$rid", "Delete repo $repo->shortname");
  809. $repo->deleteRepo($CS);
  810. $CS->commit();
  811. return;
  812. }
  813. if ($method != 'GET') {
  814. $in = MTrackAPI::getPayload();
  815. if (!is_object($in)) {
  816. MTrackAPI::error(400, "expected json payload");
  817. }
  818. MTrackACL::requireAllRights("repo:$repo->repoid", 'modify');
  819. $CS = MTrackChangeset::begin("repo:$repo->repoid",
  820. "Edit repo $repo->shortname");
  821. self::rest_apply($CS, $repo, $in);
  822. $CS->commit();
  823. }
  824. MTrackACL::requireAllRights("repo:$repo->repoid", 'read');
  825. return self::rest_return($repo);
  826. }
  827. /** returns a list of allowed owners for new repos for the
  828. * authenticated user */
  829. static function rest_allowed_targets($method, $uri, $captures) {
  830. $res = array();
  831. if (MTrackACL::hasAllRights('Browser', 'create')) {
  832. $me = mtrack_canon_username(MTrackAuth::whoami());
  833. $res = array("user:$me" => $me);
  834. foreach (MTrackDB::q(
  835. 'select projid, shortname, name from projects order by ordinal')
  836. as $row)
  837. {
  838. if (MTrackACL::hasAllRights("project:$row[0]", 'modify')) {
  839. $res['project:' . $row[1]] = $row[1];
  840. }
  841. }
  842. } else if (MTrackConfig::get('repos', 'allow_user_repo_creation')) {
  843. $me = mtrack_canon_username(MTrackAuth::whoami());
  844. $res = array("user:$me" => $me);
  845. }
  846. return $res;
  847. }
  848. /* /repo/history/default/wiki
  849. * GET params:
  850. * rev: revision
  851. * tag: tag
  852. * branch: branch
  853. * limit: #items
  854. */
  855. static function rest_history($method, $uri, $captures) {
  856. MTrackAPI::checkAllowed($method, 'GET');
  857. MTrackACL::requireAllRights('Browser', 'read');
  858. $path = '/' . $captures['path'];
  859. $repo = MTrackSCM::factory($path);
  860. MTrackACL::requireAllRights("repo:$repo->repoid", 'read');
  861. $limit = MTrackAPI::getParam('limit');
  862. if (!$limit) {
  863. $limit = 100;
  864. }
  865. $object = null;
  866. $ident = null;
  867. if (MTrackAPI::getParam('rev')) {
  868. $object = 'rev';
  869. $ident = MTrackAPI::getParam('rev');
  870. } else if (MTrackAPI::getParam('branch')) {
  871. $object = 'branch';
  872. $ident = MTrackAPI::getParam('branch');
  873. } else if (MTrackAPI::getParam('tag')) {
  874. $object = 'tag';
  875. $ident = MTrackAPI::getParam('tag');
  876. }
  877. $hist = $repo->history($path, $limit, $object, $ident);
  878. $res = array();
  879. foreach ($hist as $ent) {
  880. $h = new stdclass;
  881. $h->when = MTrackAPI::date8601($ent->ctime);
  882. $h->changelog = $ent->changelog;
  883. $h->changelog_html = MTrackWiki::format_to_html($ent->changelog);
  884. $h->who = mtrack_canon_username($ent->changeby);
  885. $h->rev = $ent->rev;
  886. if ($ent->branches && count($ent->branches)) {
  887. list($h->branch) = $ent->branches;
  888. } else {
  889. $h->branch = null;
  890. }
  891. if ($ent->tags) {
  892. $h->tags = $ent->tags;
  893. } else {
  894. $h->tags = array();
  895. }
  896. if ($ent->files) {
  897. $h->files = array();
  898. foreach ($ent->files as $file) {
  899. $f = new stdclass;
  900. $f->name = $file->name;
  901. $f->status = $file->status;
  902. $h->files[$f->name] = $f;
  903. }
  904. }
  905. $res[] = $h;
  906. }
  907. $hist = new stdclass;
  908. $hist->repo = $repo->getBrowseRootName();
  909. $hist->path = $path;
  910. $hist->limit = $limit;
  911. $hist->object = $object;
  912. $hist->ident = $ident;
  913. $hist->entries = $res;
  914. $hist->branches = $repo->getBranches();
  915. $hist->tags = $repo->getTags();
  916. return $hist;
  917. }
  918. static function getReposList() {
  919. $res = array();
  920. return MTrackDB::q("select repoid, parent, shortname, description
  921. from repos order by parent, shortname")->fetchAll(PDO::FETCH_OBJ);
  922. }
  923. static function resolve_changeset_link(MTrackLink $link)
  924. {
  925. $link->class = 'changesetlink';
  926. if (preg_match("/^(.*),(.*)$/", $link->target, $M)) {
  927. $link->url = mtrack_changeset_url($M[2], $M[1]);
  928. if (!$link->label || $link->label == $link->target) {
  929. // prettify the label
  930. $link->label = $M[2];
  931. if (strlen($link->label) > 12) {
  932. $link->label = substr($link->label, 0, 12);
  933. }
  934. }
  935. } else {
  936. $link->url = mtrack_changeset_url($link->target);
  937. }
  938. }
  939. static function resolve_repo_link(MTrackLink $link)
  940. {
  941. $link->url = $GLOBALS['ABSWEB'] . 'browse.php/' . $link->target;
  942. }
  943. static function resolve_log_link(MTrackLink $link)
  944. {
  945. $target = $link->target;
  946. if ($target == '/') {
  947. $target = mtrack_defrepo();
  948. }
  949. $link->url = $GLOBALS['ABSWEB'] . 'log.php/' . $target;
  950. }
  951. static function resolve_source_link(MTrackLink $link)
  952. {
  953. // FIXME: want to be able to anchor to a line number,
  954. // but we use '#' for rev for trac compat; perhaps use colon?
  955. @list($file, $rev) = explode('#', $link->target, 2);
  956. $file = ltrim($file, '/');
  957. /* some legacy handling here; there are three cases:
  958. * owner/repo/path -> repo = owner/repo
  959. * repo/path -> repo = default/repo
  960. * path -> repo = config.ini default repo
  961. */
  962. $bits = explode('/', $file);
  963. $repo = null;
  964. if (count($bits) > 2) {
  965. /* maybe owner/repo */
  966. $repo = MTrackRepo::loadByName($bits[0] . '/' . $bits[1]);
  967. if ($repo) {
  968. $repo = $repo->getBrowseRootName();
  969. }
  970. }
  971. if ($repo === null && count($bits) > 1) {
  972. $repo = MTrackRepo::loadByName('default/' . $bits[0]);
  973. if ($repo) {
  974. $repo = $repo->getBrowseRootName();
  975. array_unshift($bits, 'default');
  976. }
  977. }
  978. if ($repo === null) {
  979. $defrep = mtrack_defrepo();
  980. if ($defrep) {
  981. if (strpos($defrep, '/') === false) {
  982. $defrep = "default/$defrep";
  983. }
  984. $repo = MTrackRepo::loadByName($defrep);
  985. if ($repo) {
  986. $repo = $repo->getBrowseRootName();
  987. array_unshift($bits, $repo);
  988. }
  989. }
  990. }
  991. $file = join($bits, '/');
  992. if ($rev) {
  993. $link->url = $GLOBALS['ABSWEB'] . "file.php/$file@$rev";
  994. } else {
  995. $link->url = $GLOBALS['ABSWEB'] . "file.php/$file";
  996. }
  997. }
  998. }
  999. MTrackAPI::register('/repo/properties', 'MTrackRepo::rest_repo_list');
  1000. MTrackAPI::register('/repo/properties/:rid', 'MTrackRepo::rest_props');
  1001. MTrackAPI::register('/repo/history/*path', 'MTrackRepo::rest_history');
  1002. MTrackAPI::register('/repo/allowed-targets', 'MTrackRepo::rest_allowed_targets');
  1003. MTrackLink::register('changeset', 'MTrackRepo::resolve_changeset_link');
  1004. MTrackLink::register('repo', 'MTrackRepo::resolve_repo_link');
  1005. MTrackLink::register('log', 'MTrackRepo::resolve_log_link');
  1006. MTrackLink::register('source', 'MTrackRepo::resolve_source_link');