@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
at recaptime-dev/main 829 lines 25 kB view raw
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}