1<?php 2 3/** 4 * Interfaces with the VCS in the working copy. 5 * 6 * @task status Path Status 7 */ 8abstract class ArcanistRepositoryAPI extends Phobject { 9 10 const FLAG_MODIFIED = 1; 11 const FLAG_ADDED = 2; 12 const FLAG_DELETED = 4; 13 const FLAG_UNTRACKED = 8; 14 const FLAG_CONFLICT = 16; 15 const FLAG_MISSING = 32; 16 const FLAG_UNSTAGED = 64; 17 const FLAG_UNCOMMITTED = 128; 18 19 // Occurs in SVN when you have uncommitted changes to a modified external, 20 // or in Git when you have uncommitted or untracked changes in a submodule. 21 const FLAG_EXTERNALS = 256; 22 23 // Occurs in SVN when you replace a file with a directory without telling 24 // SVN about it. 25 const FLAG_OBSTRUCTED = 512; 26 27 // Occurs in SVN when an update was interrupted or failed, e.g. you ^C'd it. 28 const FLAG_INCOMPLETE = 1024; 29 30 protected $path; 31 protected $diffLinesOfContext = 0x7FFF; 32 private $baseCommitExplanation = '???'; 33 private $configurationManager; 34 private $baseCommitArgumentRules; 35 36 private $uncommittedStatusCache; 37 private $commitRangeStatusCache; 38 39 private $symbolicBaseCommit; 40 private $resolvedBaseCommit; 41 42 private $runtime; 43 private $currentWorkingCopyStateRef = false; 44 private $currentCommitRef = false; 45 private $graph; 46 47 abstract public function getSourceControlSystemName(); 48 49 public function getDiffLinesOfContext() { 50 return $this->diffLinesOfContext; 51 } 52 53 public function setDiffLinesOfContext($lines) { 54 $this->diffLinesOfContext = $lines; 55 return $this; 56 } 57 58 public function getWorkingCopyIdentity() { 59 return $this->configurationManager->getWorkingCopyIdentity(); 60 } 61 62 public function getConfigurationManager() { 63 return $this->configurationManager; 64 } 65 66 public static function newAPIFromConfigurationManager( 67 ArcanistConfigurationManager $configuration_manager) { 68 69 $working_copy = $configuration_manager->getWorkingCopyIdentity(); 70 71 if (!$working_copy) { 72 throw new Exception( 73 pht( 74 'Trying to create a %s without a working copy!', 75 __CLASS__)); 76 } 77 78 $root = $working_copy->getProjectRoot(); 79 switch ($working_copy->getVCSType()) { 80 case 'svn': 81 $api = new ArcanistSubversionAPI($root); 82 break; 83 case 'hg': 84 $api = new ArcanistMercurialAPI($root); 85 break; 86 case 'git': 87 $api = new ArcanistGitAPI($root); 88 break; 89 default: 90 throw new Exception( 91 pht( 92 'The current working directory is not part of a working copy for '. 93 'a supported version control system (Git, Subversion or '. 94 'Mercurial).')); 95 } 96 97 $api->configurationManager = $configuration_manager; 98 return $api; 99 } 100 101 public function __construct($path) { 102 $this->path = $path; 103 } 104 105 public function getPath($to_file = null) { 106 if ($to_file !== null) { 107 return $this->path.DIRECTORY_SEPARATOR. 108 ltrim($to_file, DIRECTORY_SEPARATOR); 109 } else { 110 return $this->path.DIRECTORY_SEPARATOR; 111 } 112 } 113 114 115/* -( Path Status )-------------------------------------------------------- */ 116 117 118 abstract protected function buildUncommittedStatus(); 119 abstract protected function buildCommitRangeStatus(); 120 121 122 /** 123 * Get a list of uncommitted paths in the working copy that have been changed 124 * or are affected by other status effects, like conflicts or untracked 125 * files. 126 * 127 * Convenience methods @{method:getUntrackedChanges}, 128 * @{method:getUnstagedChanges}, @{method:getUncommittedChanges}, 129 * @{method:getMergeConflicts}, and @{method:getIncompleteChanges} allow 130 * simpler selection of paths in a specific state. 131 * 132 * This method returns a map of paths to bitmasks with status, using 133 * `FLAG_` constants. For example: 134 * 135 * array( 136 * 'some/uncommitted/file.txt' => ArcanistRepositoryAPI::FLAG_UNSTAGED, 137 * ); 138 * 139 * A file may be in several states. Not all states are possible with all 140 * version control systems. 141 * 142 * @return map<string, bitmask> Map of paths, see above. 143 * @task status 144 */ 145 final public function getUncommittedStatus() { 146 if ($this->uncommittedStatusCache === null) { 147 $status = $this->buildUncommittedStatus(); 148 ksort($status); 149 $this->uncommittedStatusCache = $status; 150 } 151 return $this->uncommittedStatusCache; 152 } 153 154 155 /** 156 * @task status 157 */ 158 final public function getUntrackedChanges() { 159 return $this->getUncommittedPathsWithMask(self::FLAG_UNTRACKED); 160 } 161 162 163 /** 164 * @task status 165 */ 166 final public function getUnstagedChanges() { 167 return $this->getUncommittedPathsWithMask(self::FLAG_UNSTAGED); 168 } 169 170 171 /** 172 * @task status 173 */ 174 final public function getUncommittedChanges() { 175 return $this->getUncommittedPathsWithMask(self::FLAG_UNCOMMITTED); 176 } 177 178 179 /** 180 * @task status 181 */ 182 final public function getMergeConflicts() { 183 return $this->getUncommittedPathsWithMask(self::FLAG_CONFLICT); 184 } 185 186 187 /** 188 * @task status 189 */ 190 final public function getIncompleteChanges() { 191 return $this->getUncommittedPathsWithMask(self::FLAG_INCOMPLETE); 192 } 193 194 195 /** 196 * @task status 197 */ 198 final public function getMissingChanges() { 199 return $this->getUncommittedPathsWithMask(self::FLAG_MISSING); 200 } 201 202 203 /** 204 * @task status 205 */ 206 final public function getDirtyExternalChanges() { 207 return $this->getUncommittedPathsWithMask(self::FLAG_EXTERNALS); 208 } 209 210 211 /** 212 * @task status 213 */ 214 private function getUncommittedPathsWithMask($mask) { 215 $match = array(); 216 foreach ($this->getUncommittedStatus() as $path => $flags) { 217 if ($flags & $mask) { 218 $match[] = $path; 219 } 220 } 221 return $match; 222 } 223 224 225 /** 226 * Get a list of paths affected by the commits in the current commit range. 227 * 228 * See @{method:getUncommittedStatus} for a description of the return value. 229 * 230 * @return map<string, bitmask> Map from paths to status. 231 * @task status 232 */ 233 final public function getCommitRangeStatus() { 234 if ($this->commitRangeStatusCache === null) { 235 $status = $this->buildCommitRangeStatus(); 236 ksort($status); 237 $this->commitRangeStatusCache = $status; 238 } 239 return $this->commitRangeStatusCache; 240 } 241 242 243 /** 244 * Get a list of paths affected by commits in the current commit range, or 245 * uncommitted changes in the working copy. See @{method:getUncommittedStatus} 246 * or @{method:getCommitRangeStatus} to retrieve smaller parts of the status. 247 * 248 * See @{method:getUncommittedStatus} for a description of the return value. 249 * 250 * @return map<string, bitmask> Map from paths to status. 251 * @task status 252 */ 253 final public function getWorkingCopyStatus() { 254 $range_status = $this->getCommitRangeStatus(); 255 $uncommitted_status = $this->getUncommittedStatus(); 256 257 $result = new PhutilArrayWithDefaultValue($range_status); 258 foreach ($uncommitted_status as $path => $mask) { 259 $result[$path] |= $mask; 260 } 261 262 $result = $result->toArray(); 263 ksort($result); 264 return $result; 265 } 266 267 268 /** 269 * Drops caches after changes to the working copy. By default, some queries 270 * against the working copy are cached. They 271 * 272 * @return this 273 * @task status 274 */ 275 final public function reloadWorkingCopy() { 276 $this->uncommittedStatusCache = null; 277 $this->commitRangeStatusCache = null; 278 279 $this->didReloadWorkingCopy(); 280 $this->reloadCommitRange(); 281 282 return $this; 283 } 284 285 286 /** 287 * Hook for implementations to dirty working copy caches after the working 288 * copy has been updated. 289 * 290 * @return void 291 * @task status 292 */ 293 protected function didReloadWorkingCopy() { 294 return; 295 } 296 297 298 /** 299 * Fetches the original file data for each path provided. 300 * 301 * @return map<string, string> Map from path to file data. 302 */ 303 public function getBulkOriginalFileData($paths) { 304 $filedata = array(); 305 foreach ($paths as $path) { 306 $filedata[$path] = $this->getOriginalFileData($path); 307 } 308 309 return $filedata; 310 } 311 312 /** 313 * Fetches the current file data for each path provided. 314 * 315 * @return map<string, string> Map from path to file data. 316 */ 317 public function getBulkCurrentFileData($paths) { 318 $filedata = array(); 319 foreach ($paths as $path) { 320 $filedata[$path] = $this->getCurrentFileData($path); 321 } 322 323 return $filedata; 324 } 325 326 /** 327 * @return Traversable 328 */ 329 abstract public function getAllFiles(); 330 331 abstract public function getBlame($path); 332 333 abstract public function getRawDiffText($path); 334 abstract public function getOriginalFileData($path); 335 abstract public function getCurrentFileData($path); 336 abstract public function getLocalCommitInformation(); 337 abstract public function getSourceControlBaseRevision(); 338 abstract public function getCanonicalRevisionName($string); 339 abstract public function getBranchName(); 340 abstract public function getSourceControlPath(); 341 abstract public function isHistoryDefaultImmutable(); 342 abstract public function supportsAmend(); 343 abstract public function getWorkingCopyRevision(); 344 abstract public function updateWorkingCopy(); 345 abstract public function getMetadataPath(); 346 abstract public function loadWorkingCopyDifferentialRevisions( 347 ConduitClient $conduit, 348 array $query); 349 abstract public function getRemoteURI(); 350 351 public function getChangedFiles($since_commit) { 352 throw new ArcanistCapabilityNotSupportedException($this); 353 } 354 355 public function getAuthor() { 356 throw new ArcanistCapabilityNotSupportedException($this); 357 } 358 359 public function addToCommit(array $paths) { 360 throw new ArcanistCapabilityNotSupportedException($this); 361 } 362 363 abstract public function supportsLocalCommits(); 364 365 public function doCommit($message) { 366 throw new ArcanistCapabilityNotSupportedException($this); 367 } 368 369 public function amendCommit($message = null) { 370 throw new ArcanistCapabilityNotSupportedException($this); 371 } 372 373 public function getBaseCommitRef() { 374 throw new ArcanistCapabilityNotSupportedException($this); 375 } 376 377 public function hasLocalCommit($commit) { 378 throw new ArcanistCapabilityNotSupportedException($this); 379 } 380 381 public function getCommitMessage($commit) { 382 throw new ArcanistCapabilityNotSupportedException($this); 383 } 384 385 public function getCommitSummary($commit) { 386 throw new ArcanistCapabilityNotSupportedException($this); 387 } 388 389 public function getAllLocalChanges() { 390 throw new ArcanistCapabilityNotSupportedException($this); 391 } 392 393 public function getFinalizedRevisionMessage() { 394 throw new ArcanistCapabilityNotSupportedException($this); 395 } 396 397 public function execxLocal($pattern /* , ... */) { 398 $args = func_get_args(); 399 return $this->buildLocalFuture($args)->resolvex(); 400 } 401 402 public function execManualLocal($pattern /* , ... */) { 403 $args = func_get_args(); 404 return $this->buildLocalFuture($args)->resolve(); 405 } 406 407 public function execFutureLocal($pattern /* , ... */) { 408 $args = func_get_args(); 409 return $this->buildLocalFuture($args); 410 } 411 412 abstract protected function buildLocalFuture(array $argv); 413 414 public function canStashChanges() { 415 return false; 416 } 417 418 public function stashChanges() { 419 throw new ArcanistCapabilityNotSupportedException($this); 420 } 421 422 public function unstashChanges() { 423 throw new ArcanistCapabilityNotSupportedException($this); 424 } 425 426/* -( Scratch Files )------------------------------------------------------ */ 427 428 429 /** 430 * Try to read a scratch file, if it exists and is readable. 431 * 432 * @param string Scratch file name. 433 * @return mixed String for file contents, or false for failure. 434 * @task scratch 435 */ 436 public function readScratchFile($path) { 437 $full_path = $this->getScratchFilePath($path); 438 if (!$full_path) { 439 return false; 440 } 441 442 if (!Filesystem::pathExists($full_path)) { 443 return false; 444 } 445 446 try { 447 $result = Filesystem::readFile($full_path); 448 } catch (FilesystemException $ex) { 449 return false; 450 } 451 452 return $result; 453 } 454 455 456 /** 457 * Try to write a scratch file, if there's somewhere to put it and we can 458 * write there. 459 * 460 * @param string Scratch file name to write. 461 * @param string Data to write. 462 * @return bool True on success, false on failure. 463 * @task scratch 464 */ 465 public function writeScratchFile($path, $data) { 466 $dir = $this->getScratchFilePath(''); 467 if (!$dir) { 468 return false; 469 } 470 471 if (!Filesystem::pathExists($dir)) { 472 try { 473 Filesystem::createDirectory($dir); 474 } catch (Exception $ex) { 475 return false; 476 } 477 } 478 479 try { 480 Filesystem::writeFile($this->getScratchFilePath($path), $data); 481 } catch (FilesystemException $ex) { 482 return false; 483 } 484 485 return true; 486 } 487 488 489 /** 490 * Try to remove a scratch file. 491 * 492 * @param string Scratch file name to remove. 493 * @return bool True if the file was removed successfully. 494 * @task scratch 495 */ 496 public function removeScratchFile($path) { 497 $full_path = $this->getScratchFilePath($path); 498 if (!$full_path) { 499 return false; 500 } 501 502 try { 503 Filesystem::remove($full_path); 504 } catch (FilesystemException $ex) { 505 return false; 506 } 507 508 return true; 509 } 510 511 512 /** 513 * Get a human-readable description of the scratch file location. 514 * 515 * @param string Scratch file name. 516 * @return mixed String, or false on failure. 517 * @task scratch 518 */ 519 public function getReadableScratchFilePath($path) { 520 $full_path = $this->getScratchFilePath($path); 521 if ($full_path) { 522 return Filesystem::readablePath( 523 $full_path, 524 $this->getPath()); 525 } else { 526 return false; 527 } 528 } 529 530 531 /** 532 * Get the path to a scratch file, if possible. 533 * 534 * @param string Scratch file name. 535 * @return mixed File path, or false on failure. 536 * @task scratch 537 */ 538 public function getScratchFilePath($path) { 539 $new_scratch_path = Filesystem::resolvePath( 540 'arc', 541 $this->getMetadataPath()); 542 543 static $checked = false; 544 if (!$checked) { 545 $checked = true; 546 $old_scratch_path = $this->getPath('.arc'); 547 // we only want to do the migration once 548 // unfortunately, people have checked in .arc directories which 549 // means that the old one may get recreated after we delete it 550 if (Filesystem::pathExists($old_scratch_path) && 551 !Filesystem::pathExists($new_scratch_path)) { 552 Filesystem::createDirectory($new_scratch_path); 553 $existing_files = Filesystem::listDirectory($old_scratch_path, true); 554 foreach ($existing_files as $file) { 555 $new_path = Filesystem::resolvePath($file, $new_scratch_path); 556 $old_path = Filesystem::resolvePath($file, $old_scratch_path); 557 Filesystem::writeFile( 558 $new_path, 559 Filesystem::readFile($old_path)); 560 } 561 Filesystem::remove($old_scratch_path); 562 } 563 } 564 return Filesystem::resolvePath($path, $new_scratch_path); 565 } 566 567 568/* -( Base Commits )------------------------------------------------------- */ 569 570 abstract public function supportsCommitRanges(); 571 572 final public function setBaseCommit($symbolic_commit) { 573 if (!$this->supportsCommitRanges()) { 574 throw new ArcanistCapabilityNotSupportedException($this); 575 } 576 577 $this->symbolicBaseCommit = $symbolic_commit; 578 $this->reloadCommitRange(); 579 return $this; 580 } 581 582 public function setHeadCommit($symbolic_commit) { 583 throw new ArcanistCapabilityNotSupportedException($this); 584 } 585 586 final public function getBaseCommit() { 587 if (!$this->supportsCommitRanges()) { 588 throw new ArcanistCapabilityNotSupportedException($this); 589 } 590 591 if ($this->resolvedBaseCommit === null) { 592 $commit = $this->buildBaseCommit($this->symbolicBaseCommit); 593 $this->resolvedBaseCommit = $commit; 594 } 595 596 return $this->resolvedBaseCommit; 597 } 598 599 public function getHeadCommit() { 600 throw new ArcanistCapabilityNotSupportedException($this); 601 } 602 603 final public function reloadCommitRange() { 604 $this->resolvedBaseCommit = null; 605 $this->baseCommitExplanation = null; 606 607 $this->didReloadCommitRange(); 608 609 return $this; 610 } 611 612 protected function didReloadCommitRange() { 613 return; 614 } 615 616 protected function buildBaseCommit($symbolic_commit) { 617 throw new ArcanistCapabilityNotSupportedException($this); 618 } 619 620 public function getBaseCommitExplanation() { 621 return $this->baseCommitExplanation; 622 } 623 624 public function setBaseCommitExplanation($explanation) { 625 $this->baseCommitExplanation = $explanation; 626 return $this; 627 } 628 629 public function resolveBaseCommitRule($rule, $source) { 630 return null; 631 } 632 633 public function setBaseCommitArgumentRules($base_commit_argument_rules) { 634 $this->baseCommitArgumentRules = $base_commit_argument_rules; 635 return $this; 636 } 637 638 public function getBaseCommitArgumentRules() { 639 return $this->baseCommitArgumentRules; 640 } 641 642 public function resolveBaseCommit() { 643 $base_commit_rules = array( 644 'runtime' => $this->getBaseCommitArgumentRules(), 645 'local' => '', 646 'project' => '', 647 'user' => '', 648 'system' => '', 649 ); 650 $all_sources = $this->configurationManager->getConfigFromAllSources('base'); 651 652 $base_commit_rules = $all_sources + $base_commit_rules; 653 654 $parser = new ArcanistBaseCommitParser($this); 655 $commit = $parser->resolveBaseCommit($base_commit_rules); 656 657 return $commit; 658 } 659 660 public function getRepositoryUUID() { 661 return null; 662 } 663 664 final public function newFuture($pattern /* , ... */) { 665 $args = func_get_args(); 666 return $this->buildLocalFuture($args) 667 ->setResolveOnError(false); 668 } 669 670 public function newPassthru($pattern /* , ... */) { 671 throw new PhutilMethodNotImplementedException(); 672 } 673 674 final public function execPassthru($pattern /* , ... */) { 675 $args = func_get_args(); 676 677 $future = call_user_func_array( 678 array($this, 'newPassthru'), 679 $args); 680 681 return $future->resolve(); 682 } 683 684 final public function setRuntime(ArcanistRuntime $runtime) { 685 $this->runtime = $runtime; 686 return $this; 687 } 688 689 final public function getRuntime() { 690 return $this->runtime; 691 } 692 693 final protected function getSymbolEngine() { 694 return $this->getRuntime()->getSymbolEngine(); 695 } 696 697 final public function getCurrentWorkingCopyStateRef() { 698 if ($this->currentWorkingCopyStateRef === false) { 699 $ref = $this->newCurrentWorkingCopyStateRef(); 700 $this->currentWorkingCopyStateRef = $ref; 701 } 702 703 return $this->currentWorkingCopyStateRef; 704 } 705 706 protected function newCurrentWorkingCopyStateRef() { 707 $commit_ref = $this->getCurrentCommitRef(); 708 709 if (!$commit_ref) { 710 return null; 711 } 712 713 return id(new ArcanistWorkingCopyStateRef()) 714 ->setCommitRef($commit_ref); 715 } 716 717 final public function getCurrentCommitRef() { 718 if ($this->currentCommitRef === false) { 719 $this->currentCommitRef = $this->newCurrentCommitRef(); 720 } 721 return $this->currentCommitRef; 722 } 723 724 protected function newCurrentCommitRef() { 725 $symbols = $this->getSymbolEngine(); 726 727 $commit_symbol = $this->newCurrentCommitSymbol(); 728 729 return $symbols->loadCommitForSymbol($commit_symbol); 730 } 731 732 protected function newCurrentCommitSymbol() { 733 throw new ArcanistCapabilityNotSupportedException($this); 734 } 735 736 final public function newCommitRef() { 737 return new ArcanistCommitRef(); 738 } 739 740 final public function newMarkerRef() { 741 return new ArcanistMarkerRef(); 742 } 743 744 final public function getLandEngine() { 745 $engine = $this->newLandEngine(); 746 747 if ($engine) { 748 $engine->setRepositoryAPI($this); 749 } 750 751 return $engine; 752 } 753 754 protected function newLandEngine() { 755 return null; 756 } 757 758 final public function getWorkEngine() { 759 $engine = $this->newWorkEngine(); 760 761 if ($engine) { 762 $engine->setRepositoryAPI($this); 763 } 764 765 return $engine; 766 } 767 768 protected function newWorkEngine() { 769 return null; 770 } 771 772 final public function getSupportedMarkerTypes() { 773 return $this->newSupportedMarkerTypes(); 774 } 775 776 protected function newSupportedMarkerTypes() { 777 return array(); 778 } 779 780 final public function newMarkerRefQuery() { 781 return id($this->newMarkerRefQueryTemplate()) 782 ->setRepositoryAPI($this); 783 } 784 785 protected function newMarkerRefQueryTemplate() { 786 throw new PhutilMethodNotImplementedException(); 787 } 788 789 final public function newRemoteRefQuery() { 790 return id($this->newRemoteRefQueryTemplate()) 791 ->setRepositoryAPI($this); 792 } 793 794 protected function newRemoteRefQueryTemplate() { 795 throw new PhutilMethodNotImplementedException(); 796 } 797 798 final public function newCommitGraphQuery() { 799 return id($this->newCommitGraphQueryTemplate()); 800 } 801 802 protected function newCommitGraphQueryTemplate() { 803 throw new PhutilMethodNotImplementedException(); 804 } 805 806 final public function getDisplayHash($hash) { 807 return substr($hash, 0, 12); 808 } 809 810 811 final public function getNormalizedURI($uri) { 812 $normalized_uri = $this->newNormalizedURI($uri); 813 return $normalized_uri->getNormalizedURI(); 814 } 815 816 protected function newNormalizedURI($uri) { 817 return $uri; 818 } 819 820 final public function getPublishedCommitHashes() { 821 return $this->newPublishedCommitHashes(); 822 } 823 824 protected function newPublishedCommitHashes() { 825 return array(); 826 } 827 828 final public function getGraph() { 829 if (!$this->graph) { 830 $this->graph = id(new ArcanistCommitGraph()) 831 ->setRepositoryAPI($this); 832 } 833 834 return $this->graph; 835 } 836 837} 838