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

« back to all changes in this revision

Viewing changes to src/repository/api/ArcanistSubversionAPI.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 Subversion working copies.
 
5
 */
 
6
final class ArcanistSubversionAPI extends ArcanistRepositoryAPI {
 
7
 
 
8
  protected $svnStatus;
 
9
  protected $svnBaseRevisions;
 
10
  protected $svnInfo = array();
 
11
 
 
12
  protected $svnInfoRaw = array();
 
13
  protected $svnDiffRaw = array();
 
14
 
 
15
  private $svnBaseRevisionNumber;
 
16
  private $statusPaths = array();
 
17
 
 
18
  public function getSourceControlSystemName() {
 
19
    return 'svn';
 
20
  }
 
21
 
 
22
  public function getMetadataPath() {
 
23
    static $svn_dir = null;
 
24
    if ($svn_dir === null) {
 
25
      // from svn 1.7, subversion keeps a single .svn directly under
 
26
      // the working copy root. However, we allow .arcconfigs that
 
27
      // aren't at the working copy root.
 
28
      foreach (Filesystem::walkToRoot($this->getPath()) as $parent) {
 
29
        $possible_svn_dir = Filesystem::resolvePath('.svn', $parent);
 
30
        if (Filesystem::pathExists($possible_svn_dir)) {
 
31
          $svn_dir = $possible_svn_dir;
 
32
          break;
 
33
        }
 
34
      }
 
35
    }
 
36
    return $svn_dir;
 
37
  }
 
38
 
 
39
  protected function buildLocalFuture(array $argv) {
 
40
 
 
41
    $argv[0] = 'svn '.$argv[0];
 
42
 
 
43
    $future = newv('ExecFuture', $argv);
 
44
    $future->setCWD($this->getPath());
 
45
    return $future;
 
46
  }
 
47
 
 
48
  protected function buildCommitRangeStatus() {
 
49
    // In SVN, the commit range is always "uncommitted changes", so these
 
50
    // statuses are equivalent.
 
51
    return $this->getUncommittedStatus();
 
52
  }
 
53
 
 
54
  protected function buildUncommittedStatus() {
 
55
    return $this->getSVNStatus();
 
56
  }
 
57
 
 
58
  public function getSVNBaseRevisions() {
 
59
    if ($this->svnBaseRevisions === null) {
 
60
      $this->getSVNStatus();
 
61
    }
 
62
    return $this->svnBaseRevisions;
 
63
  }
 
64
 
 
65
  public function limitStatusToPaths(array $paths) {
 
66
    $this->statusPaths = $paths;
 
67
    return $this;
 
68
  }
 
69
 
 
70
  public function getSVNStatus($with_externals = false) {
 
71
    if ($this->svnStatus === null) {
 
72
      if ($this->statusPaths) {
 
73
        list($status) = $this->execxLocal(
 
74
          '--xml status %Ls',
 
75
          $this->statusPaths);
 
76
      } else {
 
77
        list($status) = $this->execxLocal('--xml status');
 
78
      }
 
79
      $xml = new SimpleXMLElement($status);
 
80
 
 
81
      $externals = array();
 
82
      $files = array();
 
83
 
 
84
      foreach ($xml->target as $target) {
 
85
        $this->svnBaseRevisions = array();
 
86
        foreach ($target->entry as $entry) {
 
87
          $path = (string)$entry['path'];
 
88
          // On Windows, we get paths with backslash directory separators here.
 
89
          // Normalize them to the format everything else expects and generates.
 
90
          if (phutil_is_windows()) {
 
91
            $path = str_replace(DIRECTORY_SEPARATOR, '/', $path);
 
92
          }
 
93
          $mask = 0;
 
94
 
 
95
          $props = (string)($entry->{'wc-status'}[0]['props']);
 
96
          $item  = (string)($entry->{'wc-status'}[0]['item']);
 
97
 
 
98
          $base = (string)($entry->{'wc-status'}[0]['revision']);
 
99
          $this->svnBaseRevisions[$path] = $base;
 
100
 
 
101
          switch ($props) {
 
102
            case 'none':
 
103
            case 'normal':
 
104
              break;
 
105
            case 'modified':
 
106
              $mask |= self::FLAG_MODIFIED;
 
107
              break;
 
108
            default:
 
109
              throw new Exception("Unrecognized property status '{$props}'.");
 
110
          }
 
111
 
 
112
          $mask |= $this->parseSVNStatus($item);
 
113
          if ($item == 'external') {
 
114
            $externals[] = $path;
 
115
          }
 
116
 
 
117
          // This is new in or around Subversion 1.6.
 
118
          $tree_conflicts = ($entry->{'wc-status'}[0]['tree-conflicted']);
 
119
          if ((string)$tree_conflicts) {
 
120
            $mask |= self::FLAG_CONFLICT;
 
121
          }
 
122
 
 
123
          $files[$path] = $mask;
 
124
        }
 
125
      }
 
126
 
 
127
      foreach ($files as $path => $mask) {
 
128
        foreach ($externals as $external) {
 
129
          if (!strncmp($path.'/', $external.'/', strlen($external) + 1)) {
 
130
            $files[$path] |= self::FLAG_EXTERNALS;
 
131
          }
 
132
        }
 
133
      }
 
134
 
 
135
      $this->svnStatus = $files;
 
136
    }
 
137
 
 
138
    $status = $this->svnStatus;
 
139
    if (!$with_externals) {
 
140
      foreach ($status as $path => $mask) {
 
141
        if ($mask & ArcanistRepositoryAPI::FLAG_EXTERNALS) {
 
142
          unset($status[$path]);
 
143
        }
 
144
      }
 
145
    }
 
146
 
 
147
    return $status;
 
148
  }
 
149
 
 
150
  private function parseSVNStatus($item) {
 
151
    switch ($item) {
 
152
      case 'none':
 
153
        // We can get 'none' for property changes on a directory.
 
154
      case 'normal':
 
155
        return 0;
 
156
      case 'external':
 
157
        return self::FLAG_EXTERNALS;
 
158
      case 'unversioned':
 
159
        return self::FLAG_UNTRACKED;
 
160
      case 'obstructed':
 
161
        return self::FLAG_OBSTRUCTED;
 
162
      case 'missing':
 
163
        return self::FLAG_MISSING;
 
164
      case 'added':
 
165
        return self::FLAG_ADDED;
 
166
      case 'replaced':
 
167
        // This is the result of "svn rm"-ing a file, putting another one
 
168
        // in place of it, and then "svn add"-ing the new file. Just treat
 
169
        // this as equivalent to "modified".
 
170
        return self::FLAG_MODIFIED;
 
171
      case 'modified':
 
172
        return self::FLAG_MODIFIED;
 
173
      case 'deleted':
 
174
        return self::FLAG_DELETED;
 
175
      case 'conflicted':
 
176
        return self::FLAG_CONFLICT;
 
177
      case 'incomplete':
 
178
        return self::FLAG_INCOMPLETE;
 
179
      default:
 
180
        throw new Exception("Unrecognized item status '{$item}'.");
 
181
    }
 
182
  }
 
183
 
 
184
  public function addToCommit(array $paths) {
 
185
    $add = array_filter($paths, 'Filesystem::pathExists');
 
186
    if ($add) {
 
187
      $this->execxLocal(
 
188
        'add -- %Ls',
 
189
        $add);
 
190
    }
 
191
    if ($add != $paths) {
 
192
      $this->execxLocal(
 
193
        'delete -- %Ls',
 
194
        array_diff($paths, $add));
 
195
    }
 
196
    $this->svnStatus = null;
 
197
  }
 
198
 
 
199
  public function getSVNProperty($path, $property) {
 
200
    list($stdout) = execx(
 
201
      'svn propget %s %s@',
 
202
      $property,
 
203
      $this->getPath($path));
 
204
    return trim($stdout);
 
205
  }
 
206
 
 
207
  public function getSourceControlPath() {
 
208
    return idx($this->getSVNInfo('/'), 'URL');
 
209
  }
 
210
 
 
211
  public function getSourceControlBaseRevision() {
 
212
    $info = $this->getSVNInfo('/');
 
213
    return $info['URL'].'@'.$this->getSVNBaseRevisionNumber();
 
214
  }
 
215
 
 
216
  public function getCanonicalRevisionName($string) {
 
217
    // TODO: This could be more accurate, but is only used by `arc browse`
 
218
    // for now.
 
219
 
 
220
    if (is_numeric($string)) {
 
221
      return $string;
 
222
    }
 
223
    return null;
 
224
  }
 
225
 
 
226
  public function getSVNBaseRevisionNumber() {
 
227
    if ($this->svnBaseRevisionNumber) {
 
228
      return $this->svnBaseRevisionNumber;
 
229
    }
 
230
    $info = $this->getSVNInfo('/');
 
231
    return $info['Revision'];
 
232
  }
 
233
 
 
234
  public function overrideSVNBaseRevisionNumber($effective_base_revision) {
 
235
    $this->svnBaseRevisionNumber = $effective_base_revision;
 
236
    return $this;
 
237
  }
 
238
 
 
239
  public function getBranchName() {
 
240
    $info = $this->getSVNInfo('/');
 
241
    $repo_root = idx($info, 'Repository Root');
 
242
    $repo_root_length = strlen($repo_root);
 
243
    $url = idx($info, 'URL');
 
244
    if (substr($url, 0, $repo_root_length) == $repo_root) {
 
245
      return substr($url, $repo_root_length);
 
246
    }
 
247
    return 'svn';
 
248
  }
 
249
 
 
250
  public function getRemoteURI() {
 
251
    return idx($this->getSVNInfo('/'), 'Repository Root');
 
252
  }
 
253
 
 
254
  public function buildInfoFuture($path) {
 
255
    if ($path == '/') {
 
256
      // When the root of a working copy is referenced by a symlink and you
 
257
      // execute 'svn info' on that symlink, svn fails. This is a longstanding
 
258
      // bug in svn:
 
259
      //
 
260
      // See http://subversion.tigris.org/issues/show_bug.cgi?id=2305
 
261
      //
 
262
      // To reproduce, do:
 
263
      //
 
264
      //  $ ln -s working_copy working_link
 
265
      //  $ svn info working_copy # ok
 
266
      //  $ svn info working_link # fails
 
267
      //
 
268
      // Work around this by cd-ing into the directory before executing
 
269
      // 'svn info'.
 
270
      return $this->buildLocalFuture(array('info .'));
 
271
    } else {
 
272
      // Note: here and elsewhere we need to append "@" to the path because if
 
273
      // a file has a literal "@" in it, everything after that will be
 
274
      // interpreted as a revision. By appending "@" with no argument, SVN
 
275
      // parses it properly.
 
276
      return $this->buildLocalFuture(array('info %s@', $this->getPath($path)));
 
277
    }
 
278
  }
 
279
 
 
280
  public function buildDiffFuture($path) {
 
281
    $root = phutil_get_library_root('arcanist');
 
282
 
 
283
    // The "--depth empty" flag prevents us from picking up changes in
 
284
    // children when we run 'diff' against a directory. Specifically, when a
 
285
    // user has added or modified some directory "example/", we want to return
 
286
    // ONLY changes to that directory when given it as a path. If we run
 
287
    // without "--depth empty", svn will give us changes to the directory
 
288
    // itself (such as property changes) and also give us changes to any
 
289
    // files within the directory (basically, implicit recursion). We don't
 
290
    // want that, so prevent recursive diffing. This flag does not work if the
 
291
    // directory is newly added (see T5555) so we need to filter the results
 
292
    // out later as well.
 
293
 
 
294
    if (phutil_is_windows()) {
 
295
      // TODO: Provide a binary_safe_diff script for Windows.
 
296
      // TODO: Provide a diff command which can take lines of context somehow.
 
297
      return $this->buildLocalFuture(
 
298
        array(
 
299
          'diff --depth empty %s',
 
300
          $path,
 
301
        ));
 
302
    } else {
 
303
      $diff_bin = $root.'/../scripts/repository/binary_safe_diff.sh';
 
304
      $diff_cmd = Filesystem::resolvePath($diff_bin);
 
305
      return $this->buildLocalFuture(
 
306
        array(
 
307
          'diff --depth empty --diff-cmd %s -x -U%d %s',
 
308
          $diff_cmd,
 
309
          $this->getDiffLinesOfContext(),
 
310
          $path,
 
311
        ));
 
312
    }
 
313
  }
 
314
 
 
315
  public function primeSVNInfoResult($path, $result) {
 
316
    $this->svnInfoRaw[$path] = $result;
 
317
    return $this;
 
318
  }
 
319
 
 
320
  public function primeSVNDiffResult($path, $result) {
 
321
    $this->svnDiffRaw[$path] = $result;
 
322
    return $this;
 
323
  }
 
324
 
 
325
  public function getSVNInfo($path) {
 
326
    if (empty($this->svnInfo[$path])) {
 
327
 
 
328
      if (empty($this->svnInfoRaw[$path])) {
 
329
        $this->svnInfoRaw[$path] = $this->buildInfoFuture($path)->resolve();
 
330
      }
 
331
 
 
332
      list($err, $stdout) = $this->svnInfoRaw[$path];
 
333
      if ($err) {
 
334
        throw new Exception(
 
335
          "Error #{$err} executing svn info against '{$path}'.");
 
336
      }
 
337
 
 
338
      // TODO: Hack for Windows.
 
339
      $stdout = str_replace("\r\n", "\n", $stdout);
 
340
 
 
341
      $patterns = array(
 
342
        '/^(URL): (\S+)$/m',
 
343
        '/^(Revision): (\d+)$/m',
 
344
        '/^(Last Changed Author): (\S+)$/m',
 
345
        '/^(Last Changed Rev): (\d+)$/m',
 
346
        '/^(Last Changed Date): (.+) \(.+\)$/m',
 
347
        '/^(Copied From URL): (\S+)$/m',
 
348
        '/^(Copied From Rev): (\d+)$/m',
 
349
        '/^(Repository Root): (\S+)$/m',
 
350
        '/^(Repository UUID): (\S+)$/m',
 
351
        '/^(Node Kind): (\S+)$/m',
 
352
      );
 
353
 
 
354
      $result = array();
 
355
      foreach ($patterns as $pattern) {
 
356
        $matches = null;
 
357
        if (preg_match($pattern, $stdout, $matches)) {
 
358
          $result[$matches[1]] = $matches[2];
 
359
        }
 
360
      }
 
361
 
 
362
      if (isset($result['Last Changed Date'])) {
 
363
        $result['Last Changed Date'] = strtotime($result['Last Changed Date']);
 
364
      }
 
365
 
 
366
      if (empty($result)) {
 
367
        throw new Exception('Unable to parse SVN info.');
 
368
      }
 
369
 
 
370
      $this->svnInfo[$path] = $result;
 
371
    }
 
372
 
 
373
    return $this->svnInfo[$path];
 
374
  }
 
375
 
 
376
 
 
377
  public function getRawDiffText($path) {
 
378
    $status = $this->getSVNStatus();
 
379
    if (!isset($status[$path])) {
 
380
      return null;
 
381
    }
 
382
 
 
383
    $status = $status[$path];
 
384
 
 
385
    // Build meaningful diff text for "svn copy" operations.
 
386
    if ($status & ArcanistRepositoryAPI::FLAG_ADDED) {
 
387
      $info = $this->getSVNInfo($path);
 
388
      if (!empty($info['Copied From URL'])) {
 
389
        return $this->buildSyntheticAdditionDiff(
 
390
          $path,
 
391
          $info['Copied From URL'],
 
392
          $info['Copied From Rev']);
 
393
      }
 
394
    }
 
395
 
 
396
    // If we run "diff" on a binary file which doesn't have the "svn:mime-type"
 
397
    // of "application/octet-stream", `diff' will explode in a rain of
 
398
    // unhelpful hellfire as it tries to build a textual diff of the two
 
399
    // files. We just fix this inline since it's pretty unambiguous.
 
400
    // TODO: Move this to configuration?
 
401
    $matches = null;
 
402
    if (preg_match('/\.(gif|png|jpe?g|swf|pdf|ico)$/i', $path, $matches)) {
 
403
      // Check if the file is deleted first; SVN will complain if we try to
 
404
      // get properties of a deleted file.
 
405
      if ($status & ArcanistRepositoryAPI::FLAG_DELETED) {
 
406
        return <<<EODIFF
 
407
Index: {$path}
 
408
===================================================================
 
409
Cannot display: file marked as a binary type.
 
410
svn:mime-type = application/octet-stream
 
411
 
 
412
EODIFF;
 
413
      }
 
414
 
 
415
      $mime = $this->getSVNProperty($path, 'svn:mime-type');
 
416
      if ($mime != 'application/octet-stream') {
 
417
        execx(
 
418
          'svn propset svn:mime-type application/octet-stream %s',
 
419
          self::escapeFileNameForSVN($this->getPath($path)));
 
420
      }
 
421
    }
 
422
 
 
423
    if (empty($this->svnDiffRaw[$path])) {
 
424
      $this->svnDiffRaw[$path] = $this->buildDiffFuture($path)->resolve();
 
425
    }
 
426
 
 
427
    list($err, $stdout, $stderr) = $this->svnDiffRaw[$path];
 
428
 
 
429
    // Note: GNU Diff returns 2 when SVN hands it binary files to diff and they
 
430
    // differ. This is not an error; it is documented behavior. But SVN isn't
 
431
    // happy about it. SVN will exit with code 1 and return the string below.
 
432
    if ($err != 0 && $stderr !== "svn: 'diff' returned 2\n") {
 
433
      throw new Exception(
 
434
        "svn diff returned unexpected error code: $err\n".
 
435
        "stdout: $stdout\n".
 
436
        "stderr: $stderr");
 
437
    }
 
438
 
 
439
    if ($err == 0 && empty($stdout)) {
 
440
      // If there are no changes, 'diff' exits with no output, but that means
 
441
      // we can not distinguish between empty and unmodified files. Build a
 
442
      // synthetic "diff" without any changes in it.
 
443
      return $this->buildSyntheticUnchangedDiff($path);
 
444
    }
 
445
 
 
446
    return $stdout;
 
447
  }
 
448
 
 
449
  protected function buildSyntheticAdditionDiff($path, $source, $rev) {
 
450
    $type = $this->getSVNProperty($path, 'svn:mime-type');
 
451
    if ($type == 'application/octet-stream') {
 
452
      return <<<EODIFF
 
453
Index: {$path}
 
454
===================================================================
 
455
Cannot display: file marked as a binary type.
 
456
svn:mime-type = application/octet-stream
 
457
 
 
458
EODIFF;
 
459
    }
 
460
 
 
461
    if (is_dir($this->getPath($path))) {
 
462
      return null;
 
463
    }
 
464
 
 
465
    $data = Filesystem::readFile($this->getPath($path));
 
466
    list($orig) = execx('svn cat %s@%s', $source, $rev);
 
467
 
 
468
    $src = new TempFile();
 
469
    $dst = new TempFile();
 
470
    Filesystem::writeFile($src, $orig);
 
471
    Filesystem::writeFile($dst, $data);
 
472
 
 
473
    list($err, $diff) = exec_manual(
 
474
      'diff -L a/%s -L b/%s -U%d %s %s',
 
475
      str_replace($this->getSourceControlPath().'/', '', $source),
 
476
      $path,
 
477
      $this->getDiffLinesOfContext(),
 
478
      $src,
 
479
      $dst);
 
480
 
 
481
    if ($err == 1) { // 1 means there are differences.
 
482
      return <<<EODIFF
 
483
Index: {$path}
 
484
===================================================================
 
485
{$diff}
 
486
 
 
487
EODIFF;
 
488
    } else {
 
489
      return $this->buildSyntheticUnchangedDiff($path);
 
490
    }
 
491
  }
 
492
 
 
493
  protected function buildSyntheticUnchangedDiff($path) {
 
494
    $full_path = $this->getPath($path);
 
495
    if (is_dir($full_path)) {
 
496
      return null;
 
497
    }
 
498
 
 
499
    if (!file_exists($full_path)) {
 
500
      return null;
 
501
    }
 
502
 
 
503
    $data = Filesystem::readFile($full_path);
 
504
    $lines = explode("\n", $data);
 
505
    $len = count($lines);
 
506
    foreach ($lines as $key => $line) {
 
507
      $lines[$key] = ' '.$line;
 
508
    }
 
509
    $lines = implode("\n", $lines);
 
510
    return <<<EODIFF
 
511
Index: {$path}
 
512
===================================================================
 
513
--- {$path} (synthetic)
 
514
+++ {$path} (synthetic)
 
515
@@ -1,{$len} +1,{$len} @@
 
516
{$lines}
 
517
 
 
518
EODIFF;
 
519
  }
 
520
 
 
521
  public function getAllFiles() {
 
522
    // TODO: Handle paths with newlines.
 
523
    $future = $this->buildLocalFuture(array('list -R'));
 
524
    return new PhutilCallbackFilterIterator(
 
525
      new LinesOfALargeExecFuture($future),
 
526
      array($this, 'filterFiles'));
 
527
  }
 
528
 
 
529
  public function getChangedFiles($since_commit) {
 
530
    $url = '';
 
531
    $match = null;
 
532
    if (preg_match('/(.*)@(.*)/', $since_commit, $match)) {
 
533
      list(, $url, $since_commit) = $match;
 
534
    }
 
535
    // TODO: Handle paths with newlines.
 
536
    list($stdout) = $this->execxLocal(
 
537
      '--xml diff --revision %s:HEAD --summarize %s',
 
538
      $since_commit,
 
539
      $url);
 
540
    $xml = new SimpleXMLElement($stdout);
 
541
 
 
542
    $return = array();
 
543
    foreach ($xml->paths[0]->path as $path) {
 
544
      $return[(string)$path] = $this->parseSVNStatus($path['item']);
 
545
    }
 
546
    return $return;
 
547
  }
 
548
 
 
549
  public function filterFiles($path) {
 
550
    // NOTE: SVN uses '/' also on Windows.
 
551
    if ($path == '' || substr($path, -1) == '/') {
 
552
      return null;
 
553
    }
 
554
    return $path;
 
555
  }
 
556
 
 
557
  public function getBlame($path) {
 
558
    $blame = array();
 
559
 
 
560
    list($stdout) = $this->execxLocal('blame %s', $path);
 
561
 
 
562
    $stdout = trim($stdout);
 
563
    if (!strlen($stdout)) {
 
564
      // Empty file.
 
565
      return $blame;
 
566
    }
 
567
 
 
568
    foreach (explode("\n", $stdout) as $line) {
 
569
      $m = array();
 
570
      if (!preg_match('/^\s*(\d+)\s+(\S+)/', $line, $m)) {
 
571
        throw new Exception("Bad blame? `{$line}'");
 
572
      }
 
573
      $revision = $m[1];
 
574
      $author = $m[2];
 
575
      $blame[] = array($author, $revision);
 
576
    }
 
577
 
 
578
    return $blame;
 
579
  }
 
580
 
 
581
  public function getOriginalFileData($path) {
 
582
    // SVN issues warnings for nonexistent paths, directories, etc., but still
 
583
    // returns no error code. However, for new paths in the working copy it
 
584
    // fails. Assume that failure means the original file does not exist.
 
585
    list($err, $stdout) = $this->execManualLocal('cat %s@', $path);
 
586
    if ($err) {
 
587
      return null;
 
588
    }
 
589
    return $stdout;
 
590
  }
 
591
 
 
592
  public function getCurrentFileData($path) {
 
593
    $full_path = $this->getPath($path);
 
594
    if (Filesystem::pathExists($full_path)) {
 
595
      return Filesystem::readFile($full_path);
 
596
    }
 
597
    return null;
 
598
  }
 
599
 
 
600
  public function getRepositoryUUID() {
 
601
    $info = $this->getSVNInfo('/');
 
602
    return $info['Repository UUID'];
 
603
  }
 
604
 
 
605
  public function getLocalCommitInformation() {
 
606
    return null;
 
607
  }
 
608
 
 
609
  public function isHistoryDefaultImmutable() {
 
610
    return true;
 
611
  }
 
612
 
 
613
  public function supportsAmend() {
 
614
    return false;
 
615
  }
 
616
 
 
617
  public function supportsCommitRanges() {
 
618
    return false;
 
619
  }
 
620
 
 
621
  public function supportsLocalCommits() {
 
622
    return false;
 
623
  }
 
624
 
 
625
  public function hasLocalCommit($commit) {
 
626
    return false;
 
627
  }
 
628
 
 
629
  public function getWorkingCopyRevision() {
 
630
    return $this->getSourceControlBaseRevision();
 
631
  }
 
632
 
 
633
  public function supportsLocalBranchMerge() {
 
634
    return false;
 
635
  }
 
636
 
 
637
  public function getFinalizedRevisionMessage() {
 
638
    // In other VCSes we give push instructions here, but it never makes sense
 
639
    // in SVN.
 
640
    return 'Done.';
 
641
  }
 
642
 
 
643
  public function loadWorkingCopyDifferentialRevisions(
 
644
    ConduitClient $conduit,
 
645
    array $query) {
 
646
 
 
647
    // We don't have much to go on in SVN, look for revisions that came from
 
648
    // this directory and belong to the same project.
 
649
 
 
650
    $project = $this->getWorkingCopyIdentity()->getProjectID();
 
651
    if (!$project) {
 
652
      return array();
 
653
    }
 
654
 
 
655
    $results = $conduit->callMethodSynchronous(
 
656
      'differential.query',
 
657
      $query + array(
 
658
        'arcanistProjects' => array($project),
 
659
      ));
 
660
 
 
661
    foreach ($results as $key => $result) {
 
662
      if ($result['sourcePath'] != $this->getPath()) {
 
663
        unset($results[$key]);
 
664
      }
 
665
    }
 
666
 
 
667
    foreach ($results as $key => $result) {
 
668
      $results[$key]['why'] =
 
669
        'Matching arcanist project name and working copy directory path.';
 
670
    }
 
671
 
 
672
    return $results;
 
673
  }
 
674
 
 
675
  public function updateWorkingCopy() {
 
676
    $this->execxLocal('up');
 
677
  }
 
678
 
 
679
  public static function escapeFileNamesForSVN(array $files) {
 
680
    foreach ($files as $k => $file) {
 
681
      $files[$k] = self::escapeFileNameForSVN($file);
 
682
    }
 
683
    return $files;
 
684
  }
 
685
 
 
686
  public static function escapeFileNameForSVN($file) {
 
687
    // SVN interprets "x@1" as meaning "file x at revision 1", which is not
 
688
    // intended for files named "sprite@2x.png" or similar. For files with an
 
689
    // "@" in their names, escape them by adding "@" at the end, which SVN
 
690
    // interprets as "at the working copy revision". There is a special case
 
691
    // where ".@" means "fail with an error" instead of ". at the working copy
 
692
    // revision", so avoid escaping "." into ".@".
 
693
 
 
694
    if (strpos($file, '@') !== false) {
 
695
      $file = $file.'@';
 
696
    }
 
697
 
 
698
    return $file;
 
699
  }
 
700
 
 
701
}