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