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