Correct a parameter order swap in "diffusion.historyquery" for Mercurial
[phabricator.git] / src / applications / diffusion / request / DiffusionRequest.php
blob49ea305908a798c5f68a32252919da5281cdab75
1 <?php
3 /**
4 * Contains logic to parse Diffusion requests, which have a complicated URI
5 * structure.
7 * @task new Creating Requests
8 * @task uri Managing Diffusion URIs
9 */
10 abstract class DiffusionRequest extends Phobject {
12 protected $path;
13 protected $line;
14 protected $branch;
15 protected $lint;
17 protected $symbolicCommit;
18 protected $symbolicType;
19 protected $stableCommit;
21 protected $repository;
22 protected $repositoryCommit;
23 protected $repositoryCommitData;
25 private $isClusterRequest = false;
26 private $initFromConduit = true;
27 private $user;
28 private $branchObject = false;
29 private $refAlternatives;
31 final public function supportsBranches() {
32 return $this->getRepository()->supportsRefs();
35 abstract protected function isStableCommit($symbol);
37 protected function didInitialize() {
38 return null;
42 /* -( Creating Requests )-------------------------------------------------- */
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.
50 * Parameters are:
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.
59 * @param map See documentation.
60 * @return DiffusionRequest New request object.
61 * @task new
63 final public static function newFromDictionary(array $data) {
64 $repository_key = 'repository';
65 $identifier_key = 'callsign';
66 $viewer_key = 'user';
68 $repository = idx($data, $repository_key);
69 $identifier = idx($data, $identifier_key);
71 $have_repository = ($repository !== null);
72 $have_identifier = ($identifier !== null);
74 if ($have_repository && $have_identifier) {
75 throw new Exception(
76 pht(
77 'Specify "%s" or "%s", but not both.',
78 $repository_key,
79 $identifier_key));
82 if (!$have_repository && !$have_identifier) {
83 throw new Exception(
84 pht(
85 'One of "%s" and "%s" is required.',
86 $repository_key,
87 $identifier_key));
90 if ($have_repository) {
91 if (!($repository instanceof PhabricatorRepository)) {
92 if (empty($data[$viewer_key])) {
93 throw new Exception(
94 pht(
95 'Parameter "%s" is required if "%s" is provided.',
96 $viewer_key,
97 $identifier_key));
100 $identifier = $repository;
101 $repository = null;
105 if ($identifier !== null) {
106 $object = self::newFromIdentifier(
107 $identifier,
108 $data[$viewer_key],
109 idx($data, 'edit'));
110 } else {
111 $object = self::newFromRepository($repository);
114 if (!$object) {
115 return null;
118 $object->initializeFromDictionary($data);
120 return $object;
124 * Internal.
126 * @task new
128 private function __construct() {
129 // <private>
134 * Internal. Use @{method:newFromDictionary}, not this method.
136 * @param string Repository identifier.
137 * @param PhabricatorUser Viewing user.
138 * @return DiffusionRequest New request object.
139 * @task new
141 private static function newFromIdentifier(
142 $identifier,
143 PhabricatorUser $viewer,
144 $need_edit = false) {
146 $query = id(new PhabricatorRepositoryQuery())
147 ->setViewer($viewer)
148 ->withIdentifiers(array($identifier))
149 ->needProfileImage(true)
150 ->needURIs(true);
152 if ($need_edit) {
153 $query->requireCapabilities(
154 array(
155 PhabricatorPolicyCapability::CAN_VIEW,
156 PhabricatorPolicyCapability::CAN_EDIT,
160 $repository = $query->executeOne();
162 if (!$repository) {
163 return null;
166 return self::newFromRepository($repository);
171 * Internal. Use @{method:newFromDictionary}, not this method.
173 * @param PhabricatorRepository Repository object.
174 * @return DiffusionRequest New request object.
175 * @task new
177 private static function newFromRepository(
178 PhabricatorRepository $repository) {
180 $map = array(
181 PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'DiffusionGitRequest',
182 PhabricatorRepositoryType::REPOSITORY_TYPE_SVN => 'DiffusionSvnRequest',
183 PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL =>
184 'DiffusionMercurialRequest',
187 $class = idx($map, $repository->getVersionControlSystem());
189 if (!$class) {
190 throw new Exception(pht('Unknown version control system!'));
193 $object = new $class();
195 $object->repository = $repository;
197 return $object;
202 * Internal. Use @{method:newFromDictionary}, not this method.
204 * @param map Map of parsed data.
205 * @return void
206 * @task new
208 private function initializeFromDictionary(array $data) {
209 $blob = idx($data, 'blob');
210 if (strlen($blob)) {
211 $blob = self::parseRequestBlob($blob, $this->supportsBranches());
212 $data = $blob + $data;
215 $this->path = idx($data, 'path');
216 $this->line = idx($data, 'line');
217 $this->initFromConduit = idx($data, 'initFromConduit', true);
218 $this->lint = idx($data, 'lint');
220 $this->symbolicCommit = idx($data, 'commit');
221 if ($this->supportsBranches()) {
222 $this->branch = idx($data, 'branch');
225 if (!$this->getUser()) {
226 $user = idx($data, 'user');
227 if (!$user) {
228 throw new Exception(
229 pht(
230 'You must provide a %s in the dictionary!',
231 'PhabricatorUser'));
233 $this->setUser($user);
236 $this->didInitialize();
239 final public function setUser(PhabricatorUser $user) {
240 $this->user = $user;
241 return $this;
243 final public function getUser() {
244 return $this->user;
247 public function getRepository() {
248 return $this->repository;
251 public function setPath($path) {
252 $this->path = $path;
253 return $this;
256 public function getPath() {
257 return $this->path;
260 public function getLine() {
261 return $this->line;
264 public function getCommit() {
266 // TODO: Probably remove all of this.
268 if ($this->getSymbolicCommit() !== null) {
269 return $this->getSymbolicCommit();
272 return $this->getStableCommit();
276 * Get the symbolic commit associated with this request.
278 * A symbolic commit may be a commit hash, an abbreviated commit hash, a
279 * branch name, a tag name, or an expression like "HEAD^^^". The symbolic
280 * commit may also be absent.
282 * This method always returns the symbol present in the original request,
283 * in unmodified form.
285 * See also @{method:getStableCommit}.
287 * @return string|null Symbolic commit, if one was present in the request.
289 public function getSymbolicCommit() {
290 return $this->symbolicCommit;
295 * Modify the request to move the symbolic commit elsewhere.
297 * @param string New symbolic commit.
298 * @return this
300 public function updateSymbolicCommit($symbol) {
301 $this->symbolicCommit = $symbol;
302 $this->symbolicType = null;
303 $this->stableCommit = null;
304 return $this;
309 * Get the ref type (`commit` or `tag`) of the location associated with this
310 * request.
312 * If a symbolic commit is present in the request, this method identifies
313 * the type of the symbol. Otherwise, it identifies the type of symbol of
314 * the location the request is implicitly associated with. This will probably
315 * always be `commit`.
317 * @return string Symbolic commit type (`commit` or `tag`).
319 public function getSymbolicType() {
320 if ($this->symbolicType === null) {
321 // As a side effect, this resolves the symbolic type.
322 $this->getStableCommit();
324 return $this->symbolicType;
329 * Retrieve the stable, permanent commit name identifying the repository
330 * location associated with this request.
332 * This returns a non-symbolic identifier for the current commit: in Git and
333 * Mercurial, a 40-character SHA1; in SVN, a revision number.
335 * See also @{method:getSymbolicCommit}.
337 * @return string Stable commit name, like a git hash or SVN revision. Not
338 * a symbolic commit reference.
340 public function getStableCommit() {
341 if (!$this->stableCommit) {
342 if ($this->isStableCommit($this->symbolicCommit)) {
343 $this->stableCommit = $this->symbolicCommit;
344 $this->symbolicType = 'commit';
345 } else {
346 $this->queryStableCommit();
349 return $this->stableCommit;
353 public function getBranch() {
354 return $this->branch;
357 public function getLint() {
358 return $this->lint;
361 protected function getArcanistBranch() {
362 return $this->getBranch();
365 public function loadBranch() {
366 // TODO: Get rid of this and do real Queries on real objects.
368 if ($this->branchObject === false) {
369 $this->branchObject = PhabricatorRepositoryBranch::loadBranch(
370 $this->getRepository()->getID(),
371 $this->getArcanistBranch());
374 return $this->branchObject;
377 public function loadCoverage() {
378 // TODO: This should also die.
379 $branch = $this->loadBranch();
380 if (!$branch) {
381 return;
384 $path = $this->getPath();
385 $path_map = id(new DiffusionPathIDQuery(array($path)))->loadPathIDs();
387 $coverage_row = queryfx_one(
388 id(new PhabricatorRepository())->establishConnection('r'),
389 'SELECT * FROM %T WHERE branchID = %d AND pathID = %d
390 ORDER BY commitID DESC LIMIT 1',
391 'repository_coverage',
392 $branch->getID(),
393 $path_map[$path]);
395 if (!$coverage_row) {
396 return null;
399 return idx($coverage_row, 'coverage');
403 public function loadCommit() {
404 if (empty($this->repositoryCommit)) {
405 $repository = $this->getRepository();
407 $commit = id(new DiffusionCommitQuery())
408 ->setViewer($this->getUser())
409 ->withRepository($repository)
410 ->withIdentifiers(array($this->getStableCommit()))
411 ->executeOne();
412 if ($commit) {
413 $commit->attachRepository($repository);
415 $this->repositoryCommit = $commit;
417 return $this->repositoryCommit;
420 public function loadCommitData() {
421 if (empty($this->repositoryCommitData)) {
422 $commit = $this->loadCommit();
423 $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
424 'commitID = %d',
425 $commit->getID());
426 if (!$data) {
427 $data = new PhabricatorRepositoryCommitData();
428 $data->setCommitMessage(
429 pht('(This commit has not been fully parsed yet.)'));
431 $this->repositoryCommitData = $data;
433 return $this->repositoryCommitData;
436 /* -( Managing Diffusion URIs )-------------------------------------------- */
439 public function generateURI(array $params) {
440 if (empty($params['stable'])) {
441 $default_commit = $this->getSymbolicCommit();
442 } else {
443 $default_commit = $this->getStableCommit();
446 $defaults = array(
447 'path' => $this->getPath(),
448 'branch' => $this->getBranch(),
449 'commit' => $default_commit,
450 'lint' => idx($params, 'lint', $this->getLint()),
453 foreach ($defaults as $key => $val) {
454 if (!isset($params[$key])) { // Overwrite NULL.
455 $params[$key] = $val;
459 return $this->getRepository()->generateURI($params);
463 * Internal. Public only for unit tests.
465 * Parse the request URI into components.
467 * @param string URI blob.
468 * @param bool True if this VCS supports branches.
469 * @return map Parsed URI.
471 * @task uri
473 public static function parseRequestBlob($blob, $supports_branches) {
474 $result = array(
475 'branch' => null,
476 'path' => null,
477 'commit' => null,
478 'line' => null,
481 $matches = null;
483 if ($supports_branches) {
484 // Consume the front part of the URI, up to the first "/". This is the
485 // path-component encoded branch name.
486 if (preg_match('@^([^/]+)/@', $blob, $matches)) {
487 $result['branch'] = phutil_unescape_uri_path_component($matches[1]);
488 $blob = substr($blob, strlen($matches[1]) + 1);
492 // Consume the back part of the URI, up to the first "$". Use a negative
493 // lookbehind to prevent matching '$$'. We double the '$' symbol when
494 // encoding so that files with names like "money/$100" will survive.
495 $pattern = '@(?:(?:^|[^$])(?:[$][$])*)[$]([\d,-]+)$@';
496 if (preg_match($pattern, $blob, $matches)) {
497 $result['line'] = $matches[1];
498 $blob = substr($blob, 0, -(strlen($matches[1]) + 1));
501 // We've consumed the line number if it exists, so unescape "$" in the
502 // rest of the string.
503 $blob = str_replace('$$', '$', $blob);
505 // Consume the commit name, stopping on ';;'. We allow any character to
506 // appear in commits names, as they can sometimes be symbolic names (like
507 // tag names or refs).
508 if (preg_match('@(?:(?:^|[^;])(?:;;)*);([^;].*)$@', $blob, $matches)) {
509 $result['commit'] = $matches[1];
510 $blob = substr($blob, 0, -(strlen($matches[1]) + 1));
513 // We've consumed the commit if it exists, so unescape ";" in the rest
514 // of the string.
515 $blob = str_replace(';;', ';', $blob);
517 if (strlen($blob)) {
518 $result['path'] = $blob;
521 $parts = explode('/', $result['path']);
522 foreach ($parts as $part) {
523 // Prevent any hyjinx since we're ultimately shipping this to the
524 // filesystem under a lot of workflows.
525 if ($part == '..') {
526 throw new Exception(pht('Invalid path URI.'));
530 return $result;
534 * Check that the working copy of the repository is present and readable.
536 * @param string Path to the working copy.
538 protected function validateWorkingCopy($path) {
539 if (!is_readable(dirname($path))) {
540 $this->raisePermissionException();
543 if (!Filesystem::pathExists($path)) {
544 $this->raiseCloneException();
548 protected function raisePermissionException() {
549 $host = php_uname('n');
550 throw new DiffusionSetupException(
551 pht(
552 'The clone of this repository ("%s") on the local machine ("%s") '.
553 'could not be read. Ensure that the repository is in a '.
554 'location where the web server has read permissions.',
555 $this->getRepository()->getDisplayName(),
556 $host));
559 protected function raiseCloneException() {
560 $host = php_uname('n');
561 throw new DiffusionSetupException(
562 pht(
563 'The working copy for this repository ("%s") has not been cloned yet '.
564 'on this machine ("%s"). Make sure you havestarted the Phabricator '.
565 'daemons. If this problem persists for longer than a clone should '.
566 'take, check the daemon logs (in the Daemon Console) to see if there '.
567 'were errors cloning the repository. Consult the "Diffusion User '.
568 'Guide" in the documentation for help setting up repositories.',
569 $this->getRepository()->getDisplayName(),
570 $host));
573 private function queryStableCommit() {
574 $types = array();
575 if ($this->symbolicCommit) {
576 $ref = $this->symbolicCommit;
577 } else {
578 if ($this->supportsBranches()) {
579 $ref = $this->getBranch();
580 $types = array(
581 PhabricatorRepositoryRefCursor::TYPE_BRANCH,
583 } else {
584 $ref = 'HEAD';
588 $results = $this->resolveRefs(array($ref), $types);
590 $matches = idx($results, $ref, array());
591 if (!$matches) {
592 $message = pht(
593 'Ref "%s" does not exist in this repository.',
594 $ref);
595 throw id(new DiffusionRefNotFoundException($message))
596 ->setRef($ref);
599 if (count($matches) > 1) {
600 $match = $this->chooseBestRefMatch($ref, $matches);
601 } else {
602 $match = head($matches);
605 $this->stableCommit = $match['identifier'];
606 $this->symbolicType = $match['type'];
609 public function getRefAlternatives() {
610 // Make sure we've resolved the reference into a stable commit first.
611 try {
612 $this->getStableCommit();
613 } catch (DiffusionRefNotFoundException $ex) {
614 // If we have a bad reference, just return the empty set of
615 // alternatives.
617 return $this->refAlternatives;
620 private function chooseBestRefMatch($ref, array $results) {
621 // First, filter out less-desirable matches.
622 $candidates = array();
623 foreach ($results as $result) {
624 // Exclude closed heads.
625 if ($result['type'] == 'branch') {
626 if (idx($result, 'closed')) {
627 continue;
631 $candidates[] = $result;
634 // If we filtered everything, undo the filtering.
635 if (!$candidates) {
636 $candidates = $results;
639 // TODO: Do a better job of selecting the best match?
640 $match = head($candidates);
642 // After choosing the best alternative, save all the alternatives so the
643 // UI can show them to the user.
644 if (count($candidates) > 1) {
645 $this->refAlternatives = $candidates;
648 return $match;
651 public function resolveRefs(array $refs, array $types = array()) {
652 // First, try to resolve refs from fast cache sources.
653 $cached_query = id(new DiffusionCachedResolveRefsQuery())
654 ->setRepository($this->getRepository())
655 ->withRefs($refs);
657 if ($types) {
658 $cached_query->withTypes($types);
661 $cached_results = $cached_query->execute();
663 // Throw away all the refs we resolved. Hopefully, we'll throw away
664 // everything here.
665 foreach ($refs as $key => $ref) {
666 if (isset($cached_results[$ref])) {
667 unset($refs[$key]);
671 // If we couldn't pull everything out of the cache, execute the underlying
672 // VCS operation.
673 if ($refs) {
674 $vcs_results = DiffusionQuery::callConduitWithDiffusionRequest(
675 $this->getUser(),
676 $this,
677 'diffusion.resolverefs',
678 array(
679 'types' => $types,
680 'refs' => $refs,
682 } else {
683 $vcs_results = array();
686 return $vcs_results + $cached_results;
689 public function setIsClusterRequest($is_cluster_request) {
690 $this->isClusterRequest = $is_cluster_request;
691 return $this;
694 public function getIsClusterRequest() {
695 return $this->isClusterRequest;