~ubuntu-branches/ubuntu/wily/phabricator/wily

« back to all changes in this revision

Viewing changes to src/repository/api/ArcanistRepositoryAPI.php

  • Committer: Package Import Robot
  • Author(s): Richard Sellam
  • Date: 2014-11-01 23:20:06 UTC
  • mto: This revision was merged to the branch mainline in revision 4.
  • Revision ID: package-import@ubuntu.com-20141101232006-mvlnp0cil67tsboe
Tags: upstream-0~git20141101/arcanist
Import upstream version 0~git20141101, component arcanist

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
<?php
 
2
 
 
3
/**
 
4
 * Interfaces with the VCS in the working copy.
 
5
 *
 
6
 * @task  status      Path Status
 
7
 */
 
8
abstract class ArcanistRepositoryAPI {
 
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
  const FLAG_EXTERNALS    = 256;
 
19
 
 
20
  // Occurs in SVN when you replace a file with a directory without telling
 
21
  // SVN about it.
 
22
  const FLAG_OBSTRUCTED   = 512;
 
23
 
 
24
  // Occurs in SVN when an update was interrupted or failed, e.g. you ^C'd it.
 
25
  const FLAG_INCOMPLETE   = 1024;
 
26
 
 
27
  protected $path;
 
28
  protected $diffLinesOfContext = 0x7FFF;
 
29
  private $baseCommitExplanation = '???';
 
30
  private $configurationManager;
 
31
  private $baseCommitArgumentRules;
 
32
 
 
33
  private $uncommittedStatusCache;
 
34
  private $commitRangeStatusCache;
 
35
 
 
36
  private $symbolicBaseCommit;
 
37
  private $resolvedBaseCommit;
 
38
 
 
39
  abstract public function getSourceControlSystemName();
 
40
 
 
41
  public function getDiffLinesOfContext() {
 
42
    return $this->diffLinesOfContext;
 
43
  }
 
44
 
 
45
  public function setDiffLinesOfContext($lines) {
 
46
    $this->diffLinesOfContext = $lines;
 
47
    return $this;
 
48
  }
 
49
 
 
50
  public function getWorkingCopyIdentity() {
 
51
    return $this->configurationManager->getWorkingCopyIdentity();
 
52
  }
 
53
 
 
54
  public function getConfigurationManager() {
 
55
    return $this->configurationManager;
 
56
  }
 
57
 
 
58
  public static function newAPIFromConfigurationManager(
 
59
    ArcanistConfigurationManager $configuration_manager) {
 
60
 
 
61
    $working_copy = $configuration_manager->getWorkingCopyIdentity();
 
62
 
 
63
    if (!$working_copy) {
 
64
      throw new Exception(
 
65
        pht(
 
66
          'Trying to create a RepositoryAPI without a working copy!'));
 
67
    }
 
68
 
 
69
    $root = $working_copy->getProjectRoot();
 
70
    switch ($working_copy->getVCSType()) {
 
71
      case 'svn':
 
72
        $api = new ArcanistSubversionAPI($root);
 
73
        break;
 
74
      case 'hg':
 
75
        $api = new ArcanistMercurialAPI($root);
 
76
        break;
 
77
      case 'git':
 
78
        $api = new ArcanistGitAPI($root);
 
79
        break;
 
80
      default:
 
81
        throw new Exception(
 
82
          pht(
 
83
            'The current working directory is not part of a working copy for '.
 
84
            'a supported version control system (Git, Subversion or '.
 
85
            'Mercurial).'));
 
86
    }
 
87
 
 
88
    $api->configurationManager = $configuration_manager;
 
89
    return $api;
 
90
  }
 
91
 
 
92
  public function __construct($path) {
 
93
    $this->path = $path;
 
94
  }
 
95
 
 
96
  public function getPath($to_file = null) {
 
97
    if ($to_file !== null) {
 
98
      return $this->path.DIRECTORY_SEPARATOR.
 
99
             ltrim($to_file, DIRECTORY_SEPARATOR);
 
100
    } else {
 
101
      return $this->path.DIRECTORY_SEPARATOR;
 
102
    }
 
103
  }
 
104
 
 
105
 
 
106
/* -(  Path Status  )-------------------------------------------------------- */
 
107
 
 
108
 
 
109
  abstract protected function buildUncommittedStatus();
 
110
  abstract protected function buildCommitRangeStatus();
 
111
 
 
112
 
 
113
  /**
 
114
   * Get a list of uncommitted paths in the working copy that have been changed
 
115
   * or are affected by other status effects, like conflicts or untracked
 
116
   * files.
 
117
   *
 
118
   * Convenience methods @{method:getUntrackedChanges},
 
119
   * @{method:getUnstagedChanges}, @{method:getUncommittedChanges},
 
120
   * @{method:getMergeConflicts}, and @{method:getIncompleteChanges} allow
 
121
   * simpler selection of paths in a specific state.
 
122
   *
 
123
   * This method returns a map of paths to bitmasks with status, using
 
124
   * `FLAG_` constants. For example:
 
125
   *
 
126
   *   array(
 
127
   *     'some/uncommitted/file.txt' => ArcanistRepositoryAPI::FLAG_UNSTAGED,
 
128
   *   );
 
129
   *
 
130
   * A file may be in several states. Not all states are possible with all
 
131
   * version control systems.
 
132
   *
 
133
   * @return map<string, bitmask> Map of paths, see above.
 
134
   * @task status
 
135
   */
 
136
  final public function getUncommittedStatus() {
 
137
    if ($this->uncommittedStatusCache === null) {
 
138
      $status = $this->buildUncommittedStatus();
 
139
      ksort($status);
 
140
      $this->uncommittedStatusCache = $status;
 
141
    }
 
142
    return $this->uncommittedStatusCache;
 
143
  }
 
144
 
 
145
 
 
146
  /**
 
147
   * @task status
 
148
   */
 
149
  final public function getUntrackedChanges() {
 
150
    return $this->getUncommittedPathsWithMask(self::FLAG_UNTRACKED);
 
151
  }
 
152
 
 
153
 
 
154
  /**
 
155
   * @task status
 
156
   */
 
157
  final public function getUnstagedChanges() {
 
158
    return $this->getUncommittedPathsWithMask(self::FLAG_UNSTAGED);
 
159
  }
 
160
 
 
161
 
 
162
  /**
 
163
   * @task status
 
164
   */
 
165
  final public function getUncommittedChanges() {
 
166
    return $this->getUncommittedPathsWithMask(self::FLAG_UNCOMMITTED);
 
167
  }
 
168
 
 
169
 
 
170
  /**
 
171
   * @task status
 
172
   */
 
173
  final public function getMergeConflicts() {
 
174
    return $this->getUncommittedPathsWithMask(self::FLAG_CONFLICT);
 
175
  }
 
176
 
 
177
 
 
178
  /**
 
179
   * @task status
 
180
   */
 
181
  final public function getIncompleteChanges() {
 
182
    return $this->getUncommittedPathsWithMask(self::FLAG_INCOMPLETE);
 
183
  }
 
184
 
 
185
 
 
186
  /**
 
187
   * @task status
 
188
   */
 
189
  final public function getMissingChanges() {
 
190
    return $this->getUncommittedPathsWithMask(self::FLAG_MISSING);
 
191
  }
 
192
 
 
193
 
 
194
  /**
 
195
   * @task status
 
196
   */
 
197
  private function getUncommittedPathsWithMask($mask) {
 
198
    $match = array();
 
199
    foreach ($this->getUncommittedStatus() as $path => $flags) {
 
200
      if ($flags & $mask) {
 
201
        $match[] = $path;
 
202
      }
 
203
    }
 
204
    return $match;
 
205
  }
 
206
 
 
207
 
 
208
  /**
 
209
   * Get a list of paths affected by the commits in the current commit range.
 
210
   *
 
211
   * See @{method:getUncommittedStatus} for a description of the return value.
 
212
   *
 
213
   * @return map<string, bitmask> Map from paths to status.
 
214
   * @task status
 
215
   */
 
216
  final public function getCommitRangeStatus() {
 
217
    if ($this->commitRangeStatusCache === null) {
 
218
      $status = $this->buildCommitRangeStatus();
 
219
      ksort($status);
 
220
      $this->commitRangeStatusCache = $status;
 
221
    }
 
222
    return $this->commitRangeStatusCache;
 
223
  }
 
224
 
 
225
 
 
226
  /**
 
227
   * Get a list of paths affected by commits in the current commit range, or
 
228
   * uncommitted changes in the working copy. See @{method:getUncommittedStatus}
 
229
   * or @{method:getCommitRangeStatus} to retrieve smaller parts of the status.
 
230
   *
 
231
   * See @{method:getUncommittedStatus} for a description of the return value.
 
232
   *
 
233
   * @return map<string, bitmask> Map from paths to status.
 
234
   * @task status
 
235
   */
 
236
  final public function getWorkingCopyStatus() {
 
237
    $range_status = $this->getCommitRangeStatus();
 
238
    $uncommitted_status = $this->getUncommittedStatus();
 
239
 
 
240
    $result = new PhutilArrayWithDefaultValue($range_status);
 
241
    foreach ($uncommitted_status as $path => $mask) {
 
242
      $result[$path] |= $mask;
 
243
    }
 
244
 
 
245
    $result = $result->toArray();
 
246
    ksort($result);
 
247
    return $result;
 
248
  }
 
249
 
 
250
 
 
251
  /**
 
252
   * Drops caches after changes to the working copy. By default, some queries
 
253
   * against the working copy are cached. They
 
254
   *
 
255
   * @return this
 
256
   * @task status
 
257
   */
 
258
  final public function reloadWorkingCopy() {
 
259
    $this->uncommittedStatusCache = null;
 
260
    $this->commitRangeStatusCache = null;
 
261
 
 
262
    $this->didReloadWorkingCopy();
 
263
    $this->reloadCommitRange();
 
264
 
 
265
    return $this;
 
266
  }
 
267
 
 
268
 
 
269
  /**
 
270
   * Hook for implementations to dirty working copy caches after the working
 
271
   * copy has been updated.
 
272
   *
 
273
   * @return this
 
274
   * @task status
 
275
   */
 
276
  protected function didReloadWorkingCopy() {
 
277
    return;
 
278
  }
 
279
 
 
280
 
 
281
  /**
 
282
   * Fetches the original file data for each path provided.
 
283
   *
 
284
   * @return map<string, string> Map from path to file data.
 
285
   */
 
286
  public function getBulkOriginalFileData($paths) {
 
287
    $filedata = array();
 
288
    foreach ($paths as $path) {
 
289
      $filedata[$path] = $this->getOriginalFileData($path);
 
290
    }
 
291
 
 
292
    return $filedata;
 
293
  }
 
294
 
 
295
  /**
 
296
   * Fetches the current file data for each path provided.
 
297
   *
 
298
   * @return map<string, string> Map from path to file data.
 
299
   */
 
300
  public function getBulkCurrentFileData($paths) {
 
301
    $filedata = array();
 
302
    foreach ($paths as $path) {
 
303
      $filedata[$path] = $this->getCurrentFileData($path);
 
304
    }
 
305
 
 
306
    return $filedata;
 
307
  }
 
308
 
 
309
  /**
 
310
   * @return Traversable
 
311
   */
 
312
  abstract public function getAllFiles();
 
313
 
 
314
  abstract public function getBlame($path);
 
315
 
 
316
  abstract public function getRawDiffText($path);
 
317
  abstract public function getOriginalFileData($path);
 
318
  abstract public function getCurrentFileData($path);
 
319
  abstract public function getLocalCommitInformation();
 
320
  abstract public function getSourceControlBaseRevision();
 
321
  abstract public function getCanonicalRevisionName($string);
 
322
  abstract public function getBranchName();
 
323
  abstract public function getSourceControlPath();
 
324
  abstract public function isHistoryDefaultImmutable();
 
325
  abstract public function supportsAmend();
 
326
  abstract public function getWorkingCopyRevision();
 
327
  abstract public function updateWorkingCopy();
 
328
  abstract public function getMetadataPath();
 
329
  abstract public function loadWorkingCopyDifferentialRevisions(
 
330
    ConduitClient $conduit,
 
331
    array $query);
 
332
  abstract public function getRemoteURI();
 
333
 
 
334
 
 
335
  public function getUnderlyingWorkingCopyRevision() {
 
336
    return $this->getWorkingCopyRevision();
 
337
  }
 
338
 
 
339
  public function getChangedFiles($since_commit) {
 
340
    throw new ArcanistCapabilityNotSupportedException($this);
 
341
  }
 
342
 
 
343
  public function getAuthor() {
 
344
    throw new ArcanistCapabilityNotSupportedException($this);
 
345
  }
 
346
 
 
347
  public function addToCommit(array $paths) {
 
348
    throw new ArcanistCapabilityNotSupportedException($this);
 
349
  }
 
350
 
 
351
  abstract public function supportsLocalCommits();
 
352
 
 
353
  public function doCommit($message) {
 
354
    throw new ArcanistCapabilityNotSupportedException($this);
 
355
  }
 
356
 
 
357
  public function amendCommit($message = null) {
 
358
    throw new ArcanistCapabilityNotSupportedException($this);
 
359
  }
 
360
 
 
361
  public function getAllBranches() {
 
362
    // TODO: Implement for Mercurial/SVN and make abstract.
 
363
    return array();
 
364
  }
 
365
 
 
366
  public function hasLocalCommit($commit) {
 
367
    throw new ArcanistCapabilityNotSupportedException($this);
 
368
  }
 
369
 
 
370
  public function getCommitMessage($commit) {
 
371
    throw new ArcanistCapabilityNotSupportedException($this);
 
372
  }
 
373
 
 
374
  public function getCommitSummary($commit) {
 
375
    throw new ArcanistCapabilityNotSupportedException($this);
 
376
  }
 
377
 
 
378
  public function getAllLocalChanges() {
 
379
    throw new ArcanistCapabilityNotSupportedException($this);
 
380
  }
 
381
 
 
382
  abstract public function supportsLocalBranchMerge();
 
383
 
 
384
  public function performLocalBranchMerge($branch, $message) {
 
385
    throw new ArcanistCapabilityNotSupportedException($this);
 
386
  }
 
387
 
 
388
  public function getFinalizedRevisionMessage() {
 
389
    throw new ArcanistCapabilityNotSupportedException($this);
 
390
  }
 
391
 
 
392
  public function execxLocal($pattern /* , ... */) {
 
393
    $args = func_get_args();
 
394
    return $this->buildLocalFuture($args)->resolvex();
 
395
  }
 
396
 
 
397
  public function execManualLocal($pattern /* , ... */) {
 
398
    $args = func_get_args();
 
399
    return $this->buildLocalFuture($args)->resolve();
 
400
  }
 
401
 
 
402
  public function execFutureLocal($pattern /* , ... */) {
 
403
    $args = func_get_args();
 
404
    return $this->buildLocalFuture($args);
 
405
  }
 
406
 
 
407
  abstract protected function buildLocalFuture(array $argv);
 
408
 
 
409
  public function canStashChanges() {
 
410
    return false;
 
411
  }
 
412
 
 
413
  public function stashChanges() {
 
414
    throw new ArcanistCapabilityNotSupportedException($this);
 
415
  }
 
416
 
 
417
  public function unstashChanges() {
 
418
    throw new ArcanistCapabilityNotSupportedException($this);
 
419
  }
 
420
 
 
421
/* -(  Scratch Files  )------------------------------------------------------ */
 
422
 
 
423
 
 
424
  /**
 
425
   * Try to read a scratch file, if it exists and is readable.
 
426
   *
 
427
   * @param string Scratch file name.
 
428
   * @return mixed String for file contents, or false for failure.
 
429
   * @task scratch
 
430
   */
 
431
  public function readScratchFile($path) {
 
432
    $full_path = $this->getScratchFilePath($path);
 
433
    if (!$full_path) {
 
434
      return false;
 
435
    }
 
436
 
 
437
    if (!Filesystem::pathExists($full_path)) {
 
438
      return false;
 
439
    }
 
440
 
 
441
    try {
 
442
      $result = Filesystem::readFile($full_path);
 
443
    } catch (FilesystemException $ex) {
 
444
      return false;
 
445
    }
 
446
 
 
447
    return $result;
 
448
  }
 
449
 
 
450
 
 
451
  /**
 
452
   * Try to write a scratch file, if there's somewhere to put it and we can
 
453
   * write there.
 
454
   *
 
455
   * @param  string Scratch file name to write.
 
456
   * @param  string Data to write.
 
457
   * @return bool   True on success, false on failure.
 
458
   * @task scratch
 
459
   */
 
460
  public function writeScratchFile($path, $data) {
 
461
    $dir = $this->getScratchFilePath('');
 
462
    if (!$dir) {
 
463
      return false;
 
464
    }
 
465
 
 
466
    if (!Filesystem::pathExists($dir)) {
 
467
      try {
 
468
        Filesystem::createDirectory($dir);
 
469
      } catch (Exception $ex) {
 
470
        return false;
 
471
      }
 
472
    }
 
473
 
 
474
    try {
 
475
      Filesystem::writeFile($this->getScratchFilePath($path), $data);
 
476
    } catch (FilesystemException $ex) {
 
477
      return false;
 
478
    }
 
479
 
 
480
    return true;
 
481
  }
 
482
 
 
483
 
 
484
  /**
 
485
   * Try to remove a scratch file.
 
486
   *
 
487
   * @param   string  Scratch file name to remove.
 
488
   * @return  bool    True if the file was removed successfully.
 
489
   * @task scratch
 
490
   */
 
491
  public function removeScratchFile($path) {
 
492
    $full_path = $this->getScratchFilePath($path);
 
493
    if (!$full_path) {
 
494
      return false;
 
495
    }
 
496
 
 
497
    try {
 
498
      Filesystem::remove($full_path);
 
499
    } catch (FilesystemException $ex) {
 
500
      return false;
 
501
    }
 
502
 
 
503
    return true;
 
504
  }
 
505
 
 
506
 
 
507
  /**
 
508
   * Get a human-readable description of the scratch file location.
 
509
   *
 
510
   * @param string  Scratch file name.
 
511
   * @return mixed  String, or false on failure.
 
512
   * @task scratch
 
513
   */
 
514
  public function getReadableScratchFilePath($path) {
 
515
    $full_path = $this->getScratchFilePath($path);
 
516
    if ($full_path) {
 
517
      return Filesystem::readablePath(
 
518
        $full_path,
 
519
        $this->getPath());
 
520
    } else {
 
521
      return false;
 
522
    }
 
523
  }
 
524
 
 
525
 
 
526
  /**
 
527
   * Get the path to a scratch file, if possible.
 
528
   *
 
529
   * @param string  Scratch file name.
 
530
   * @return mixed  File path, or false on failure.
 
531
   * @task scratch
 
532
   */
 
533
  public function getScratchFilePath($path) {
 
534
    $new_scratch_path  = Filesystem::resolvePath(
 
535
      'arc',
 
536
      $this->getMetadataPath());
 
537
 
 
538
    static $checked = false;
 
539
    if (!$checked) {
 
540
      $checked = true;
 
541
      $old_scratch_path = $this->getPath('.arc');
 
542
      // we only want to do the migration once
 
543
      // unfortunately, people have checked in .arc directories which
 
544
      // means that the old one may get recreated after we delete it
 
545
      if (Filesystem::pathExists($old_scratch_path) &&
 
546
          !Filesystem::pathExists($new_scratch_path)) {
 
547
        Filesystem::createDirectory($new_scratch_path);
 
548
        $existing_files = Filesystem::listDirectory($old_scratch_path, true);
 
549
        foreach ($existing_files as $file) {
 
550
          $new_path = Filesystem::resolvePath($file, $new_scratch_path);
 
551
          $old_path = Filesystem::resolvePath($file, $old_scratch_path);
 
552
          Filesystem::writeFile(
 
553
            $new_path,
 
554
            Filesystem::readFile($old_path));
 
555
        }
 
556
        Filesystem::remove($old_scratch_path);
 
557
      }
 
558
    }
 
559
    return Filesystem::resolvePath($path, $new_scratch_path);
 
560
  }
 
561
 
 
562
 
 
563
/* -(  Base Commits  )------------------------------------------------------- */
 
564
 
 
565
  abstract public function supportsCommitRanges();
 
566
 
 
567
  final public function setBaseCommit($symbolic_commit) {
 
568
    if (!$this->supportsCommitRanges()) {
 
569
      throw new ArcanistCapabilityNotSupportedException($this);
 
570
    }
 
571
 
 
572
    $this->symbolicBaseCommit = $symbolic_commit;
 
573
    $this->reloadCommitRange();
 
574
    return $this;
 
575
  }
 
576
 
 
577
  public function setHeadCommit($symbolic_commit) {
 
578
    throw new ArcanistCapabilityNotSupportedException($this);
 
579
  }
 
580
 
 
581
  final public function getBaseCommit() {
 
582
    if (!$this->supportsCommitRanges()) {
 
583
      throw new ArcanistCapabilityNotSupportedException($this);
 
584
    }
 
585
 
 
586
    if ($this->resolvedBaseCommit === null) {
 
587
      $commit = $this->buildBaseCommit($this->symbolicBaseCommit);
 
588
      $this->resolvedBaseCommit = $commit;
 
589
    }
 
590
 
 
591
    return $this->resolvedBaseCommit;
 
592
  }
 
593
 
 
594
  public function getHeadCommit() {
 
595
    throw new ArcanistCapabilityNotSupportedException($this);
 
596
  }
 
597
 
 
598
  final public function reloadCommitRange() {
 
599
    $this->resolvedBaseCommit = null;
 
600
    $this->baseCommitExplanation = null;
 
601
 
 
602
    $this->didReloadCommitRange();
 
603
 
 
604
    return $this;
 
605
  }
 
606
 
 
607
  protected function didReloadCommitRange() {
 
608
    return;
 
609
  }
 
610
 
 
611
  protected function buildBaseCommit($symbolic_commit) {
 
612
    throw new ArcanistCapabilityNotSupportedException($this);
 
613
  }
 
614
 
 
615
  public function getBaseCommitExplanation() {
 
616
    return $this->baseCommitExplanation;
 
617
  }
 
618
 
 
619
  public function setBaseCommitExplanation($explanation) {
 
620
    $this->baseCommitExplanation = $explanation;
 
621
    return $this;
 
622
  }
 
623
 
 
624
  public function resolveBaseCommitRule($rule, $source) {
 
625
    return null;
 
626
  }
 
627
 
 
628
  public function setBaseCommitArgumentRules($base_commit_argument_rules) {
 
629
    $this->baseCommitArgumentRules = $base_commit_argument_rules;
 
630
    return $this;
 
631
  }
 
632
 
 
633
  public function getBaseCommitArgumentRules() {
 
634
    return $this->baseCommitArgumentRules;
 
635
  }
 
636
 
 
637
  public function resolveBaseCommit() {
 
638
    $base_commit_rules = array(
 
639
      'runtime' => $this->getBaseCommitArgumentRules(),
 
640
      'local'   => '',
 
641
      'project' => '',
 
642
      'user'    => '',
 
643
      'system'  => '',
 
644
    );
 
645
    $all_sources = $this->configurationManager->getConfigFromAllSources('base');
 
646
 
 
647
    $base_commit_rules = $all_sources + $base_commit_rules;
 
648
 
 
649
    $parser = new ArcanistBaseCommitParser($this);
 
650
    $commit = $parser->resolveBaseCommit($base_commit_rules);
 
651
 
 
652
    return $commit;
 
653
  }
 
654
 
 
655
  public function getRepositoryUUID() {
 
656
    return null;
 
657
  }
 
658
 
 
659
}