[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * Contains logic to parse Diffusion requests, which have a complicated URI 5 * structure. 6 * 7 * @task new Creating Requests 8 * @task uri Managing Diffusion URIs 9 */ 10 abstract class DiffusionRequest { 11 12 protected $callsign; 13 protected $path; 14 protected $line; 15 protected $branch; 16 protected $lint; 17 18 protected $symbolicCommit; 19 protected $symbolicType; 20 protected $stableCommit; 21 22 protected $repository; 23 protected $repositoryCommit; 24 protected $repositoryCommitData; 25 protected $arcanistProjects; 26 27 private $initFromConduit = true; 28 private $user; 29 private $branchObject = false; 30 31 abstract public function supportsBranches(); 32 abstract protected function isStableCommit($symbol); 33 34 protected function didInitialize() { 35 return null; 36 } 37 38 39 /* -( Creating Requests )-------------------------------------------------- */ 40 41 42 /** 43 * Create a new synthetic request from a parameter dictionary. If you need 44 * a @{class:DiffusionRequest} object in order to issue a DiffusionQuery, you 45 * can use this method to build one. 46 * 47 * Parameters are: 48 * 49 * - `callsign` Repository callsign. Provide this or `repository`. 50 * - `user` Viewing user. Required if `callsign` is provided. 51 * - `repository` Repository object. Provide this or `callsign`. 52 * - `branch` Optional, branch name. 53 * - `path` Optional, file path. 54 * - `commit` Optional, commit identifier. 55 * - `line` Optional, line range. 56 * 57 * @param map See documentation. 58 * @return DiffusionRequest New request object. 59 * @task new 60 */ 61 final public static function newFromDictionary(array $data) { 62 if (isset($data['repository']) && isset($data['callsign'])) { 63 throw new Exception( 64 "Specify 'repository' or 'callsign', but not both."); 65 } else if (!isset($data['repository']) && !isset($data['callsign'])) { 66 throw new Exception( 67 "One of 'repository' and 'callsign' is required."); 68 } else if (isset($data['callsign']) && empty($data['user'])) { 69 throw new Exception( 70 "Parameter 'user' is required if 'callsign' is provided."); 71 } 72 73 if (isset($data['repository'])) { 74 $object = self::newFromRepository($data['repository']); 75 } else { 76 $object = self::newFromCallsign($data['callsign'], $data['user']); 77 } 78 79 $object->initializeFromDictionary($data); 80 81 return $object; 82 } 83 84 85 /** 86 * Create a new request from an Aphront request dictionary. This is an 87 * internal method that you generally should not call directly; instead, 88 * call @{method:newFromDictionary}. 89 * 90 * @param map Map of Aphront request data. 91 * @return DiffusionRequest New request object. 92 * @task new 93 */ 94 final public static function newFromAphrontRequestDictionary( 95 array $data, 96 AphrontRequest $request) { 97 98 $callsign = phutil_unescape_uri_path_component(idx($data, 'callsign')); 99 $object = self::newFromCallsign($callsign, $request->getUser()); 100 101 $use_branches = $object->supportsBranches(); 102 $parsed = self::parseRequestBlob(idx($data, 'dblob'), $use_branches); 103 104 $object->setUser($request->getUser()); 105 $object->initializeFromDictionary($parsed); 106 $object->lint = $request->getStr('lint'); 107 return $object; 108 } 109 110 111 /** 112 * Internal. 113 * 114 * @task new 115 */ 116 final private function __construct() { 117 // <private> 118 } 119 120 121 /** 122 * Internal. Use @{method:newFromDictionary}, not this method. 123 * 124 * @param string Repository callsign. 125 * @param PhabricatorUser Viewing user. 126 * @return DiffusionRequest New request object. 127 * @task new 128 */ 129 final private static function newFromCallsign( 130 $callsign, 131 PhabricatorUser $viewer) { 132 133 $repository = id(new PhabricatorRepositoryQuery()) 134 ->setViewer($viewer) 135 ->withCallsigns(array($callsign)) 136 ->executeOne(); 137 138 if (!$repository) { 139 throw new Exception("No such repository '{$callsign}'."); 140 } 141 142 return self::newFromRepository($repository); 143 } 144 145 146 /** 147 * Internal. Use @{method:newFromDictionary}, not this method. 148 * 149 * @param PhabricatorRepository Repository object. 150 * @return DiffusionRequest New request object. 151 * @task new 152 */ 153 final private static function newFromRepository( 154 PhabricatorRepository $repository) { 155 156 $map = array( 157 PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'DiffusionGitRequest', 158 PhabricatorRepositoryType::REPOSITORY_TYPE_SVN => 'DiffusionSvnRequest', 159 PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => 160 'DiffusionMercurialRequest', 161 ); 162 163 $class = idx($map, $repository->getVersionControlSystem()); 164 165 if (!$class) { 166 throw new Exception('Unknown version control system!'); 167 } 168 169 $object = new $class(); 170 171 $object->repository = $repository; 172 $object->callsign = $repository->getCallsign(); 173 174 return $object; 175 } 176 177 178 /** 179 * Internal. Use @{method:newFromDictionary}, not this method. 180 * 181 * @param map Map of parsed data. 182 * @return void 183 * @task new 184 */ 185 final private function initializeFromDictionary(array $data) { 186 $this->path = idx($data, 'path'); 187 $this->line = idx($data, 'line'); 188 $this->initFromConduit = idx($data, 'initFromConduit', true); 189 190 $this->symbolicCommit = idx($data, 'commit'); 191 if ($this->supportsBranches()) { 192 $this->branch = idx($data, 'branch'); 193 } 194 195 if (!$this->getUser()) { 196 $user = idx($data, 'user'); 197 if (!$user) { 198 throw new Exception( 199 'You must provide a PhabricatorUser in the dictionary!'); 200 } 201 $this->setUser($user); 202 } 203 204 $this->didInitialize(); 205 } 206 207 final protected function shouldInitFromConduit() { 208 return $this->initFromConduit; 209 } 210 211 final public function setUser(PhabricatorUser $user) { 212 $this->user = $user; 213 return $this; 214 } 215 final public function getUser() { 216 return $this->user; 217 } 218 219 public function getRepository() { 220 return $this->repository; 221 } 222 223 public function getCallsign() { 224 return $this->callsign; 225 } 226 227 public function setPath($path) { 228 $this->path = $path; 229 return $this; 230 } 231 232 public function getPath() { 233 return $this->path; 234 } 235 236 public function getLine() { 237 return $this->line; 238 } 239 240 public function getCommit() { 241 242 // TODO: Probably remove all of this. 243 244 if ($this->getSymbolicCommit() !== null) { 245 return $this->getSymbolicCommit(); 246 } 247 248 return $this->getStableCommit(); 249 } 250 251 /** 252 * Get the symbolic commit associated with this request. 253 * 254 * A symbolic commit may be a commit hash, an abbreviated commit hash, a 255 * branch name, a tag name, or an expression like "HEAD^^^". The symbolic 256 * commit may also be absent. 257 * 258 * This method always returns the symbol present in the original request, 259 * in unmodified form. 260 * 261 * See also @{method:getStableCommit}. 262 * 263 * @return string|null Symbolic commit, if one was present in the request. 264 */ 265 public function getSymbolicCommit() { 266 return $this->symbolicCommit; 267 } 268 269 270 /** 271 * Modify the request to move the symbolic commit elsewhere. 272 * 273 * @param string New symbolic commit. 274 * @return this 275 */ 276 public function updateSymbolicCommit($symbol) { 277 $this->symbolicCommit = $symbol; 278 $this->symbolicType = null; 279 $this->stableCommit = null; 280 return $this; 281 } 282 283 284 /** 285 * Get the ref type (`commit` or `tag`) of the location associated with this 286 * request. 287 * 288 * If a symbolic commit is present in the request, this method identifies 289 * the type of the symbol. Otherwise, it identifies the type of symbol of 290 * the location the request is implicitly associated with. This will probably 291 * always be `commit`. 292 * 293 * @return string Symbolic commit type (`commit` or `tag`). 294 */ 295 public function getSymbolicType() { 296 if ($this->symbolicType === null) { 297 // As a side effect, this resolves the symbolic type. 298 $this->getStableCommit(); 299 } 300 return $this->symbolicType; 301 } 302 303 304 /** 305 * Retrieve the stable, permanent commit name identifying the repository 306 * location associated with this request. 307 * 308 * This returns a non-symbolic identifier for the current commit: in Git and 309 * Mercurial, a 40-character SHA1; in SVN, a revision number. 310 * 311 * See also @{method:getSymbolicCommit}. 312 * 313 * @return string Stable commit name, like a git hash or SVN revision. Not 314 * a symbolic commit reference. 315 */ 316 public function getStableCommit() { 317 if (!$this->stableCommit) { 318 if ($this->isStableCommit($this->symbolicCommit)) { 319 $this->stableCommit = $this->symbolicCommit; 320 $this->symbolicType = 'commit'; 321 } else { 322 $this->queryStableCommit(); 323 } 324 } 325 return $this->stableCommit; 326 } 327 328 329 public function getBranch() { 330 return $this->branch; 331 } 332 333 public function getLint() { 334 return $this->lint; 335 } 336 337 protected function getArcanistBranch() { 338 return $this->getBranch(); 339 } 340 341 public function loadBranch() { 342 // TODO: Get rid of this and do real Queries on real objects. 343 344 if ($this->branchObject === false) { 345 $this->branchObject = PhabricatorRepositoryBranch::loadBranch( 346 $this->getRepository()->getID(), 347 $this->getArcanistBranch()); 348 } 349 350 return $this->branchObject; 351 } 352 353 public function loadCoverage() { 354 // TODO: This should also die. 355 $branch = $this->loadBranch(); 356 if (!$branch) { 357 return; 358 } 359 360 $path = $this->getPath(); 361 $path_map = id(new DiffusionPathIDQuery(array($path)))->loadPathIDs(); 362 363 $coverage_row = queryfx_one( 364 id(new PhabricatorRepository())->establishConnection('r'), 365 'SELECT * FROM %T WHERE branchID = %d AND pathID = %d 366 ORDER BY commitID DESC LIMIT 1', 367 'repository_coverage', 368 $branch->getID(), 369 $path_map[$path]); 370 371 if (!$coverage_row) { 372 return null; 373 } 374 375 return idx($coverage_row, 'coverage'); 376 } 377 378 379 public function loadCommit() { 380 if (empty($this->repositoryCommit)) { 381 $repository = $this->getRepository(); 382 383 // TODO: (T603) This should be a real query, but we need to sort out 384 // the viewer. 385 $commit = id(new PhabricatorRepositoryCommit())->loadOneWhere( 386 'repositoryID = %d AND commitIdentifier = %s', 387 $repository->getID(), 388 $this->getStableCommit()); 389 if ($commit) { 390 $commit->attachRepository($repository); 391 } 392 $this->repositoryCommit = $commit; 393 } 394 return $this->repositoryCommit; 395 } 396 397 public function loadArcanistProjects() { 398 if (empty($this->arcanistProjects)) { 399 $projects = id(new PhabricatorRepositoryArcanistProject())->loadAllWhere( 400 'repositoryID = %d', 401 $this->getRepository()->getID()); 402 $this->arcanistProjects = $projects; 403 } 404 return $this->arcanistProjects; 405 } 406 407 public function loadCommitData() { 408 if (empty($this->repositoryCommitData)) { 409 $commit = $this->loadCommit(); 410 $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( 411 'commitID = %d', 412 $commit->getID()); 413 if (!$data) { 414 $data = new PhabricatorRepositoryCommitData(); 415 $data->setCommitMessage( 416 '(This commit has not been fully parsed yet.)'); 417 } 418 $this->repositoryCommitData = $data; 419 } 420 return $this->repositoryCommitData; 421 } 422 423 /* -( Managing Diffusion URIs )-------------------------------------------- */ 424 425 426 /** 427 * Generate a Diffusion URI using this request to provide defaults. See 428 * @{method:generateDiffusionURI} for details. This method is the same, but 429 * preserves the request parameters if they are not overridden. 430 * 431 * @param map See @{method:generateDiffusionURI}. 432 * @return PhutilURI Generated URI. 433 * @task uri 434 */ 435 public function generateURI(array $params) { 436 if (empty($params['stable'])) { 437 $default_commit = $this->getSymbolicCommit(); 438 } else { 439 $default_commit = $this->getStableCommit(); 440 } 441 442 $defaults = array( 443 'callsign' => $this->getCallsign(), 444 'path' => $this->getPath(), 445 'branch' => $this->getBranch(), 446 'commit' => $default_commit, 447 'lint' => idx($params, 'lint', $this->getLint()), 448 ); 449 foreach ($defaults as $key => $val) { 450 if (!isset($params[$key])) { // Overwrite NULL. 451 $params[$key] = $val; 452 } 453 } 454 return self::generateDiffusionURI($params); 455 } 456 457 458 /** 459 * Generate a Diffusion URI from a parameter map. Applies the correct encoding 460 * and formatting to the URI. Parameters are: 461 * 462 * - `action` One of `history`, `browse`, `change`, `lastmodified`, 463 * `branch`, `tags`, `branches`, or `revision-ref`. The action specified 464 * by the URI. 465 * - `callsign` Repository callsign. 466 * - `branch` Optional if action is not `branch`, branch name. 467 * - `path` Optional, path to file. 468 * - `commit` Optional, commit identifier. 469 * - `line` Optional, line range. 470 * - `lint` Optional, lint code. 471 * - `params` Optional, query parameters. 472 * 473 * The function generates the specified URI and returns it. 474 * 475 * @param map See documentation. 476 * @return PhutilURI Generated URI. 477 * @task uri 478 */ 479 public static function generateDiffusionURI(array $params) { 480 $action = idx($params, 'action'); 481 482 $callsign = idx($params, 'callsign'); 483 $path = idx($params, 'path'); 484 $branch = idx($params, 'branch'); 485 $commit = idx($params, 'commit'); 486 $line = idx($params, 'line'); 487 488 if (strlen($callsign)) { 489 $callsign = phutil_escape_uri_path_component($callsign).'/'; 490 } 491 492 if (strlen($branch)) { 493 $branch = phutil_escape_uri_path_component($branch).'/'; 494 } 495 496 if (strlen($path)) { 497 $path = ltrim($path, '/'); 498 $path = str_replace(array(';', '$'), array(';;', '$$'), $path); 499 $path = phutil_escape_uri($path); 500 } 501 502 $path = "{$branch}{$path}"; 503 504 if (strlen($commit)) { 505 $commit = str_replace('$', '$$', $commit); 506 $commit = ';'.phutil_escape_uri($commit); 507 } 508 509 if (strlen($line)) { 510 $line = '$'.phutil_escape_uri($line); 511 } 512 513 $req_callsign = false; 514 $req_branch = false; 515 $req_commit = false; 516 517 switch ($action) { 518 case 'history': 519 case 'browse': 520 case 'change': 521 case 'lastmodified': 522 case 'tags': 523 case 'branches': 524 case 'lint': 525 $req_callsign = true; 526 break; 527 case 'branch': 528 $req_callsign = true; 529 $req_branch = true; 530 break; 531 case 'commit': 532 $req_callsign = true; 533 $req_commit = true; 534 break; 535 } 536 537 if ($req_callsign && !strlen($callsign)) { 538 throw new Exception( 539 "Diffusion URI action '{$action}' requires callsign!"); 540 } 541 542 if ($req_commit && !strlen($commit)) { 543 throw new Exception( 544 "Diffusion URI action '{$action}' requires commit!"); 545 } 546 547 switch ($action) { 548 case 'change': 549 case 'history': 550 case 'browse': 551 case 'lastmodified': 552 case 'tags': 553 case 'branches': 554 case 'lint': 555 case 'pathtree': 556 $uri = "/diffusion/{$callsign}{$action}/{$path}{$commit}{$line}"; 557 break; 558 case 'branch': 559 if (strlen($path)) { 560 $uri = "/diffusion/{$callsign}repository/{$path}"; 561 } else { 562 $uri = "/diffusion/{$callsign}"; 563 } 564 break; 565 case 'external': 566 $commit = ltrim($commit, ';'); 567 $uri = "/diffusion/external/{$commit}/"; 568 break; 569 case 'rendering-ref': 570 // This isn't a real URI per se, it's passed as a query parameter to 571 // the ajax changeset stuff but then we parse it back out as though 572 // it came from a URI. 573 $uri = rawurldecode("{$path}{$commit}"); 574 break; 575 case 'commit': 576 $commit = ltrim($commit, ';'); 577 $callsign = rtrim($callsign, '/'); 578 $uri = "/r{$callsign}{$commit}"; 579 break; 580 default: 581 throw new Exception("Unknown Diffusion URI action '{$action}'!"); 582 } 583 584 if ($action == 'rendering-ref') { 585 return $uri; 586 } 587 588 $uri = new PhutilURI($uri); 589 590 if (isset($params['lint'])) { 591 $params['params'] = idx($params, 'params', array()) + array( 592 'lint' => $params['lint'], 593 ); 594 } 595 596 if (idx($params, 'params')) { 597 $uri->setQueryParams($params['params']); 598 } 599 600 return $uri; 601 } 602 603 604 /** 605 * Internal. Public only for unit tests. 606 * 607 * Parse the request URI into components. 608 * 609 * @param string URI blob. 610 * @param bool True if this VCS supports branches. 611 * @return map Parsed URI. 612 * 613 * @task uri 614 */ 615 public static function parseRequestBlob($blob, $supports_branches) { 616 $result = array( 617 'branch' => null, 618 'path' => null, 619 'commit' => null, 620 'line' => null, 621 ); 622 623 $matches = null; 624 625 if ($supports_branches) { 626 // Consume the front part of the URI, up to the first "/". This is the 627 // path-component encoded branch name. 628 if (preg_match('@^([^/]+)/@', $blob, $matches)) { 629 $result['branch'] = phutil_unescape_uri_path_component($matches[1]); 630 $blob = substr($blob, strlen($matches[1]) + 1); 631 } 632 } 633 634 // Consume the back part of the URI, up to the first "$". Use a negative 635 // lookbehind to prevent matching '$$'. We double the '$' symbol when 636 // encoding so that files with names like "money/$100" will survive. 637 $pattern = '@(?:(?:^|[^$])(?:[$][$])*)[$]([\d-,]+)$@'; 638 if (preg_match($pattern, $blob, $matches)) { 639 $result['line'] = $matches[1]; 640 $blob = substr($blob, 0, -(strlen($matches[1]) + 1)); 641 } 642 643 // We've consumed the line number if it exists, so unescape "$" in the 644 // rest of the string. 645 $blob = str_replace('$$', '$', $blob); 646 647 // Consume the commit name, stopping on ';;'. We allow any character to 648 // appear in commits names, as they can sometimes be symbolic names (like 649 // tag names or refs). 650 if (preg_match('@(?:(?:^|[^;])(?:;;)*);([^;].*)$@', $blob, $matches)) { 651 $result['commit'] = $matches[1]; 652 $blob = substr($blob, 0, -(strlen($matches[1]) + 1)); 653 } 654 655 // We've consumed the commit if it exists, so unescape ";" in the rest 656 // of the string. 657 $blob = str_replace(';;', ';', $blob); 658 659 if (strlen($blob)) { 660 $result['path'] = $blob; 661 } 662 663 $parts = explode('/', $result['path']); 664 foreach ($parts as $part) { 665 // Prevent any hyjinx since we're ultimately shipping this to the 666 // filesystem under a lot of workflows. 667 if ($part == '..') { 668 throw new Exception('Invalid path URI.'); 669 } 670 } 671 672 return $result; 673 } 674 675 /** 676 * Check that the working copy of the repository is present and readable. 677 * 678 * @param string Path to the working copy. 679 */ 680 protected function validateWorkingCopy($path) { 681 if (!is_readable(dirname($path))) { 682 $this->raisePermissionException(); 683 } 684 685 if (!Filesystem::pathExists($path)) { 686 $this->raiseCloneException(); 687 } 688 } 689 690 protected function raisePermissionException() { 691 $host = php_uname('n'); 692 $callsign = $this->getRepository()->getCallsign(); 693 throw new DiffusionSetupException( 694 "The clone of this repository ('{$callsign}') on the local machine ". 695 "('{$host}') could not be read. Ensure that the repository is in a ". 696 "location where the web server has read permissions."); 697 } 698 699 protected function raiseCloneException() { 700 $host = php_uname('n'); 701 $callsign = $this->getRepository()->getCallsign(); 702 throw new DiffusionSetupException( 703 "The working copy for this repository ('{$callsign}') hasn't been ". 704 "cloned yet on this machine ('{$host}'). Make sure you've started the ". 705 "Phabricator daemons. If this problem persists for longer than a clone ". 706 "should take, check the daemon logs (in the Daemon Console) to see if ". 707 "there were errors cloning the repository. Consult the 'Diffusion User ". 708 "Guide' in the documentation for help setting up repositories."); 709 } 710 711 private function queryStableCommit() { 712 if ($this->symbolicCommit) { 713 $ref = $this->symbolicCommit; 714 } else { 715 if ($this->supportsBranches()) { 716 $ref = $this->getResolvableBranchName($this->getBranch()); 717 } else { 718 $ref = 'HEAD'; 719 } 720 } 721 722 $results = $this->resolveRefs(array($ref)); 723 724 $matches = idx($results, $ref, array()); 725 if (count($matches) !== 1) { 726 $message = pht('Ref "%s" is ambiguous or does not exist.', $ref); 727 throw id(new DiffusionRefNotFoundException($message)) 728 ->setRef($ref); 729 } 730 731 $match = head($matches); 732 733 $this->stableCommit = $match['identifier']; 734 $this->symbolicType = $match['type']; 735 } 736 737 protected function getResolvableBranchName($branch) { 738 return $branch; 739 } 740 741 private function resolveRefs(array $refs) { 742 if ($this->shouldInitFromConduit()) { 743 return DiffusionQuery::callConduitWithDiffusionRequest( 744 $this->getUser(), 745 $this, 746 'diffusion.resolverefs', 747 array( 748 'refs' => $refs, 749 )); 750 } else { 751 return id(new DiffusionLowLevelResolveRefsQuery()) 752 ->setRepository($this->getRepository()) 753 ->withRefs($refs) 754 ->execute(); 755 } 756 } 757 758 759 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |