4 * Contains logic to parse Diffusion requests, which have a complicated URI
7 * @task new Creating Requests
8 * @task uri Managing Diffusion URIs
10 abstract class DiffusionRequest
extends Phobject
{
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;
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() {
42 /* -( Creating Requests )-------------------------------------------------- */
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.
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.
63 final public static function newFromDictionary(array $data) {
64 $repository_key = 'repository';
65 $identifier_key = 'callsign';
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) {
77 'Specify "%s" or "%s", but not both.',
82 if (!$have_repository && !$have_identifier) {
85 'One of "%s" and "%s" is required.',
90 if ($have_repository) {
91 if (!($repository instanceof PhabricatorRepository
)) {
92 if (empty($data[$viewer_key])) {
95 'Parameter "%s" is required if "%s" is provided.',
100 $identifier = $repository;
105 if ($identifier !== null) {
106 $object = self
::newFromIdentifier(
111 $object = self
::newFromRepository($repository);
118 $object->initializeFromDictionary($data);
128 private function __construct() {
134 * Internal. Use @{method:newFromDictionary}, not this method.
136 * @param string Repository identifier.
137 * @param PhabricatorUser Viewing user.
138 * @return DiffusionRequest New request object.
141 private static function newFromIdentifier(
143 PhabricatorUser
$viewer,
144 $need_edit = false) {
146 $query = id(new PhabricatorRepositoryQuery())
148 ->withIdentifiers(array($identifier))
149 ->needProfileImage(true)
153 $query->requireCapabilities(
155 PhabricatorPolicyCapability
::CAN_VIEW
,
156 PhabricatorPolicyCapability
::CAN_EDIT
,
160 $repository = $query->executeOne();
166 return self
::newFromRepository($repository);
171 * Internal. Use @{method:newFromDictionary}, not this method.
173 * @param PhabricatorRepository Repository object.
174 * @return DiffusionRequest New request object.
177 private static function newFromRepository(
178 PhabricatorRepository
$repository) {
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());
190 throw new Exception(pht('Unknown version control system!'));
193 $object = new $class();
195 $object->repository
= $repository;
202 * Internal. Use @{method:newFromDictionary}, not this method.
204 * @param map Map of parsed data.
208 private function initializeFromDictionary(array $data) {
209 $blob = idx($data, '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');
230 'You must provide a %s in the dictionary!',
233 $this->setUser($user);
236 $this->didInitialize();
239 final public function setUser(PhabricatorUser
$user) {
243 final public function getUser() {
247 public function getRepository() {
248 return $this->repository
;
251 public function setPath($path) {
256 public function getPath() {
260 public function getLine() {
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.
300 public function updateSymbolicCommit($symbol) {
301 $this->symbolicCommit
= $symbol;
302 $this->symbolicType
= null;
303 $this->stableCommit
= null;
309 * Get the ref type (`commit` or `tag`) of the location associated with this
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';
346 $this->queryStableCommit();
349 return $this->stableCommit
;
353 public function getBranch() {
354 return $this->branch
;
357 public function getLint() {
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();
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',
395 if (!$coverage_row) {
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()))
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(
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();
443 $default_commit = $this->getStableCommit();
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.
473 public static function parseRequestBlob($blob, $supports_branches) {
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
515 $blob = str_replace(';;', ';', $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.
526 throw new Exception(pht('Invalid path URI.'));
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(
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(),
559 protected function raiseCloneException() {
560 $host = php_uname('n');
561 throw new DiffusionSetupException(
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(),
573 private function queryStableCommit() {
575 if ($this->symbolicCommit
) {
576 $ref = $this->symbolicCommit
;
578 if ($this->supportsBranches()) {
579 $ref = $this->getBranch();
581 PhabricatorRepositoryRefCursor
::TYPE_BRANCH
,
588 $results = $this->resolveRefs(array($ref), $types);
590 $matches = idx($results, $ref, array());
593 'Ref "%s" does not exist in this repository.',
595 throw id(new DiffusionRefNotFoundException($message))
599 if (count($matches) > 1) {
600 $match = $this->chooseBestRefMatch($ref, $matches);
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.
612 $this->getStableCommit();
613 } catch (DiffusionRefNotFoundException
$ex) {
614 // If we have a bad reference, just return the empty set of
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')) {
631 $candidates[] = $result;
634 // If we filtered everything, undo the filtering.
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;
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())
658 $cached_query->withTypes($types);
661 $cached_results = $cached_query->execute();
663 // Throw away all the refs we resolved. Hopefully, we'll throw away
665 foreach ($refs as $key => $ref) {
666 if (isset($cached_results[$ref])) {
671 // If we couldn't pull everything out of the cache, execute the underlying
674 $vcs_results = DiffusionQuery
::callConduitWithDiffusionRequest(
677 'diffusion.resolverefs',
683 $vcs_results = array();
686 return $vcs_results +
$cached_results;
689 public function setIsClusterRequest($is_cluster_request) {
690 $this->isClusterRequest
= $is_cluster_request;
694 public function getIsClusterRequest() {
695 return $this->isClusterRequest
;