You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

701 lines
25 KiB

  1. <?php
  2. namespace dokuwiki\ChangeLog;
  3. use dokuwiki\Logger;
  4. /**
  5. * ChangeLog Prototype; methods for handling changelog
  6. */
  7. abstract class ChangeLog
  8. {
  9. use ChangeLogTrait;
  10. /** @var string */
  11. protected $id;
  12. /** @var false|int */
  13. protected $currentRevision;
  14. /** @var array */
  15. protected $cache = [];
  16. /**
  17. * Constructor
  18. *
  19. * @param string $id page id
  20. * @param int $chunk_size maximum block size read from file
  21. */
  22. public function __construct($id, $chunk_size = 8192)
  23. {
  24. global $cache_revinfo;
  25. $this->cache =& $cache_revinfo;
  26. if (!isset($this->cache[$id])) {
  27. $this->cache[$id] = [];
  28. }
  29. $this->id = $id;
  30. $this->setChunkSize($chunk_size);
  31. }
  32. /**
  33. * Returns path to current page/media
  34. *
  35. * @param string|int $rev empty string or revision timestamp
  36. * @return string path to file
  37. */
  38. abstract protected function getFilename($rev = '');
  39. /**
  40. * Returns mode
  41. *
  42. * @return string RevisionInfo::MODE_MEDIA or RevisionInfo::MODE_PAGE
  43. */
  44. abstract protected function getMode();
  45. /**
  46. * Check whether given revision is the current page
  47. *
  48. * @param int $rev timestamp of current page
  49. * @return bool true if $rev is current revision, otherwise false
  50. */
  51. public function isCurrentRevision($rev)
  52. {
  53. return $rev == $this->currentRevision();
  54. }
  55. /**
  56. * Checks if the revision is last revision
  57. *
  58. * @param int $rev revision timestamp
  59. * @return bool true if $rev is last revision, otherwise false
  60. */
  61. public function isLastRevision($rev = null)
  62. {
  63. return $rev === $this->lastRevision();
  64. }
  65. /**
  66. * Return the current revision identifier
  67. *
  68. * The "current" revision means current version of the page or media file. It is either
  69. * identical with or newer than the "last" revision, that depends on whether the file
  70. * has modified, created or deleted outside of DokuWiki.
  71. * The value of identifier can be determined by timestamp as far as the file exists,
  72. * otherwise it must be assigned larger than any other revisions to keep them sortable.
  73. *
  74. * @return int|false revision timestamp
  75. */
  76. public function currentRevision()
  77. {
  78. if (!isset($this->currentRevision)) {
  79. // set ChangeLog::currentRevision property
  80. $this->getCurrentRevisionInfo();
  81. }
  82. return $this->currentRevision;
  83. }
  84. /**
  85. * Return the last revision identifier, date value of the last entry of the changelog
  86. *
  87. * @return int|false revision timestamp
  88. */
  89. public function lastRevision()
  90. {
  91. $revs = $this->getRevisions(-1, 1);
  92. return empty($revs) ? false : $revs[0];
  93. }
  94. /**
  95. * Parses a changelog line into its components and save revision info to the cache pool
  96. *
  97. * @param string $value changelog line
  98. * @return array|bool parsed line or false
  99. */
  100. protected function parseAndCacheLogLine($value)
  101. {
  102. $info = static::parseLogLine($value);
  103. if (is_array($info)) {
  104. $info['mode'] = $this->getMode();
  105. $this->cache[$this->id][$info['date']] ??= $info;
  106. return $info;
  107. }
  108. return false;
  109. }
  110. /**
  111. * Get the changelog information for a specific revision (timestamp)
  112. *
  113. * Adjacent changelog lines are optimistically parsed and cached to speed up
  114. * consecutive calls to getRevisionInfo. For large changelog files, only the chunk
  115. * containing the requested changelog line is read.
  116. *
  117. * @param int $rev revision timestamp
  118. * @param bool $retrieveCurrentRevInfo allows to skip for getting other revision info in the
  119. * getCurrentRevisionInfo() where $currentRevision is not yet determined
  120. * @return bool|array false or array with entries:
  121. * - date: unix timestamp
  122. * - ip: IPv4 address (127.0.0.1)
  123. * - type: log line type
  124. * - id: page id
  125. * - user: user name
  126. * - sum: edit summary (or action reason)
  127. * - extra: extra data (varies by line type)
  128. * - sizechange: change of filesize
  129. * additional:
  130. * - mode: page or media
  131. *
  132. * @author Ben Coburn <btcoburn@silicodon.net>
  133. * @author Kate Arzamastseva <pshns@ukr.net>
  134. */
  135. public function getRevisionInfo($rev, $retrieveCurrentRevInfo = true)
  136. {
  137. $rev = max(0, $rev);
  138. if (!$rev) return false;
  139. //ensure the external edits are cached as well
  140. if (!isset($this->currentRevision) && $retrieveCurrentRevInfo) {
  141. $this->getCurrentRevisionInfo();
  142. }
  143. // check if it's already in the memory cache
  144. if (isset($this->cache[$this->id][$rev])) {
  145. return $this->cache[$this->id][$rev];
  146. }
  147. //read lines from changelog
  148. [$fp, $lines] = $this->readloglines($rev);
  149. if ($fp) {
  150. fclose($fp);
  151. }
  152. if (empty($lines)) return false;
  153. // parse and cache changelog lines
  154. foreach ($lines as $line) {
  155. $this->parseAndCacheLogLine($line);
  156. }
  157. return $this->cache[$this->id][$rev] ?? false;
  158. }
  159. /**
  160. * Return a list of page revisions numbers
  161. *
  162. * Does not guarantee that the revision exists in the attic,
  163. * only that a line with the date exists in the changelog.
  164. * By default the current revision is skipped.
  165. *
  166. * The current revision is automatically skipped when the page exists.
  167. * See $INFO['meta']['last_change'] for the current revision.
  168. * A negative $first let read the current revision too.
  169. *
  170. * For efficiency, the log lines are parsed and cached for later
  171. * calls to getRevisionInfo. Large changelog files are read
  172. * backwards in chunks until the requested number of changelog
  173. * lines are received.
  174. *
  175. * @param int $first skip the first n changelog lines
  176. * @param int $num number of revisions to return
  177. * @return array with the revision timestamps
  178. *
  179. * @author Ben Coburn <btcoburn@silicodon.net>
  180. * @author Kate Arzamastseva <pshns@ukr.net>
  181. */
  182. public function getRevisions($first, $num)
  183. {
  184. $revs = [];
  185. $lines = [];
  186. $count = 0;
  187. $logfile = $this->getChangelogFilename();
  188. if (!file_exists($logfile)) return $revs;
  189. $num = max($num, 0);
  190. if ($num == 0) {
  191. return $revs;
  192. }
  193. if ($first < 0) {
  194. $first = 0;
  195. } else {
  196. $fileLastMod = $this->getFilename();
  197. if (file_exists($fileLastMod) && $this->isLastRevision(filemtime($fileLastMod))) {
  198. // skip last revision if the page exists
  199. $first = max($first + 1, 0);
  200. }
  201. }
  202. if (filesize($logfile) < $this->chunk_size || $this->chunk_size == 0) {
  203. // read whole file
  204. $lines = file($logfile);
  205. if ($lines === false) {
  206. return $revs;
  207. }
  208. } else {
  209. // read chunks backwards
  210. $fp = fopen($logfile, 'rb'); // "file pointer"
  211. if ($fp === false) {
  212. return $revs;
  213. }
  214. fseek($fp, 0, SEEK_END);
  215. $tail = ftell($fp);
  216. // chunk backwards
  217. $finger = max($tail - $this->chunk_size, 0);
  218. while ($count < $num + $first) {
  219. $nl = $this->getNewlinepointer($fp, $finger);
  220. // was the chunk big enough? if not, take another bite
  221. if ($nl > 0 && $tail <= $nl) {
  222. $finger = max($finger - $this->chunk_size, 0);
  223. continue;
  224. } else {
  225. $finger = $nl;
  226. }
  227. // read chunk
  228. $chunk = '';
  229. $read_size = max($tail - $finger, 0); // found chunk size
  230. $got = 0;
  231. while ($got < $read_size && !feof($fp)) {
  232. $tmp = @fread($fp, max(min($this->chunk_size, $read_size - $got), 0));
  233. if ($tmp === false) {
  234. break;
  235. } //error state
  236. $got += strlen($tmp);
  237. $chunk .= $tmp;
  238. }
  239. $tmp = explode("\n", $chunk);
  240. array_pop($tmp); // remove trailing newline
  241. // combine with previous chunk
  242. $count += count($tmp);
  243. $lines = [...$tmp, ...$lines];
  244. // next chunk
  245. if ($finger == 0) {
  246. break;
  247. } else { // already read all the lines
  248. $tail = $finger;
  249. $finger = max($tail - $this->chunk_size, 0);
  250. }
  251. }
  252. fclose($fp);
  253. }
  254. // skip parsing extra lines
  255. $num = max(min(count($lines) - $first, $num), 0);
  256. if ($first > 0 && $num > 0) {
  257. $lines = array_slice($lines, max(count($lines) - $first - $num, 0), $num);
  258. } elseif ($first > 0 && $num == 0) {
  259. $lines = array_slice($lines, 0, max(count($lines) - $first, 0));
  260. } elseif ($first == 0 && $num > 0) {
  261. $lines = array_slice($lines, max(count($lines) - $num, 0));
  262. }
  263. // handle lines in reverse order
  264. for ($i = count($lines) - 1; $i >= 0; $i--) {
  265. $info = $this->parseAndCacheLogLine($lines[$i]);
  266. if (is_array($info)) {
  267. $revs[] = $info['date'];
  268. }
  269. }
  270. return $revs;
  271. }
  272. /**
  273. * Get the nth revision left or right-hand side for a specific page id and revision (timestamp)
  274. *
  275. * For large changelog files, only the chunk containing the
  276. * reference revision $rev is read and sometimes a next chunk.
  277. *
  278. * Adjacent changelog lines are optimistically parsed and cached to speed up
  279. * consecutive calls to getRevisionInfo.
  280. *
  281. * @param int $rev revision timestamp used as start date
  282. * (doesn't need to be exact revision number)
  283. * @param int $direction give position of returned revision with respect to $rev;
  284. positive=next, negative=prev
  285. * @return bool|int
  286. * timestamp of the requested revision
  287. * otherwise false
  288. */
  289. public function getRelativeRevision($rev, $direction)
  290. {
  291. $rev = max($rev, 0);
  292. $direction = (int)$direction;
  293. //no direction given or last rev, so no follow-up
  294. if (!$direction || ($direction > 0 && $this->isCurrentRevision($rev))) {
  295. return false;
  296. }
  297. //get lines from changelog
  298. [$fp, $lines, $head, $tail, $eof] = $this->readloglines($rev);
  299. if (empty($lines)) return false;
  300. // look for revisions later/earlier than $rev, when founded count till the wanted revision is reached
  301. // also parse and cache changelog lines for getRevisionInfo().
  302. $revCounter = 0;
  303. $relativeRev = false;
  304. $checkOtherChunk = true; //always runs once
  305. while (!$relativeRev && $checkOtherChunk) {
  306. $info = [];
  307. //parse in normal or reverse order
  308. $count = count($lines);
  309. if ($direction > 0) {
  310. $start = 0;
  311. $step = 1;
  312. } else {
  313. $start = $count - 1;
  314. $step = -1;
  315. }
  316. for ($i = $start; $i >= 0 && $i < $count; $i += $step) {
  317. $info = $this->parseAndCacheLogLine($lines[$i]);
  318. if (is_array($info)) {
  319. //look for revs older/earlier then reference $rev and select $direction-th one
  320. if (($direction > 0 && $info['date'] > $rev) || ($direction < 0 && $info['date'] < $rev)) {
  321. $revCounter++;
  322. if ($revCounter == abs($direction)) {
  323. $relativeRev = $info['date'];
  324. }
  325. }
  326. }
  327. }
  328. //true when $rev is found, but not the wanted follow-up.
  329. $checkOtherChunk = $fp
  330. && ($info['date'] == $rev || ($revCounter > 0 && !$relativeRev))
  331. && (!($tail == $eof && $direction > 0) && !($head == 0 && $direction < 0));
  332. if ($checkOtherChunk) {
  333. [$lines, $head, $tail] = $this->readAdjacentChunk($fp, $head, $tail, $direction);
  334. if (empty($lines)) break;
  335. }
  336. }
  337. if ($fp) {
  338. fclose($fp);
  339. }
  340. return $relativeRev;
  341. }
  342. /**
  343. * Returns revisions around rev1 and rev2
  344. * When available it returns $max entries for each revision
  345. *
  346. * @param int $rev1 oldest revision timestamp
  347. * @param int $rev2 newest revision timestamp (0 looks up last revision)
  348. * @param int $max maximum number of revisions returned
  349. * @return array with two arrays with revisions surrounding rev1 respectively rev2
  350. */
  351. public function getRevisionsAround($rev1, $rev2, $max = 50)
  352. {
  353. $max = (int) (abs($max) / 2) * 2 + 1;
  354. $rev1 = max($rev1, 0);
  355. $rev2 = max($rev2, 0);
  356. if ($rev2) {
  357. if ($rev2 < $rev1) {
  358. $rev = $rev2;
  359. $rev2 = $rev1;
  360. $rev1 = $rev;
  361. }
  362. } else {
  363. //empty right side means a removed page. Look up last revision.
  364. $rev2 = $this->currentRevision();
  365. }
  366. //collect revisions around rev2
  367. [$revs2, $allRevs, $fp, $lines, $head, $tail] = $this->retrieveRevisionsAround($rev2, $max);
  368. if (empty($revs2)) return [[], []];
  369. //collect revisions around rev1
  370. $index = array_search($rev1, $allRevs);
  371. if ($index === false) {
  372. //no overlapping revisions
  373. [$revs1, , , , , ] = $this->retrieveRevisionsAround($rev1, $max);
  374. if (empty($revs1)) $revs1 = [];
  375. } else {
  376. //revisions overlaps, reuse revisions around rev2
  377. $lastRev = array_pop($allRevs); //keep last entry that could be external edit
  378. $revs1 = $allRevs;
  379. while ($head > 0) {
  380. for ($i = count($lines) - 1; $i >= 0; $i--) {
  381. $info = $this->parseAndCacheLogLine($lines[$i]);
  382. if (is_array($info)) {
  383. $revs1[] = $info['date'];
  384. $index++;
  385. if ($index > (int) ($max / 2)) {
  386. break 2;
  387. }
  388. }
  389. }
  390. [$lines, $head, $tail] = $this->readAdjacentChunk($fp, $head, $tail, -1);
  391. }
  392. sort($revs1);
  393. $revs1[] = $lastRev; //push back last entry
  394. //return wanted selection
  395. $revs1 = array_slice($revs1, max($index - (int) ($max / 2), 0), $max);
  396. }
  397. return [array_reverse($revs1), array_reverse($revs2)];
  398. }
  399. /**
  400. * Return an existing revision for a specific date which is
  401. * the current one or younger or equal then the date
  402. *
  403. * @param number $date_at timestamp
  404. * @return string revision ('' for current)
  405. */
  406. public function getLastRevisionAt($date_at)
  407. {
  408. $fileLastMod = $this->getFilename();
  409. //requested date_at(timestamp) younger or equal then modified_time($this->id) => load current
  410. if (file_exists($fileLastMod) && $date_at >= @filemtime($fileLastMod)) {
  411. return '';
  412. } elseif ($rev = $this->getRelativeRevision($date_at + 1, -1)) {
  413. //+1 to get also the requested date revision
  414. return $rev;
  415. } else {
  416. return false;
  417. }
  418. }
  419. /**
  420. * Collect the $max revisions near to the timestamp $rev
  421. *
  422. * Ideally, half of retrieved timestamps are older than $rev, another half are newer.
  423. * The returned array $requestedRevs may not contain the reference timestamp $rev
  424. * when it does not match any revision value recorded in changelog.
  425. *
  426. * @param int $rev revision timestamp
  427. * @param int $max maximum number of revisions to be returned
  428. * @return bool|array
  429. * return array with entries:
  430. * - $requestedRevs: array of with $max revision timestamps
  431. * - $revs: all parsed revision timestamps
  432. * - $fp: file pointer only defined for chuck reading, needs closing.
  433. * - $lines: non-parsed changelog lines before the parsed revisions
  434. * - $head: position of first read changelog line
  435. * - $lastTail: position of end of last read changelog line
  436. * otherwise false
  437. */
  438. protected function retrieveRevisionsAround($rev, $max)
  439. {
  440. $revs = [];
  441. $afterCount = 0;
  442. $beforeCount = 0;
  443. //get lines from changelog
  444. [$fp, $lines, $startHead, $startTail, $eof] = $this->readloglines($rev);
  445. if (empty($lines)) return false;
  446. //parse changelog lines in chunk, and read forward more chunks until $max/2 is reached
  447. $head = $startHead;
  448. $tail = $startTail;
  449. while (count($lines) > 0) {
  450. foreach ($lines as $line) {
  451. $info = $this->parseAndCacheLogLine($line);
  452. if (is_array($info)) {
  453. $revs[] = $info['date'];
  454. if ($info['date'] >= $rev) {
  455. //count revs after reference $rev
  456. $afterCount++;
  457. if ($afterCount == 1) {
  458. $beforeCount = count($revs);
  459. }
  460. }
  461. //enough revs after reference $rev?
  462. if ($afterCount > (int) ($max / 2)) {
  463. break 2;
  464. }
  465. }
  466. }
  467. //retrieve next chunk
  468. [$lines, $head, $tail] = $this->readAdjacentChunk($fp, $head, $tail, 1);
  469. }
  470. $lastTail = $tail;
  471. // add a possible revision of external edit, create or deletion
  472. if (
  473. $lastTail == $eof && $afterCount <= (int) ($max / 2) &&
  474. count($revs) && !$this->isCurrentRevision($revs[count($revs) - 1])
  475. ) {
  476. $revs[] = $this->currentRevision;
  477. $afterCount++;
  478. }
  479. if ($afterCount == 0) {
  480. //given timestamp $rev is newer than the most recent line in chunk
  481. return false; //FIXME: or proceed to collect older revisions?
  482. }
  483. //read more chunks backward until $max/2 is reached and total number of revs is equal to $max
  484. $lines = [];
  485. $i = 0;
  486. $head = $startHead;
  487. $tail = $startTail;
  488. while ($head > 0) {
  489. [$lines, $head, $tail] = $this->readAdjacentChunk($fp, $head, $tail, -1);
  490. for ($i = count($lines) - 1; $i >= 0; $i--) {
  491. $info = $this->parseAndCacheLogLine($lines[$i]);
  492. if (is_array($info)) {
  493. $revs[] = $info['date'];
  494. $beforeCount++;
  495. //enough revs before reference $rev?
  496. if ($beforeCount > max((int) ($max / 2), $max - $afterCount)) {
  497. break 2;
  498. }
  499. }
  500. }
  501. }
  502. //keep only non-parsed lines
  503. $lines = array_slice($lines, 0, $i);
  504. sort($revs);
  505. //trunk desired selection
  506. $requestedRevs = array_slice($revs, -$max, $max);
  507. return [$requestedRevs, $revs, $fp, $lines, $head, $lastTail];
  508. }
  509. /**
  510. * Get the current revision information, considering external edit, create or deletion
  511. *
  512. * When the file has not modified since its last revision, the information of the last
  513. * change that had already recorded in the changelog is returned as current change info.
  514. * Otherwise, the change information since the last revision caused outside DokuWiki
  515. * should be returned, which is referred as "external revision".
  516. *
  517. * The change date of the file can be determined by timestamp as far as the file exists,
  518. * however this is not possible when the file has already deleted outside of DokuWiki.
  519. * In such case we assign 1 sec before current time() for the external deletion.
  520. * As a result, the value of current revision identifier may change each time because:
  521. * 1) the file has again modified outside of DokuWiki, or
  522. * 2) the value is essentially volatile for deleted but once existed files.
  523. *
  524. * @return bool|array false when page had never existed or array with entries:
  525. * - date: revision identifier (timestamp or last revision +1)
  526. * - ip: IPv4 address (127.0.0.1)
  527. * - type: log line type
  528. * - id: id of page or media
  529. * - user: user name
  530. * - sum: edit summary (or action reason)
  531. * - extra: extra data (varies by line type)
  532. * - sizechange: change of filesize
  533. * - timestamp: unix timestamp or false (key set only for external edit occurred)
  534. * additional:
  535. * - mode: page or media
  536. *
  537. * @author Satoshi Sahara <sahara.satoshi@gmail.com>
  538. */
  539. public function getCurrentRevisionInfo()
  540. {
  541. global $lang;
  542. if (isset($this->currentRevision)) {
  543. return $this->getRevisionInfo($this->currentRevision);
  544. }
  545. // get revision id from the item file timestamp and changelog
  546. $fileLastMod = $this->getFilename();
  547. $fileRev = @filemtime($fileLastMod); // false when the file not exist
  548. $lastRev = $this->lastRevision(); // false when no changelog
  549. if (!$fileRev && !$lastRev) { // has never existed
  550. $this->currentRevision = false;
  551. return false;
  552. } elseif ($fileRev === $lastRev) { // not external edit
  553. $this->currentRevision = $lastRev;
  554. return $this->getRevisionInfo($lastRev);
  555. }
  556. if (!$fileRev && $lastRev) { // item file does not exist
  557. // check consistency against changelog
  558. $revInfo = $this->getRevisionInfo($lastRev, false);
  559. if ($revInfo['type'] == DOKU_CHANGE_TYPE_DELETE) {
  560. $this->currentRevision = $lastRev;
  561. return $revInfo;
  562. }
  563. // externally deleted, set revision date as late as possible
  564. $revInfo = [
  565. 'date' => max($lastRev + 1, time() - 1), // 1 sec before now or new page save
  566. 'ip' => '127.0.0.1',
  567. 'type' => DOKU_CHANGE_TYPE_DELETE,
  568. 'id' => $this->id,
  569. 'user' => '',
  570. 'sum' => $lang['deleted'] . ' - ' . $lang['external_edit'] . ' (' . $lang['unknowndate'] . ')',
  571. 'extra' => '',
  572. 'sizechange' => -io_getSizeFile($this->getFilename($lastRev)),
  573. 'timestamp' => false,
  574. 'mode' => $this->getMode()
  575. ];
  576. } else { // item file exists, with timestamp $fileRev
  577. // here, file timestamp $fileRev is different with last revision timestamp $lastRev in changelog
  578. $isJustCreated = $lastRev === false || (
  579. $fileRev > $lastRev &&
  580. $this->getRevisionInfo($lastRev, false)['type'] == DOKU_CHANGE_TYPE_DELETE
  581. );
  582. $filesize_new = filesize($this->getFilename());
  583. $filesize_old = $isJustCreated ? 0 : io_getSizeFile($this->getFilename($lastRev));
  584. $sizechange = $filesize_new - $filesize_old;
  585. if ($isJustCreated) {
  586. $timestamp = $fileRev;
  587. $sum = $lang['created'] . ' - ' . $lang['external_edit'];
  588. } elseif ($fileRev > $lastRev) {
  589. $timestamp = $fileRev;
  590. $sum = $lang['external_edit'];
  591. } else {
  592. // $fileRev is older than $lastRev, that is erroneous/incorrect occurrence.
  593. $msg = "Warning: current file modification time is older than last revision date";
  594. $details = 'File revision: ' . $fileRev . ' ' . dformat($fileRev, "%Y-%m-%d %H:%M:%S") . "\n"
  595. . 'Last revision: ' . $lastRev . ' ' . dformat($lastRev, "%Y-%m-%d %H:%M:%S");
  596. Logger::error($msg, $details, $this->getFilename());
  597. $timestamp = false;
  598. $sum = $lang['external_edit'] . ' (' . $lang['unknowndate'] . ')';
  599. }
  600. // externally created or edited
  601. $revInfo = [
  602. 'date' => $timestamp ?: $lastRev + 1,
  603. 'ip' => '127.0.0.1',
  604. 'type' => $isJustCreated ? DOKU_CHANGE_TYPE_CREATE : DOKU_CHANGE_TYPE_EDIT,
  605. 'id' => $this->id,
  606. 'user' => '',
  607. 'sum' => $sum,
  608. 'extra' => '',
  609. 'sizechange' => $sizechange,
  610. 'timestamp' => $timestamp,
  611. 'mode' => $this->getMode()
  612. ];
  613. }
  614. // cache current revision information of external edition
  615. $this->currentRevision = $revInfo['date'];
  616. $this->cache[$this->id][$this->currentRevision] = $revInfo;
  617. return $this->getRevisionInfo($this->currentRevision);
  618. }
  619. /**
  620. * Mechanism to trace no-actual external current revision
  621. * @param int $rev
  622. */
  623. public function traceCurrentRevision($rev)
  624. {
  625. if ($rev > $this->lastRevision()) {
  626. $rev = $this->currentRevision();
  627. }
  628. return $rev;
  629. }
  630. }