PageRenderTime 50ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/extensions/CodeReview/backend/CodeRepository.php

https://github.com/ChuguluGames/mediawiki-svn
PHP | 610 lines | 379 code | 54 blank | 177 comment | 42 complexity | f39eea92b4d1f997663f7e7c99ecd6c3 MD5 | raw file
  1. <?php
  2. /**
  3. * Core class for interacting with a repository of code.
  4. */
  5. class CodeRepository {
  6. const DIFFRESULT_BadRevision = 0;
  7. const DIFFRESULT_NothingToCompare = 1;
  8. const DIFFRESULT_TooManyPaths = 2;
  9. const DIFFRESULT_NoDataReturned = 3;
  10. const DIFFRESULT_NotInCache = 4;
  11. /**
  12. * Local cache of Wiki user -> SVN user mappings
  13. * @var Array
  14. */
  15. private static $userLinks = array();
  16. /**
  17. * Sort of the same, but looking it up for the other direction
  18. * @var Array
  19. */
  20. private static $authorLinks = array();
  21. /**
  22. * Various data about the repo
  23. */
  24. private $id, $name, $path, $viewVc, $bugzilla;
  25. /**
  26. * Constructor, can't use it. Call one of the static newFrom* methods
  27. * @param $id Int Database id for the repo
  28. * @param $name String User-defined name for the repository
  29. * @param $path String Path to SVN
  30. * @param $viewvc String Base path to ViewVC URLs
  31. * @param $bugzilla String Base path to Bugzilla
  32. */
  33. public function __construct( $id, $name, $path, $viewvc, $bugzilla ) {
  34. $this->id = $id;
  35. $this->name = $name;
  36. $this->path = $path;
  37. $this->viewVc = $viewvc;
  38. $this->bugzilla = $bugzilla;
  39. }
  40. /**
  41. * @param $name string
  42. * @return CodeRepository|null
  43. */
  44. public static function newFromName( $name ) {
  45. $dbw = wfGetDB( DB_MASTER );
  46. $row = $dbw->selectRow(
  47. 'code_repo',
  48. array(
  49. 'repo_id',
  50. 'repo_name',
  51. 'repo_path',
  52. 'repo_viewvc',
  53. 'repo_bugzilla' ),
  54. array( 'repo_name' => $name ),
  55. __METHOD__ );
  56. if ( $row ) {
  57. return self::newFromRow( $row );
  58. } else {
  59. return null;
  60. }
  61. }
  62. /**
  63. * @param $id int
  64. * @return CodeRepository|null
  65. */
  66. public static function newFromId( $id ) {
  67. $dbw = wfGetDB( DB_MASTER );
  68. $row = $dbw->selectRow(
  69. 'code_repo',
  70. array(
  71. 'repo_id',
  72. 'repo_name',
  73. 'repo_path',
  74. 'repo_viewvc',
  75. 'repo_bugzilla' ),
  76. array( 'repo_id' => intval( $id ) ),
  77. __METHOD__ );
  78. if ( $row ) {
  79. return self::newFromRow( $row );
  80. } else {
  81. return null;
  82. }
  83. }
  84. /**
  85. * @param $row
  86. * @return CodeRepository
  87. */
  88. static function newFromRow( $row ) {
  89. return new CodeRepository(
  90. intval( $row->repo_id ),
  91. $row->repo_name,
  92. $row->repo_path,
  93. $row->repo_viewvc,
  94. $row->repo_bugzilla
  95. );
  96. }
  97. /**
  98. * @return array
  99. */
  100. static function getRepoList() {
  101. $dbr = wfGetDB( DB_SLAVE );
  102. $options = array( 'ORDER BY' => 'repo_name' );
  103. $res = $dbr->select( 'code_repo', '*', array(), __METHOD__, $options );
  104. $repos = array();
  105. foreach ( $res as $row ) {
  106. $repos[] = self::newFromRow( $row );
  107. }
  108. return $repos;
  109. }
  110. /**
  111. * @return int
  112. */
  113. public function getId() {
  114. return intval( $this->id );
  115. }
  116. /**
  117. * @return String
  118. */
  119. public function getName() {
  120. return $this->name;
  121. }
  122. /**
  123. * @return String
  124. */
  125. public function getPath() {
  126. return $this->path;
  127. }
  128. /**
  129. * @return String
  130. */
  131. public function getViewVcBase() {
  132. return $this->viewVc;
  133. }
  134. /**
  135. * @return String
  136. */
  137. public function getBugzillaBase() {
  138. return $this->bugzilla;
  139. }
  140. /**
  141. * Return a bug URL or false
  142. *
  143. * @param $bugId int|string
  144. * @return string|false.
  145. */
  146. public function getBugPath( $bugId ) {
  147. if ( $this->bugzilla ) {
  148. return str_replace( '$1',
  149. urlencode( $bugId ), $this->bugzilla );
  150. }
  151. return false;
  152. }
  153. /**
  154. * @return int
  155. */
  156. public function getLastStoredRev() {
  157. $dbr = wfGetDB( DB_SLAVE );
  158. $row = $dbr->selectField(
  159. 'code_rev',
  160. 'MAX(cr_id)',
  161. array( 'cr_repo_id' => $this->getId() ),
  162. __METHOD__
  163. );
  164. return intval( $row );
  165. }
  166. /**
  167. * @return array
  168. */
  169. public function getAuthorList() {
  170. global $wgMemc;
  171. $key = wfMemcKey( 'codereview', 'authors', $this->getId() );
  172. $authors = $wgMemc->get( $key );
  173. if ( is_array( $authors ) ) {
  174. return $authors;
  175. }
  176. $dbr = wfGetDB( DB_SLAVE );
  177. $res = $dbr->select(
  178. 'code_rev',
  179. array( 'cr_author', 'MAX(cr_timestamp) AS time' ),
  180. array( 'cr_repo_id' => $this->getId() ),
  181. __METHOD__,
  182. array( 'GROUP BY' => 'cr_author',
  183. 'ORDER BY' => 'cr_author', 'LIMIT' => 500 )
  184. );
  185. $authors = array();
  186. foreach( $res as $row ) {
  187. if ( $row->cr_author !== null ) {
  188. $authors[] = array( 'author' => $row->cr_author, 'lastcommit' => $row->time );
  189. }
  190. }
  191. $wgMemc->set( $key, $authors, 3600 * 24 );
  192. return $authors;
  193. }
  194. /**
  195. * @return int
  196. */
  197. public function getAuthorCount() {
  198. return count( $this->getAuthorList() );
  199. }
  200. /**
  201. * Get a list of all tags in use in the repository
  202. * @param $recache Bool whether to get clean data
  203. * @return array
  204. */
  205. public function getTagList( $recache = false ) {
  206. global $wgMemc;
  207. $key = wfMemcKey( 'codereview', 'tags', $this->getId() );
  208. $tags = $wgMemc->get( $key );
  209. if ( is_array( $tags ) && !$recache ) {
  210. return $tags;
  211. }
  212. $dbr = wfGetDB( DB_SLAVE );
  213. $res = $dbr->select(
  214. 'code_tags',
  215. array( 'ct_tag', 'COUNT(*) AS revs' ),
  216. array( 'ct_repo_id' => $this->getId() ),
  217. __METHOD__,
  218. array( 'GROUP BY' => 'ct_tag',
  219. 'ORDER BY' => 'revs DESC', 'LIMIT' => 500 )
  220. );
  221. $tags = array();
  222. foreach( $res as $row ) {
  223. $tags[$row->ct_tag] = $row->revs;
  224. }
  225. $wgMemc->set( $key, $tags, 3600 * 3 );
  226. return $tags;
  227. }
  228. /**
  229. * Load a particular revision out of the DB
  230. * @param $id int|string
  231. * @return CodeRevision
  232. */
  233. public function getRevision( $id ) {
  234. if ( !$this->isValidRev( $id ) ) {
  235. return null;
  236. }
  237. $dbr = wfGetDB( DB_SLAVE );
  238. $row = $dbr->selectRow( 'code_rev',
  239. '*',
  240. array(
  241. 'cr_id' => $id,
  242. 'cr_repo_id' => $this->getId(),
  243. ),
  244. __METHOD__
  245. );
  246. if ( !$row ) {
  247. throw new MWException( 'Failed to load expected revision data' );
  248. }
  249. return CodeRevision::newFromRow( $this, $row );
  250. }
  251. /**
  252. * Returns the supplied revision ID as a string ready for output, including the
  253. * appropriate (localisable) prefix (e.g. "r123" instead of 123).
  254. *
  255. * @param $id string
  256. * @return string
  257. */
  258. public function getRevIdString( $id ) {
  259. return wfMsg( 'code-rev-id', $id );
  260. }
  261. /**
  262. * Like getRevIdString(), but if more than one repository is defined
  263. * on the wiki then it includes the repo name as a prefix to the revision ID
  264. * (separated with a period).
  265. * This ensures you get a unique reference, as the revision ID alone can be
  266. * confusing (e.g. in e-mails, page titles etc.). If only one repository is
  267. * defined then this returns the same as getRevIdString() as there
  268. * is no ambiguity.
  269. *
  270. * @param $id string
  271. * @return string
  272. */
  273. public function getRevIdStringUnique( $id ) {
  274. $id = wfMsg( 'code-rev-id', $id );
  275. // If there is more than one repo, use the repo name as well.
  276. $repos = CodeRepository::getRepoList();
  277. if ( count( $repos ) > 1 ) {
  278. $id = $this->getName() . "." . $id;
  279. }
  280. return $id;
  281. }
  282. /**
  283. * @param $rev int Revision ID
  284. * @param $useCache string 'skipcache' to avoid caching
  285. * 'cached' to *only* fetch if cached
  286. * @return string|int The diff text on success, a DIFFRESULT_* constant on failure.
  287. */
  288. public function getDiff( $rev, $useCache = '' ) {
  289. global $wgMemc, $wgCodeReviewMaxDiffPaths;
  290. wfProfileIn( __METHOD__ );
  291. $data = null;
  292. $rev1 = $rev - 1;
  293. $rev2 = $rev;
  294. // Check that a valid revision was specified.
  295. $revision = $this->getRevision( $rev );
  296. if ( $revision == null ) {
  297. $data = self::DIFFRESULT_BadRevision;
  298. } else {
  299. // Check that there is at least one, and at most $wgCodeReviewMaxDiffPaths
  300. // paths changed in this revision.
  301. $paths = $revision->getModifiedPaths();
  302. if ( !$paths->numRows() ) {
  303. $data = self::DIFFRESULT_NothingToCompare;
  304. } elseif ( $wgCodeReviewMaxDiffPaths > 0 && $paths->numRows() > $wgCodeReviewMaxDiffPaths ) {
  305. $data = self::DIFFRESULT_TooManyPaths;
  306. }
  307. }
  308. // If an error has occurred, return it.
  309. if ( $data !== null ) {
  310. wfProfileOut( __METHOD__ );
  311. return $data;
  312. }
  313. // Set up the cache key, which will be used both to check if already in the
  314. // cache, and to write the final result to the cache.
  315. $key = wfMemcKey( 'svn', md5( $this->path ), 'diff', $rev1, $rev2 );
  316. // If not set to explicitly skip the cache, get the current diff from memcached
  317. // directly.
  318. if ( $useCache === 'skipcache' ) {
  319. $data = null;
  320. } else {
  321. $data = $wgMemc->get( $key );
  322. }
  323. // If the diff hasn't already been retrieved from the cache, see if we can get
  324. // it from the DB.
  325. if ( !$data && $useCache != 'skipcache' ) {
  326. $dbr = wfGetDB( DB_SLAVE );
  327. $row = $dbr->selectRow( 'code_rev',
  328. array( 'cr_diff', 'cr_flags' ),
  329. array( 'cr_repo_id' => $this->id, 'cr_id' => $rev, 'cr_diff IS NOT NULL' ),
  330. __METHOD__
  331. );
  332. if ( $row ) {
  333. $flags = explode( ',', $row->cr_flags );
  334. $data = $row->cr_diff;
  335. // If the text was fetched without an error, convert it
  336. if ( $data !== false && in_array( 'gzip', $flags ) ) {
  337. # Deal with optional compression of archived pages.
  338. # This can be done periodically via maintenance/compressOld.php, and
  339. # as pages are saved if $wgCompressRevisions is set.
  340. $data = gzinflate( $data );
  341. }
  342. }
  343. }
  344. // If the data was not already in the cache or in the DB, we need to retrieve
  345. // it from SVN.
  346. if ( !$data ) {
  347. // If the calling code is forcing a cache check, report that it wasn't
  348. // in the cache.
  349. if ( $useCache === 'cached' ) {
  350. $data = self::DIFFRESULT_NotInCache;
  351. // Otherwise, retrieve the diff using SubversionAdaptor.
  352. } else {
  353. $svn = SubversionAdaptor::newFromRepo( $this->path );
  354. $data = $svn->getDiff( '', $rev1, $rev2 );
  355. // If $data is blank, report the error that no data was returned.
  356. // TODO: Currently we can't tell the difference between an SVN/connection
  357. // failure and an empty diff. See if we can remedy this!
  358. if ($data == "") {
  359. $data = self::DIFFRESULT_NoDataReturned;
  360. } else {
  361. // Otherwise, store the resulting diff to both the temporary cache and
  362. // permanent DB storage.
  363. // Store to cache
  364. $wgMemc->set( $key, $data, 3600 * 24 * 3 );
  365. // Permanent DB storage
  366. $storedData = $data;
  367. $flags = Revision::compressRevisionText( $storedData );
  368. $dbw = wfGetDB( DB_MASTER );
  369. $dbw->update( 'code_rev',
  370. array( 'cr_diff' => $storedData, 'cr_flags' => $flags ),
  371. array( 'cr_repo_id' => $this->id, 'cr_id' => $rev ),
  372. __METHOD__
  373. );
  374. }
  375. }
  376. }
  377. wfProfileOut( __METHOD__ );
  378. return $data;
  379. }
  380. /**
  381. * Set diff cache (for import operations)
  382. * @param $codeRev CodeRevision
  383. */
  384. public function setDiffCache( CodeRevision $codeRev ) {
  385. global $wgMemc;
  386. wfProfileIn( __METHOD__ );
  387. $rev1 = $codeRev->getId() - 1;
  388. $rev2 = $codeRev->getId();
  389. $svn = SubversionAdaptor::newFromRepo( $this->path );
  390. $data = $svn->getDiff( '', $rev1, $rev2 );
  391. // Store to cache
  392. $key = wfMemcKey( 'svn', md5( $this->path ), 'diff', $rev1, $rev2 );
  393. $wgMemc->set( $key, $data, 3600 * 24 * 3 );
  394. // Permanent DB storage
  395. $storedData = $data;
  396. $flags = Revision::compressRevisionText( $storedData );
  397. $dbw = wfGetDB( DB_MASTER );
  398. $dbw->update( 'code_rev',
  399. array( 'cr_diff' => $storedData, 'cr_flags' => $flags ),
  400. array( 'cr_repo_id' => $this->id, 'cr_id' => $codeRev->getId() ),
  401. __METHOD__
  402. );
  403. wfProfileOut( __METHOD__ );
  404. }
  405. /**
  406. * Is the requested revid a valid revision to show?
  407. * @return bool
  408. * @param $rev int Rev id to check
  409. */
  410. public function isValidRev( $rev ) {
  411. $rev = intval( $rev );
  412. if ( $rev > 0 && $rev <= $this->getLastStoredRev() ) {
  413. return true;
  414. }
  415. return false;
  416. }
  417. /**
  418. * Link the $author to the wikiuser $user
  419. * @param $author String
  420. * @param $user User
  421. * @return bool Success
  422. */
  423. public function linkUser( $author, User $user ) {
  424. // We must link to an existing user
  425. if ( !$user->getId() ) {
  426. return false;
  427. }
  428. $dbw = wfGetDB( DB_MASTER );
  429. // Insert in the auther -> user link row.
  430. // Skip existing rows.
  431. $dbw->insert( 'code_authors',
  432. array(
  433. 'ca_repo_id' => $this->getId(),
  434. 'ca_author' => $author,
  435. 'ca_user_text' => $user->getName()
  436. ),
  437. __METHOD__,
  438. array( 'IGNORE' )
  439. );
  440. // If the last query already found a row, then update it.
  441. if ( !$dbw->affectedRows() ) {
  442. $dbw->update(
  443. 'code_authors',
  444. array( 'ca_user_text' => $user->getName() ),
  445. array(
  446. 'ca_repo_id' => $this->getId(),
  447. 'ca_author' => $author,
  448. ),
  449. __METHOD__
  450. );
  451. }
  452. self::$userLinks[$author] = $user;
  453. return ( $dbw->affectedRows() > 0 );
  454. }
  455. /**
  456. * Remove local user links for $author
  457. * @param string $author
  458. * @return bool success
  459. */
  460. public function unlinkUser( $author ) {
  461. $dbw = wfGetDB( DB_MASTER );
  462. $dbw->delete(
  463. 'code_authors',
  464. array(
  465. 'ca_repo_id' => $this->getId(),
  466. 'ca_author' => $author,
  467. ),
  468. __METHOD__
  469. );
  470. self::$userLinks[$author] = false;
  471. return ( $dbw->affectedRows() > 0 );
  472. }
  473. /**
  474. * returns a User object if $author has a wikiuser associated,
  475. * or false
  476. *
  477. * @param $author string
  478. *
  479. * @return User|bool
  480. */
  481. public function authorWikiUser( $author ) {
  482. if ( isset( self::$userLinks[$author] ) ) {
  483. return self::$userLinks[$author];
  484. }
  485. $dbr = wfGetDB( DB_SLAVE );
  486. $wikiUser = $dbr->selectField(
  487. 'code_authors',
  488. 'ca_user_text',
  489. array(
  490. 'ca_repo_id' => $this->getId(),
  491. 'ca_author' => $author,
  492. ),
  493. __METHOD__
  494. );
  495. $user = null;
  496. if ( $wikiUser !== false ) {
  497. $user = User::newFromName( $wikiUser );
  498. }
  499. if ( $user instanceof User ){
  500. $res = $user;
  501. } else {
  502. $res = false;
  503. }
  504. return self::$userLinks[$author] = $res;
  505. }
  506. /**
  507. * returns an author name if $name wikiuser has an author associated,
  508. * or false
  509. *
  510. * @param $name string
  511. *
  512. * @return string|false
  513. */
  514. public function wikiUserAuthor( $name ) {
  515. if ( isset( self::$authorLinks[$name] ) )
  516. return self::$authorLinks[$name];
  517. $dbr = wfGetDB( DB_SLAVE );
  518. $res = $dbr->selectField(
  519. 'code_authors',
  520. 'ca_author',
  521. array(
  522. 'ca_repo_id' => $this->getId(),
  523. 'ca_user_text' => $name,
  524. ),
  525. __METHOD__
  526. );
  527. return self::$authorLinks[$name] = $res;
  528. }
  529. /**
  530. * @static
  531. * @param $diff int (error code) or string (diff text), as returned from getDiff()
  532. * @return string (error message, or empty string if valid diff)
  533. */
  534. public static function getDiffErrorMessage( $diff ) {
  535. global $wgCodeReviewMaxDiffPaths;
  536. if ( is_integer( $diff ) ) {
  537. switch( $diff ) {
  538. case self::DIFFRESULT_BadRevision:
  539. return 'Bad revision';
  540. case self::DIFFRESULT_NothingToCompare:
  541. return 'Nothing to compare';
  542. case self::DIFFRESULT_TooManyPaths:
  543. return 'Too many paths ($wgCodeReviewMaxDiffPaths = '
  544. . $wgCodeReviewMaxDiffPaths . ')';
  545. case self::DIFFRESULT_NoDataReturned:
  546. return 'No data returned - no diff data, or connection lost';
  547. case self::DIFFRESULT_NotInCache:
  548. return 'Not in cache';
  549. default:
  550. return 'Unknown reason!';
  551. }
  552. }
  553. // TODO: Should this return "", $diff or a message string, e.g. "OK"?
  554. return "";
  555. }
  556. }