@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3/**
4 * Manages execution of `git pull` and `hg pull` commands for
5 * @{class:PhabricatorRepository} objects. Used by
6 * @{class:PhabricatorRepositoryPullLocalDaemon}.
7 *
8 * This class also covers initial working copy setup through `git clone`,
9 * `git init`, `hg clone`, `hg init`, or `svnadmin create`.
10 *
11 * @task pull Pulling Working Copies
12 * @task git Pulling Git Working Copies
13 * @task hg Pulling Mercurial Working Copies
14 * @task svn Pulling Subversion Working Copies
15 * @task internal Internals
16 */
17final class PhabricatorRepositoryPullEngine
18 extends PhabricatorRepositoryEngine {
19
20
21/* -( Pulling Working Copies )--------------------------------------------- */
22
23
24 public function pullRepository() {
25 $repository = $this->getRepository();
26
27 $lock = $this->newRepositoryLock($repository, 'repo.pull', true);
28
29 try {
30 $lock->lock();
31 } catch (PhutilLockException $ex) {
32 throw new DiffusionDaemonLockException(
33 pht(
34 'Another process is currently updating repository "%s", '.
35 'skipping pull.',
36 $repository->getDisplayName()));
37 }
38
39 try {
40 $result = $this->pullRepositoryWithLock();
41 } catch (Exception $ex) {
42 $lock->unlock();
43 throw $ex;
44 }
45
46 $lock->unlock();
47
48 return $result;
49 }
50
51 private function pullRepositoryWithLock() {
52 $repository = $this->getRepository();
53 $viewer = PhabricatorUser::getOmnipotentUser();
54
55 if ($repository->isReadOnly()) {
56 $this->skipPull(
57 pht(
58 "Skipping pull on read-only repository.\n\n%s",
59 $repository->getReadOnlyMessageForDisplay()));
60 }
61
62 $is_hg = false;
63 $is_git = false;
64 $is_svn = false;
65
66 $vcs = $repository->getVersionControlSystem();
67
68 switch ($vcs) {
69 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
70 // We never pull a local copy of non-hosted Subversion repositories.
71 if (!$repository->isHosted()) {
72 $this->skipPull(
73 pht(
74 'Repository "%s" is a non-hosted Subversion repository, which '.
75 'does not require a local working copy to be pulled.',
76 $repository->getDisplayName()));
77 return;
78 }
79 $is_svn = true;
80 break;
81 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
82 $is_git = true;
83 break;
84 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
85 $is_hg = true;
86 break;
87 default:
88 $this->abortPull(pht('Unknown VCS "%s"!', $vcs));
89 break;
90 }
91
92 $local_path = $repository->getLocalPath();
93 if ($local_path === null) {
94 $this->abortPull(
95 pht(
96 'No local path is configured for repository "%s".',
97 $repository->getDisplayName()));
98 }
99
100 try {
101 $dirname = dirname($local_path);
102 if (!Filesystem::pathExists($dirname)) {
103 Filesystem::createDirectory($dirname, 0755, $recursive = true);
104 }
105
106 if (!Filesystem::pathExists($local_path)) {
107 $this->logPull(
108 pht(
109 'Creating a new working copy for repository "%s".',
110 $repository->getDisplayName()));
111 if ($is_git) {
112 $this->executeGitCreate();
113 } else if ($is_hg) {
114 $this->executeMercurialCreate();
115 } else {
116 $this->executeSubversionCreate();
117 }
118 }
119
120 id(new DiffusionRepositoryClusterEngine())
121 ->setViewer($viewer)
122 ->setRepository($repository)
123 ->synchronizeWorkingCopyBeforeRead();
124
125 if (!$repository->isHosted()) {
126 $this->logPull(
127 pht(
128 'Updating the working copy for repository "%s".',
129 $repository->getDisplayName()));
130
131 if ($is_git) {
132 $this->executeGitUpdate();
133 } else if ($is_hg) {
134 $this->executeMercurialUpdate();
135 }
136 }
137
138 if ($repository->isHosted()) {
139 if ($is_git) {
140 $this->installGitHook();
141 } else if ($is_svn) {
142 $this->installSubversionHook();
143 } else if ($is_hg) {
144 $this->installMercurialHook();
145 }
146
147 foreach ($repository->getHookDirectories() as $directory) {
148 $this->installHookDirectory($directory);
149 }
150 }
151
152 if ($is_git) {
153 $this->updateGitWorkingCopyConfiguration();
154 }
155
156 } catch (Exception $ex) {
157 $this->abortPull(
158 pht(
159 "Pull of '%s' failed: %s",
160 $repository->getDisplayName(),
161 $ex->getMessage()),
162 $ex);
163 }
164
165 $this->donePull();
166
167 return $this;
168 }
169
170 private function skipPull($message) {
171 $this->log($message);
172 $this->donePull();
173 }
174
175 private function abortPull($message, ?Exception $ex = null) {
176 $code_error = PhabricatorRepositoryStatusMessage::CODE_ERROR;
177 $this->updateRepositoryInitStatus($code_error, $message);
178 if ($ex) {
179 throw $ex;
180 } else {
181 throw new Exception($message);
182 }
183 }
184
185 private function logPull($message) {
186 $this->log($message);
187 }
188
189 private function donePull() {
190 $code_okay = PhabricatorRepositoryStatusMessage::CODE_OKAY;
191 $this->updateRepositoryInitStatus($code_okay);
192 }
193
194 private function updateRepositoryInitStatus($code, $message = null) {
195 $this->getRepository()->writeStatusMessage(
196 PhabricatorRepositoryStatusMessage::TYPE_INIT,
197 $code,
198 array(
199 'message' => $message,
200 ));
201 }
202
203 private function installHook($path, array $hook_argv = array()) {
204 $this->log(pht('Installing commit hook to "%s"...', $path));
205
206 $repository = $this->getRepository();
207 $identifier = $this->getHookContextIdentifier($repository);
208
209 $root = dirname(phutil_get_library_root('phabricator'));
210 $bin = $root.'/bin/commit-hook';
211
212 $full_php_path = Filesystem::resolveBinary('php');
213 $cmd = csprintf(
214 'exec %s -f %s -- %s %Ls "$@"',
215 $full_php_path,
216 $bin,
217 $identifier,
218 $hook_argv);
219
220 $hook = "#!/bin/sh\nexport TERM=dumb\n{$cmd}\n";
221
222 Filesystem::writeFile($path, $hook);
223 Filesystem::changePermissions($path, 0755);
224 }
225
226 private function installHookDirectory($path) {
227 $readme = pht(
228 "To add custom hook scripts to this repository, add them to this ".
229 "directory.\n\n%s will run any executables in this directory ".
230 "after running its own checks, as though they were normal hook ".
231 "scripts.",
232 PlatformSymbols::getPlatformServerName());
233
234 Filesystem::createDirectory($path, 0755);
235 Filesystem::writeFile($path.'/README', $readme);
236 }
237
238 private function getHookContextIdentifier(PhabricatorRepository $repository) {
239 $identifier = $repository->getPHID();
240
241 $instance = PhabricatorEnv::getEnvConfig('cluster.instance');
242 if (phutil_nonempty_string($instance)) {
243 $identifier = "{$identifier}:{$instance}";
244 }
245
246 return $identifier;
247 }
248
249
250/* -( Pulling Git Working Copies )----------------------------------------- */
251
252
253 /**
254 * @task git
255 */
256 private function executeGitCreate() {
257 $repository = $this->getRepository();
258
259 $path = rtrim($repository->getLocalPath(), '/');
260
261 // See T13448. In all cases, we create repositories by using "git init"
262 // to build a bare, empty working copy. If we try to use "git clone"
263 // instead, we'll pull in too many refs if "Fetch Refs" is also
264 // configured. There's no apparent way to make "git clone" behave narrowly
265 // and no apparent reason to bother.
266
267 $repository->execxRemoteCommand(
268 'init --bare -- %s',
269 $path);
270 }
271
272
273 /**
274 * @task git
275 */
276 private function executeGitUpdate() {
277 $repository = $this->getRepository();
278
279 // See T13479. We previously used "--show-toplevel", but this stopped
280 // working in Git 2.25.0 when run in a bare repository.
281
282 // NOTE: As of Git 2.21.1, "git rev-parse" can not parse "--" in its
283 // argument list, so we can not specify arguments unambiguously. Any
284 // version of Git which does not recognize the "--git-dir" flag will
285 // treat this as a request to parse the literal refname "--git-dir".
286
287 list($err, $stdout) = $repository->execLocalCommand(
288 'rev-parse --git-dir');
289
290 $repository_root = null;
291 $path = $repository->getLocalPath();
292
293 if (!$err) {
294 $repository_root = Filesystem::resolvePath(
295 rtrim($stdout, "\n"),
296 $path);
297
298 // If we're in a bare Git repository, the "--git-dir" will be the
299 // root directory. If we're in a working copy, the "--git-dir" will
300 // be the ".git/" directory.
301
302 // Test if the result is the root directory. If it is, we're in good
303 // shape and appear to be inside a bare repository. If not, take the
304 // parent directory to get out of the ".git/" folder.
305
306 if (!Filesystem::pathsAreEquivalent($repository_root, $path)) {
307 $repository_root = dirname($repository_root);
308 }
309 }
310
311 $message = null;
312 if ($err) {
313 // Try to raise a more tailored error message in the more common case
314 // of the user creating an empty directory. (We could try to remove it,
315 // but might not be able to, and it's much simpler to raise a good
316 // message than try to navigate those waters.)
317 if (is_dir($path)) {
318 $files = Filesystem::listDirectory($path, $include_hidden = true);
319 if (!$files) {
320 $message = pht(
321 'Expected to find a Git repository at "%s", but there is an '.
322 'empty directory there. Remove the directory. A daemon will '.
323 'construct the working copy for you.',
324 $path);
325 } else {
326 $message = pht(
327 'Expected to find a Git repository at "%s", but there is '.
328 'a non-repository directory (with other stuff in it) there. '.
329 'Move or remove this directory. A daemon will construct '.
330 'the working copy for you.',
331 $path);
332 }
333 } else if (is_file($path)) {
334 $message = pht(
335 'Expected to find a Git repository at "%s", but there is a '.
336 'file there instead. Move or remove this file. A daemon will '.
337 'construct the working copy for you.',
338 $path);
339 } else {
340 $message = pht(
341 'Expected to find a git repository at "%s", but did not.',
342 $path);
343 }
344 } else {
345
346 // Prior to Git 2.25.0, we used "--show-toplevel", which had a weird
347 // case here when the working copy was inside another working copy.
348 // The switch to "--git-dir" seems to have resolved this; we now seem
349 // to find the nearest git directory and thus the correct repository
350 // root.
351
352 if (!Filesystem::pathsAreEquivalent($repository_root, $path)) {
353 $err = true;
354 $message = pht(
355 'Expected to find a Git repository at "%s", but the actual Git '.
356 'repository root for this directory is "%s". Something is '.
357 'misconfigured. This directory should be writable by the daemons '.
358 'and not inside another Git repository.',
359 $path,
360 $repository_root);
361 }
362 }
363
364 if ($err && $repository->canDestroyWorkingCopy()) {
365 phlog(
366 pht(
367 "Repository working copy at '%s' failed sanity check; ".
368 "destroying and re-cloning. %s",
369 $path,
370 $message));
371 Filesystem::remove($path);
372 $this->executeGitCreate();
373 } else if ($err) {
374 throw new Exception($message);
375 }
376
377 // Load the refs we're planning to fetch from the remote repository.
378 $remote_refs = $this->loadGitRemoteRefs(
379 $repository,
380 $repository->getRemoteURIEnvelope(),
381 $is_local = false);
382
383 // Load the refs we're planning to fetch from the local repository, by
384 // using the local working copy path as the "remote" repository URI.
385 $local_refs = $this->loadGitRemoteRefs(
386 $repository,
387 new PhutilOpaqueEnvelope($path),
388 $is_local = true);
389
390 // See T13448. The "git fetch --prune ..." flag only prunes local refs
391 // matching the refspecs we pass it. If "Fetch Refs" is configured, we'll
392 // pass it a very narrow list of refspecs, and it won't prune older refs
393 // that aren't currently subject to fetching.
394
395 // Since we want to prune everything that isn't (a) on the fetch list and
396 // (b) in the remote, handle pruning of any surplus leftover refs ourselves
397 // before we fetch anything.
398
399 // (We don't have to do this if "Fetch Refs" isn't set up, since "--prune"
400 // will work in that case, but it's a little simpler to always go down the
401 // same code path.)
402
403 $surplus_refs = array();
404 foreach ($local_refs as $local_ref => $local_hash) {
405 $remote_hash = idx($remote_refs, $local_ref);
406 if ($remote_hash === null) {
407 $surplus_refs[] = $local_ref;
408 }
409 }
410
411 if ($surplus_refs) {
412 $this->log(
413 pht(
414 'Found %s surplus local ref(s) to delete.',
415 phutil_count($surplus_refs)));
416 foreach ($surplus_refs as $surplus_ref) {
417 $this->log(
418 pht(
419 'Deleting surplus local ref "%s" ("%s").',
420 $surplus_ref,
421 $local_refs[$surplus_ref]));
422
423 $repository->execLocalCommand(
424 'update-ref -d %R --',
425 $surplus_ref);
426
427 unset($local_refs[$surplus_ref]);
428 }
429 }
430
431 if ($remote_refs === $local_refs) {
432 $this->log(
433 pht(
434 'Skipping fetch because local and remote refs are already '.
435 'identical.'));
436 return false;
437 }
438
439 $this->logRefDifferences($remote_refs, $local_refs);
440
441 $fetch_rules = $this->getGitFetchRules($repository);
442
443 // For very old non-bare working copies, we need to use "--update-head-ok"
444 // to tell Git that it is allowed to overwrite whatever is currently
445 // checked out. See T13280.
446
447 $future = $repository->getRemoteCommandFuture(
448 'fetch --no-tags --update-head-ok -- %P %Ls',
449 $repository->getRemoteURIEnvelope(),
450 $fetch_rules);
451
452 $future
453 ->setCWD($path)
454 ->resolvex();
455 }
456
457 private function getGitRefRules(PhabricatorRepository $repository) {
458 $ref_rules = $repository->getFetchRules();
459
460 if (!$ref_rules) {
461 $ref_rules = array(
462 'refs/*',
463 );
464 }
465
466 return $ref_rules;
467 }
468
469 private function getGitFetchRules(PhabricatorRepository $repository) {
470 $ref_rules = $this->getGitRefRules($repository);
471
472 // Rewrite each ref rule "X" into "+X:X".
473
474 // The "X" means "fetch ref X".
475 // The "...:X" means "...and copy it into local ref X".
476 // The "+..." means "...and overwrite the local ref if it already exists".
477
478 $fetch_rules = array();
479 foreach ($ref_rules as $key => $ref_rule) {
480 $fetch_rules[] = sprintf(
481 '+%s:%s',
482 $ref_rule,
483 $ref_rule);
484 }
485
486 return $fetch_rules;
487 }
488
489 /**
490 * @task git
491 */
492 private function installGitHook() {
493 $repository = $this->getRepository();
494 $root = $repository->getLocalPath();
495
496 if ($repository->isWorkingCopyBare()) {
497 $path = '/hooks/pre-receive';
498 } else {
499 $path = '/.git/hooks/pre-receive';
500 }
501
502 $this->installHook($root.$path);
503 }
504
505 private function updateGitWorkingCopyConfiguration() {
506 $repository = $this->getRepository();
507
508 // See T5963. When you "git clone" from a remote with no "master", the
509 // client warns you that it isn't sure what it should check out as an
510 // initial state:
511
512 // warning: remote HEAD refers to nonexistent ref, unable to checkout
513
514 // We can tell the client what it should check out by making "HEAD"
515 // point somewhere. However:
516 //
517 // (1) If we don't set "receive.denyDeleteCurrent" to "ignore" and a user
518 // tries to delete the default branch, Git raises an error and refuses.
519 // We want to allow this; we already have sufficient protections around
520 // dangerous changes and do not need to special case the default branch.
521 //
522 // (2) A repository may have a nonexistent default branch configured.
523 // For now, we just respect configuration. This will raise a warning when
524 // users clone the repository.
525 //
526 // In any case, these changes are both advisory, so ignore any errors we
527 // may encounter.
528
529 // We do this for both hosted and observed repositories. Although it is
530 // not terribly common to clone from Phabricator's copy of an observed
531 // repository, it works fine and makes sense occasionally.
532
533 if ($repository->isWorkingCopyBare()) {
534 $repository->execLocalCommand(
535 'config -- receive.denyDeleteCurrent ignore');
536 $repository->execLocalCommand(
537 'symbolic-ref HEAD %s',
538 'refs/heads/'.$repository->getDefaultBranch());
539 }
540 }
541
542 private function loadGitRemoteRefs(
543 PhabricatorRepository $repository,
544 PhutilOpaqueEnvelope $remote_envelope,
545 $is_local) {
546
547 // See T13448. When listing local remotes, we want to list everything,
548 // not just refs we expect to fetch. This allows us to detect that we have
549 // undesirable refs (which have been deleted in the remote, but are still
550 // present locally) so we can update our state to reflect the correct
551 // remote state.
552
553 if ($is_local) {
554 $ref_rules = array();
555 } else {
556 $ref_rules = $this->getGitRefRules($repository);
557
558 // NOTE: "git ls-remote" does not support "--" until circa January 2016.
559 // See T12416. None of the flags to "ls-remote" appear dangerous, but
560 // refuse to list any refs beginning with "-" just in case.
561
562 foreach ($ref_rules as $ref_rule) {
563 if (preg_match('/^-/', $ref_rule)) {
564 throw new Exception(
565 pht(
566 'Refusing to list potentially dangerous ref ("%s") beginning '.
567 'with "-".',
568 $ref_rule));
569 }
570 }
571 }
572
573 list($stdout) = $repository->execxRemoteCommand(
574 'ls-remote %P %Ls',
575 $remote_envelope,
576 $ref_rules);
577
578 // Empty repositories don't have any refs.
579 if (!strlen(rtrim($stdout))) {
580 return array();
581 }
582
583 $map = array();
584 $lines = phutil_split_lines($stdout, false);
585 foreach ($lines as $line) {
586 list($hash, $name) = preg_split('/\s+/', $line, 2);
587
588 // If the remote has a HEAD, just ignore it.
589 if ($name == 'HEAD') {
590 continue;
591 }
592
593 // If the remote ref is itself a remote ref, ignore it.
594 if (preg_match('(^refs/remotes/)', $name)) {
595 continue;
596 }
597
598 $map[$name] = $hash;
599 }
600
601 ksort($map);
602
603 return $map;
604 }
605
606 private function logRefDifferences(array $remote, array $local) {
607 $all = $local + $remote;
608
609 $differences = array();
610 foreach ($all as $key => $ignored) {
611 $remote_ref = idx($remote, $key, pht('<null>'));
612 $local_ref = idx($local, $key, pht('<null>'));
613 if ($remote_ref !== $local_ref) {
614 $differences[] = pht(
615 '%s (remote: "%s", local: "%s")',
616 $key,
617 $remote_ref,
618 $local_ref);
619 }
620 }
621
622 $this->log(
623 pht(
624 "Updating repository after detecting ref differences:\n%s",
625 implode("\n", $differences)));
626 }
627
628
629
630/* -( Pulling Mercurial Working Copies )----------------------------------- */
631
632
633 /**
634 * @task hg
635 */
636 private function executeMercurialCreate() {
637 $repository = $this->getRepository();
638
639 $path = rtrim($repository->getLocalPath(), '/');
640
641 if ($repository->isHosted()) {
642 $repository->execxRemoteCommand(
643 'init -- %s',
644 $path);
645 } else {
646 $remote = $repository->getRemoteURIEnvelope();
647
648 // NOTE: Mercurial prior to 3.2.4 has an severe command injection
649 // vulnerability. See: <http://bit.ly/19B58E9>
650
651 // On vulnerable versions of Mercurial, we refuse to clone remotes which
652 // contain characters which may be interpreted by the shell.
653 $hg_binary = PhutilBinaryAnalyzer::getForBinary('hg');
654 $is_vulnerable = $hg_binary->isMercurialVulnerableToInjection();
655 if ($is_vulnerable) {
656 $cleartext = $remote->openEnvelope();
657 // The use of "%R" here is an attempt to limit collateral damage
658 // for normal URIs because it isn't clear how long this vulnerability
659 // has been around for.
660
661 $escaped = csprintf('%R', $cleartext);
662 if ((string)$escaped !== (string)$cleartext) {
663 throw new Exception(
664 pht(
665 'You have an old version of Mercurial (%s) which has a severe '.
666 'command injection security vulnerability. The remote URI for '.
667 'this repository (%s) is potentially unsafe. Upgrade Mercurial '.
668 'to at least 3.2.4 to clone it.',
669 $hg_binary->getBinaryVersion(),
670 $repository->getMonogram()));
671 }
672 }
673
674 try {
675 $repository->execxRemoteCommand(
676 'clone --noupdate -- %P %s',
677 $remote,
678 $path);
679 } catch (Exception $ex) {
680 $message = $ex->getMessage();
681 $message = $this->censorMercurialErrorMessage($message);
682 throw new Exception($message);
683 }
684 }
685 }
686
687
688 /**
689 * @task hg
690 */
691 private function executeMercurialUpdate() {
692 $repository = $this->getRepository();
693 $path = $repository->getLocalPath();
694
695 // This is a local command, but needs credentials.
696 $remote = $repository->getRemoteURIEnvelope();
697 $future = $repository->getRemoteCommandFuture('pull -- %P', $remote);
698 $future->setCWD($path);
699
700 try {
701 $future->resolvex();
702 } catch (CommandException $ex) {
703 $err = $ex->getError();
704 $stdout = $ex->getStdout();
705
706 // NOTE: Between versions 2.1 and 2.1.1, Mercurial changed the behavior
707 // of "hg pull" to return 1 in case of a successful pull with no changes.
708 // This behavior has been reverted, but users who updated between Feb 1,
709 // 2012 and Mar 1, 2012 will have the erroring version. Do a dumb test
710 // against stdout to check for this possibility.
711
712 // NOTE: Mercurial has translated versions, which translate this error
713 // string. In a translated version, the string will be something else,
714 // like "aucun changement trouve". There didn't seem to be an easy way
715 // to handle this (there are hard ways but this is not a common problem
716 // and only creates log spam, not application failures). Assume English.
717
718 // TODO: Remove this once we're far enough in the future that deployment
719 // of 2.1 is exceedingly rare?
720 if ($err == 1 && preg_match('/no changes found/', $stdout)) {
721 return;
722 } else {
723 $message = $ex->getMessage();
724 $message = $this->censorMercurialErrorMessage($message);
725 throw new Exception($message);
726 }
727 }
728 }
729
730
731 /**
732 * Censor response bodies from Mercurial error messages.
733 *
734 * When Mercurial attempts to clone an HTTP repository but does not
735 * receive a response it expects, it emits the response body in the
736 * command output.
737 *
738 * This represents a potential SSRF issue, because an attacker with
739 * permission to create repositories can create one which points at the
740 * remote URI for some local service, then read the response from the
741 * error message. To prevent this, censor response bodies out of error
742 * messages.
743 *
744 * @param string $message Uncensored Mercurial command output.
745 * @return string Censored Mercurial command output.
746 */
747 private function censorMercurialErrorMessage($message) {
748 return preg_replace(
749 '/^---%<---.*/sm',
750 pht('<Response body omitted from Mercurial error message.>')."\n",
751 $message);
752 }
753
754
755 /**
756 * @task hg
757 */
758 private function installMercurialHook() {
759 $repository = $this->getRepository();
760 $path = $repository->getLocalPath().'/.hg/hgrc';
761
762 $identifier = $this->getHookContextIdentifier($repository);
763
764 $root = dirname(phutil_get_library_root('phabricator'));
765 $bin = $root.'/bin/commit-hook';
766
767 $data = array();
768 $data[] = '[hooks]';
769
770 // This hook handles normal pushes.
771 $data[] = csprintf(
772 'pretxnchangegroup.phabricator = TERM=dumb %s %s %s',
773 $bin,
774 $identifier,
775 'pretxnchangegroup');
776
777 // This one handles creating bookmarks.
778 $data[] = csprintf(
779 'prepushkey.phabricator = TERM=dumb %s %s %s',
780 $bin,
781 $identifier,
782 'prepushkey');
783
784 $data[] = null;
785
786 $data = implode("\n", $data);
787
788 $this->log('%s', pht('Installing commit hook config to "%s"...', $path));
789
790 Filesystem::writeFile($path, $data);
791 }
792
793
794/* -( Pulling Subversion Working Copies )---------------------------------- */
795
796
797 /**
798 * @task svn
799 */
800 private function executeSubversionCreate() {
801 $repository = $this->getRepository();
802
803 $path = rtrim($repository->getLocalPath(), '/');
804 execx('svnadmin create -- %s', $path);
805 }
806
807
808 /**
809 * @task svn
810 */
811 private function installSubversionHook() {
812 $repository = $this->getRepository();
813 $root = $repository->getLocalPath();
814
815 $path = '/hooks/pre-commit';
816 $this->installHook($root.$path);
817
818 $revprop_path = '/hooks/pre-revprop-change';
819
820 $revprop_argv = array(
821 '--hook-mode',
822 'svn-revprop',
823 );
824
825 $this->installHook($root.$revprop_path, $revprop_argv);
826 }
827
828
829}