diff --git a/src/applications/diffusion/conduit/DiffusionResolveRefsConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionResolveRefsConduitAPIMethod.php index 0cfb2bf565..4971ef5552 100644 --- a/src/applications/diffusion/conduit/DiffusionResolveRefsConduitAPIMethod.php +++ b/src/applications/diffusion/conduit/DiffusionResolveRefsConduitAPIMethod.php @@ -1,33 +1,40 @@ >>'; } protected function defineCustomParamTypes() { return array( 'refs' => 'required list', + 'types' => 'optional list', ); } protected function getResult(ConduitAPIRequest $request) { $refs = $request->getValue('refs'); + $types = $request->getValue('types'); - return id(new DiffusionLowLevelResolveRefsQuery()) + $query = id(new DiffusionLowLevelResolveRefsQuery()) ->setRepository($this->getDiffusionRequest()->getRepository()) - ->withRefs($refs) - ->execute(); + ->withRefs($refs); + + if ($types) { + $query->withTypes($types); + } + + return $query->execute(); } } diff --git a/src/applications/diffusion/query/DiffusionCachedResolveRefsQuery.php b/src/applications/diffusion/query/DiffusionCachedResolveRefsQuery.php index 5dbadb3791..8190a035f2 100644 --- a/src/applications/diffusion/query/DiffusionCachedResolveRefsQuery.php +++ b/src/applications/diffusion/query/DiffusionCachedResolveRefsQuery.php @@ -1,175 +1,185 @@ refs = $refs; return $this; } + public function withTypes(array $types) { + $this->types = $types; + return $this; + } + protected function executeQuery() { if (!$this->refs) { return array(); } switch ($this->getRepository()->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $result = $this->resolveGitAndMercurialRefs(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $result = $this->resolveSubversionRefs(); break; default: throw new Exception('Unsupported repository type!'); } + if ($this->types !== null) { + $result = $this->filterRefsByType($result, $this->types); + } + return $result; } /** * Resolve refs in Git and Mercurial repositories. * * We can resolve commit hashes from the commits table, and branch and tag * names from the refcursor table. */ private function resolveGitAndMercurialRefs() { $repository = $this->getRepository(); $conn_r = $repository->establishConnection('r'); $results = array(); $prefixes = array(); foreach ($this->refs as $ref) { // We require refs to look like hashes and be at least 4 characters // long. This is similar to the behavior of git. if (preg_match('/^[a-f0-9]{4,}$/', $ref)) { $prefixes[] = qsprintf( $conn_r, '(commitIdentifier LIKE %>)', $ref); } } if ($prefixes) { $commits = queryfx_all( $conn_r, 'SELECT commitIdentifier FROM %T WHERE repositoryID = %s AND %Q', id(new PhabricatorRepositoryCommit())->getTableName(), $repository->getID(), implode(' OR ', $prefixes)); foreach ($commits as $commit) { $hash = $commit['commitIdentifier']; foreach ($this->refs as $ref) { if (!strncmp($hash, $ref, strlen($ref))) { $results[$ref][] = array( 'type' => 'commit', 'identifier' => $hash, ); } } } } $name_hashes = array(); foreach ($this->refs as $ref) { $name_hashes[PhabricatorHash::digestForIndex($ref)] = $ref; } $cursors = queryfx_all( $conn_r, 'SELECT refNameHash, refType, commitIdentifier, isClosed FROM %T WHERE repositoryPHID = %s AND refNameHash IN (%Ls)', id(new PhabricatorRepositoryRefCursor())->getTableName(), $repository->getPHID(), array_keys($name_hashes)); foreach ($cursors as $cursor) { if (isset($name_hashes[$cursor['refNameHash']])) { $results[$name_hashes[$cursor['refNameHash']]][] = array( 'type' => $cursor['refType'], 'identifier' => $cursor['commitIdentifier'], 'closed' => (bool)$cursor['isClosed'], ); // TODO: In Git, we don't store (and thus don't return) the hash // of the tag itself. It would be vaguely nice to do this. } } return $results; } /** * Resolve refs in Subversion repositories. * * We can resolve all numeric identifiers and the keyword `HEAD`. */ private function resolveSubversionRefs() { $repository = $this->getRepository(); $max_commit = id(new PhabricatorRepositoryCommit()) ->loadOneWhere( 'repositoryID = %d ORDER BY epoch DESC, id DESC LIMIT 1', $repository->getID()); if (!$max_commit) { // This repository is empty or hasn't parsed yet, so none of the refs are // going to resolve. return array(); } $max_commit_id = (int)$max_commit->getCommitIdentifier(); $results = array(); foreach ($this->refs as $ref) { if ($ref == 'HEAD') { // Resolve "HEAD" to mean "the most recent commit". $results[$ref][] = array( 'type' => 'commit', 'identifier' => $max_commit_id, ); continue; } if (!preg_match('/^\d+$/', $ref)) { // This ref is non-numeric, so it doesn't resolve to anything. continue; } // Resolve other commits if we can deduce their existence. // TODO: When we import only part of a repository, we won't necessarily // have all of the smaller commits. Should we fail to resolve them here // for repositories with a subpath? It might let us simplify other things // elsewhere. if ((int)$ref <= $max_commit_id) { $results[$ref][] = array( 'type' => 'commit', 'identifier' => (int)$ref, ); } } return $results; } } diff --git a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelQuery.php b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelQuery.php index 6c19f9839b..0e8472c24d 100644 --- a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelQuery.php +++ b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelQuery.php @@ -1,26 +1,43 @@ repository = $repository; return $this; } public function getRepository() { return $this->repository; } public function execute() { if (!$this->getRepository()) { throw new Exception('Call setRepository() before execute()!'); } return $this->executeQuery(); } + protected function filterRefsByType(array $refs, array $types) { + $type_map = array_fuse($types); + + foreach ($refs as $name => $ref_list) { + foreach ($ref_list as $key => $ref) { + if (empty($type_map[$ref['type']])) { + unset($refs[$name][$key]); + } + } + if (!$refs[$name]) { + unset($refs[$name]); + } + } + + return $refs; + } + } diff --git a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php index 7d8c01d890..556ddf0904 100644 --- a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php +++ b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php @@ -1,222 +1,232 @@ refs = $refs; return $this; } + public function withTypes(array $types) { + $this->types = $types; + return $this; + } + protected function executeQuery() { if (!$this->refs) { return array(); } switch ($this->getRepository()->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $result = $this->resolveGitRefs(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $result = $this->resolveMercurialRefs(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $result = $this->resolveSubversionRefs(); break; default: throw new Exception('Unsupported repository type!'); } + if ($this->types !== null) { + $result = $this->filterRefsByType($result, $this->types); + } + return $result; } private function resolveGitRefs() { $repository = $this->getRepository(); // TODO: When refs are ambiguous (for example, tags and branches with // the same name) this will only resolve one of them. $future = $repository->getLocalCommandFuture('cat-file --batch-check'); $future->write(implode("\n", $this->refs)); list($stdout) = $future->resolvex(); $lines = explode("\n", rtrim($stdout, "\n")); if (count($lines) !== count($this->refs)) { throw new Exception('Unexpected line count from `git cat-file`!'); } $hits = array(); $tags = array(); $lines = array_combine($this->refs, $lines); foreach ($lines as $ref => $line) { $parts = explode(' ', $line); if (count($parts) < 2) { throw new Exception("Failed to parse `git cat-file` output: {$line}"); } list($identifier, $type) = $parts; if ($type == 'missing') { // This is either an ambiguous reference which resolves to several // objects, or an invalid reference. For now, always treat it as // invalid. It would be nice to resolve all possibilities for // ambiguous references at some point, although the strategy for doing // so isn't clear to me. continue; } switch ($type) { case 'commit': break; case 'tag': $tags[] = $identifier; break; default: throw new Exception( "Unexpected object type from `git cat-file`: {$line}"); } $hits[] = array( 'ref' => $ref, 'type' => $type, 'identifier' => $identifier, ); } $tag_map = array(); if ($tags) { // If some of the refs were tags, just load every tag in order to figure // out which commits they map to. This might be somewhat inefficient in // repositories with a huge number of tags. $tag_refs = id(new DiffusionLowLevelGitRefQuery()) ->setRepository($repository) ->withIsTag(true) ->executeQuery(); foreach ($tag_refs as $tag_ref) { $tag_map[$tag_ref->getShortName()] = $tag_ref->getCommitIdentifier(); } } $results = array(); foreach ($hits as $hit) { $type = $hit['type']; $ref = $hit['ref']; $alternate = null; if ($type == 'tag') { $alternate = $identifier; $identifier = idx($tag_map, $ref); if (!$identifier) { throw new Exception("Failed to look up tag '{$ref}'!"); } } $result = array( 'type' => $type, 'identifier' => $identifier, ); if ($alternate !== null) { $result['alternate'] = $alternate; } $results[$ref][] = $result; } return $results; } private function resolveMercurialRefs() { $repository = $this->getRepository(); // First, pull all of the branch heads in the repository. Doing this in // bulk is much faster than querying each individual head if we're // checking even a small number of refs. $branches = id(new DiffusionLowLevelMercurialBranchesQuery()) ->setRepository($repository) ->executeQuery(); $branches = mgroup($branches, 'getShortName'); $results = array(); $unresolved = $this->refs; foreach ($unresolved as $key => $ref) { if (empty($branches[$ref])) { continue; } foreach ($branches[$ref] as $branch) { $fields = $branch->getRawFields(); $results[$ref][] = array( 'type' => 'branch', 'identifier' => $branch->getCommitIdentifier(), 'closed' => idx($fields, 'closed', false), ); } unset($unresolved[$key]); } if (!$unresolved) { return $results; } // If we still have unresolved refs (which might be things like "tip"), // try to resolve them individually. $futures = array(); foreach ($unresolved as $ref) { $futures[$ref] = $repository->getLocalCommandFuture( 'log --template=%s --rev %s', '{node}', hgsprintf('%s', $ref)); } foreach (new FutureIterator($futures) as $ref => $future) { try { list($stdout) = $future->resolvex(); } catch (CommandException $ex) { if (preg_match('/ambiguous identifier/', $ex->getStdErr())) { // This indicates that the ref ambiguously matched several things. // Eventually, it would be nice to return all of them, but it is // unclear how to best do that. For now, treat it as a miss instead. continue; } throw $ex; } // It doesn't look like we can figure out the type (commit/branch/rev) // from this output very easily. For now, just call everything a commit. $type = 'commit'; $results[$ref][] = array( 'type' => $type, 'identifier' => trim($stdout), ); } return $results; } private function resolveSubversionRefs() { // We don't have any VCS logic for Subversion, so just use the cached // query. return id(new DiffusionCachedResolveRefsQuery()) ->setRepository($this->getRepository()) ->withRefs($this->refs) ->execute(); } } diff --git a/src/applications/diffusion/request/DiffusionRequest.php b/src/applications/diffusion/request/DiffusionRequest.php index 0bd386d7ff..2af40ac930 100644 --- a/src/applications/diffusion/request/DiffusionRequest.php +++ b/src/applications/diffusion/request/DiffusionRequest.php @@ -1,834 +1,844 @@ initializeFromDictionary($data); return $object; } /** * Create a new request from an Aphront request dictionary. This is an * internal method that you generally should not call directly; instead, * call @{method:newFromDictionary}. * * @param map Map of Aphront request data. * @return DiffusionRequest New request object. * @task new */ final public static function newFromAphrontRequestDictionary( array $data, AphrontRequest $request) { $callsign = phutil_unescape_uri_path_component(idx($data, 'callsign')); $object = self::newFromCallsign($callsign, $request->getUser()); $use_branches = $object->supportsBranches(); if (isset($data['dblob'])) { $parsed = self::parseRequestBlob(idx($data, 'dblob'), $use_branches); } else { $parsed = array( 'commit' => idx($data, 'commit'), 'path' => idx($data, 'path'), 'line' => idx($data, 'line'), 'branch' => idx($data, 'branch'), ); } $object->setUser($request->getUser()); $object->initializeFromDictionary($parsed); $object->lint = $request->getStr('lint'); return $object; } /** * Internal. * * @task new */ final private function __construct() { // } /** * Internal. Use @{method:newFromDictionary}, not this method. * * @param string Repository callsign. * @param PhabricatorUser Viewing user. * @return DiffusionRequest New request object. * @task new */ final private static function newFromCallsign( $callsign, PhabricatorUser $viewer) { $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withCallsigns(array($callsign)) ->executeOne(); if (!$repository) { throw new Exception("No such repository '{$callsign}'."); } return self::newFromRepository($repository); } /** * Internal. Use @{method:newFromDictionary}, not this method. * * @param PhabricatorRepository Repository object. * @return DiffusionRequest New request object. * @task new */ final private static function newFromRepository( PhabricatorRepository $repository) { $map = array( PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'DiffusionGitRequest', PhabricatorRepositoryType::REPOSITORY_TYPE_SVN => 'DiffusionSvnRequest', PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => 'DiffusionMercurialRequest', ); $class = idx($map, $repository->getVersionControlSystem()); if (!$class) { throw new Exception('Unknown version control system!'); } $object = new $class(); $object->repository = $repository; $object->callsign = $repository->getCallsign(); return $object; } /** * Internal. Use @{method:newFromDictionary}, not this method. * * @param map Map of parsed data. * @return void * @task new */ final private function initializeFromDictionary(array $data) { $this->path = idx($data, 'path'); $this->line = idx($data, 'line'); $this->initFromConduit = idx($data, 'initFromConduit', true); $this->symbolicCommit = idx($data, 'commit'); if ($this->supportsBranches()) { $this->branch = idx($data, 'branch'); } if (!$this->getUser()) { $user = idx($data, 'user'); if (!$user) { throw new Exception( 'You must provide a PhabricatorUser in the dictionary!'); } $this->setUser($user); } $this->didInitialize(); } final public function setUser(PhabricatorUser $user) { $this->user = $user; return $this; } final public function getUser() { return $this->user; } public function getRepository() { return $this->repository; } public function getCallsign() { return $this->callsign; } public function setPath($path) { $this->path = $path; return $this; } public function getPath() { return $this->path; } public function getLine() { return $this->line; } public function getCommit() { // TODO: Probably remove all of this. if ($this->getSymbolicCommit() !== null) { return $this->getSymbolicCommit(); } return $this->getStableCommit(); } /** * Get the symbolic commit associated with this request. * * A symbolic commit may be a commit hash, an abbreviated commit hash, a * branch name, a tag name, or an expression like "HEAD^^^". The symbolic * commit may also be absent. * * This method always returns the symbol present in the original request, * in unmodified form. * * See also @{method:getStableCommit}. * * @return string|null Symbolic commit, if one was present in the request. */ public function getSymbolicCommit() { return $this->symbolicCommit; } /** * Modify the request to move the symbolic commit elsewhere. * * @param string New symbolic commit. * @return this */ public function updateSymbolicCommit($symbol) { $this->symbolicCommit = $symbol; $this->symbolicType = null; $this->stableCommit = null; return $this; } /** * Get the ref type (`commit` or `tag`) of the location associated with this * request. * * If a symbolic commit is present in the request, this method identifies * the type of the symbol. Otherwise, it identifies the type of symbol of * the location the request is implicitly associated with. This will probably * always be `commit`. * * @return string Symbolic commit type (`commit` or `tag`). */ public function getSymbolicType() { if ($this->symbolicType === null) { // As a side effect, this resolves the symbolic type. $this->getStableCommit(); } return $this->symbolicType; } /** * Retrieve the stable, permanent commit name identifying the repository * location associated with this request. * * This returns a non-symbolic identifier for the current commit: in Git and * Mercurial, a 40-character SHA1; in SVN, a revision number. * * See also @{method:getSymbolicCommit}. * * @return string Stable commit name, like a git hash or SVN revision. Not * a symbolic commit reference. */ public function getStableCommit() { if (!$this->stableCommit) { if ($this->isStableCommit($this->symbolicCommit)) { $this->stableCommit = $this->symbolicCommit; $this->symbolicType = 'commit'; } else { $this->queryStableCommit(); } } return $this->stableCommit; } public function getBranch() { return $this->branch; } public function getLint() { return $this->lint; } protected function getArcanistBranch() { return $this->getBranch(); } public function loadBranch() { // TODO: Get rid of this and do real Queries on real objects. if ($this->branchObject === false) { $this->branchObject = PhabricatorRepositoryBranch::loadBranch( $this->getRepository()->getID(), $this->getArcanistBranch()); } return $this->branchObject; } public function loadCoverage() { // TODO: This should also die. $branch = $this->loadBranch(); if (!$branch) { return; } $path = $this->getPath(); $path_map = id(new DiffusionPathIDQuery(array($path)))->loadPathIDs(); $coverage_row = queryfx_one( id(new PhabricatorRepository())->establishConnection('r'), 'SELECT * FROM %T WHERE branchID = %d AND pathID = %d ORDER BY commitID DESC LIMIT 1', 'repository_coverage', $branch->getID(), $path_map[$path]); if (!$coverage_row) { return null; } return idx($coverage_row, 'coverage'); } public function loadCommit() { if (empty($this->repositoryCommit)) { $repository = $this->getRepository(); $commit = id(new DiffusionCommitQuery()) ->setViewer($this->getUser()) ->withRepository($repository) ->withIdentifiers(array($this->getStableCommit())) ->executeOne(); if ($commit) { $commit->attachRepository($repository); } $this->repositoryCommit = $commit; } return $this->repositoryCommit; } public function loadArcanistProjects() { if (empty($this->arcanistProjects)) { $projects = id(new PhabricatorRepositoryArcanistProject())->loadAllWhere( 'repositoryID = %d', $this->getRepository()->getID()); $this->arcanistProjects = $projects; } return $this->arcanistProjects; } public function loadCommitData() { if (empty($this->repositoryCommitData)) { $commit = $this->loadCommit(); $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( 'commitID = %d', $commit->getID()); if (!$data) { $data = new PhabricatorRepositoryCommitData(); $data->setCommitMessage( '(This commit has not been fully parsed yet.)'); } $this->repositoryCommitData = $data; } return $this->repositoryCommitData; } /* -( Managing Diffusion URIs )-------------------------------------------- */ /** * Generate a Diffusion URI using this request to provide defaults. See * @{method:generateDiffusionURI} for details. This method is the same, but * preserves the request parameters if they are not overridden. * * @param map See @{method:generateDiffusionURI}. * @return PhutilURI Generated URI. * @task uri */ public function generateURI(array $params) { if (empty($params['stable'])) { $default_commit = $this->getSymbolicCommit(); } else { $default_commit = $this->getStableCommit(); } $defaults = array( 'callsign' => $this->getCallsign(), 'path' => $this->getPath(), 'branch' => $this->getBranch(), 'commit' => $default_commit, 'lint' => idx($params, 'lint', $this->getLint()), ); foreach ($defaults as $key => $val) { if (!isset($params[$key])) { // Overwrite NULL. $params[$key] = $val; } } return self::generateDiffusionURI($params); } /** * Generate a Diffusion URI from a parameter map. Applies the correct encoding * and formatting to the URI. Parameters are: * * - `action` One of `history`, `browse`, `change`, `lastmodified`, * `branch`, `tags`, `branches`, or `revision-ref`. The action specified * by the URI. * - `callsign` Repository callsign. * - `branch` Optional if action is not `branch`, branch name. * - `path` Optional, path to file. * - `commit` Optional, commit identifier. * - `line` Optional, line range. * - `lint` Optional, lint code. * - `params` Optional, query parameters. * * The function generates the specified URI and returns it. * * @param map See documentation. * @return PhutilURI Generated URI. * @task uri */ public static function generateDiffusionURI(array $params) { $action = idx($params, 'action'); $callsign = idx($params, 'callsign'); $path = idx($params, 'path'); $branch = idx($params, 'branch'); $commit = idx($params, 'commit'); $line = idx($params, 'line'); if (strlen($callsign)) { $callsign = phutil_escape_uri_path_component($callsign).'/'; } if (strlen($branch)) { $branch = phutil_escape_uri_path_component($branch).'/'; } if (strlen($path)) { $path = ltrim($path, '/'); $path = str_replace(array(';', '$'), array(';;', '$$'), $path); $path = phutil_escape_uri($path); } $path = "{$branch}{$path}"; if (strlen($commit)) { $commit = str_replace('$', '$$', $commit); $commit = ';'.phutil_escape_uri($commit); } if (strlen($line)) { $line = '$'.phutil_escape_uri($line); } $req_callsign = false; $req_branch = false; $req_commit = false; switch ($action) { case 'history': case 'browse': case 'change': case 'lastmodified': case 'tags': case 'branches': case 'lint': case 'refs': $req_callsign = true; break; case 'branch': $req_callsign = true; $req_branch = true; break; case 'commit': $req_callsign = true; $req_commit = true; break; } if ($req_callsign && !strlen($callsign)) { throw new Exception( "Diffusion URI action '{$action}' requires callsign!"); } if ($req_commit && !strlen($commit)) { throw new Exception( "Diffusion URI action '{$action}' requires commit!"); } switch ($action) { case 'change': case 'history': case 'browse': case 'lastmodified': case 'tags': case 'branches': case 'lint': case 'pathtree': case 'refs': $uri = "/diffusion/{$callsign}{$action}/{$path}{$commit}{$line}"; break; case 'branch': if (strlen($path)) { $uri = "/diffusion/{$callsign}repository/{$path}"; } else { $uri = "/diffusion/{$callsign}"; } break; case 'external': $commit = ltrim($commit, ';'); $uri = "/diffusion/external/{$commit}/"; break; case 'rendering-ref': // This isn't a real URI per se, it's passed as a query parameter to // the ajax changeset stuff but then we parse it back out as though // it came from a URI. $uri = rawurldecode("{$path}{$commit}"); break; case 'commit': $commit = ltrim($commit, ';'); $callsign = rtrim($callsign, '/'); $uri = "/r{$callsign}{$commit}"; break; default: throw new Exception("Unknown Diffusion URI action '{$action}'!"); } if ($action == 'rendering-ref') { return $uri; } $uri = new PhutilURI($uri); if (isset($params['lint'])) { $params['params'] = idx($params, 'params', array()) + array( 'lint' => $params['lint'], ); } if (idx($params, 'params')) { $uri->setQueryParams($params['params']); } return $uri; } /** * Internal. Public only for unit tests. * * Parse the request URI into components. * * @param string URI blob. * @param bool True if this VCS supports branches. * @return map Parsed URI. * * @task uri */ public static function parseRequestBlob($blob, $supports_branches) { $result = array( 'branch' => null, 'path' => null, 'commit' => null, 'line' => null, ); $matches = null; if ($supports_branches) { // Consume the front part of the URI, up to the first "/". This is the // path-component encoded branch name. if (preg_match('@^([^/]+)/@', $blob, $matches)) { $result['branch'] = phutil_unescape_uri_path_component($matches[1]); $blob = substr($blob, strlen($matches[1]) + 1); } } // Consume the back part of the URI, up to the first "$". Use a negative // lookbehind to prevent matching '$$'. We double the '$' symbol when // encoding so that files with names like "money/$100" will survive. $pattern = '@(?:(?:^|[^$])(?:[$][$])*)[$]([\d-,]+)$@'; if (preg_match($pattern, $blob, $matches)) { $result['line'] = $matches[1]; $blob = substr($blob, 0, -(strlen($matches[1]) + 1)); } // We've consumed the line number if it exists, so unescape "$" in the // rest of the string. $blob = str_replace('$$', '$', $blob); // Consume the commit name, stopping on ';;'. We allow any character to // appear in commits names, as they can sometimes be symbolic names (like // tag names or refs). if (preg_match('@(?:(?:^|[^;])(?:;;)*);([^;].*)$@', $blob, $matches)) { $result['commit'] = $matches[1]; $blob = substr($blob, 0, -(strlen($matches[1]) + 1)); } // We've consumed the commit if it exists, so unescape ";" in the rest // of the string. $blob = str_replace(';;', ';', $blob); if (strlen($blob)) { $result['path'] = $blob; } $parts = explode('/', $result['path']); foreach ($parts as $part) { // Prevent any hyjinx since we're ultimately shipping this to the // filesystem under a lot of workflows. if ($part == '..') { throw new Exception('Invalid path URI.'); } } return $result; } /** * Check that the working copy of the repository is present and readable. * * @param string Path to the working copy. */ protected function validateWorkingCopy($path) { if (!is_readable(dirname($path))) { $this->raisePermissionException(); } if (!Filesystem::pathExists($path)) { $this->raiseCloneException(); } } protected function raisePermissionException() { $host = php_uname('n'); $callsign = $this->getRepository()->getCallsign(); throw new DiffusionSetupException( "The clone of this repository ('{$callsign}') on the local machine ". "('{$host}') could not be read. Ensure that the repository is in a ". "location where the web server has read permissions."); } protected function raiseCloneException() { $host = php_uname('n'); $callsign = $this->getRepository()->getCallsign(); throw new DiffusionSetupException( "The working copy for this repository ('{$callsign}') hasn't been ". "cloned yet on this machine ('{$host}'). Make sure you've started the ". "Phabricator daemons. If this problem persists for longer than a clone ". "should take, check the daemon logs (in the Daemon Console) to see if ". "there were errors cloning the repository. Consult the 'Diffusion User ". "Guide' in the documentation for help setting up repositories."); } private function queryStableCommit() { + $types = array(); if ($this->symbolicCommit) { $ref = $this->symbolicCommit; } else { if ($this->supportsBranches()) { $ref = $this->getResolvableBranchName($this->getBranch()); + $types = array( + PhabricatorRepositoryRefCursor::TYPE_BRANCH, + ); } else { $ref = 'HEAD'; } } - $results = $this->resolveRefs(array($ref)); + $results = $this->resolveRefs(array($ref), $types); $matches = idx($results, $ref, array()); if (!$matches) { $message = pht( 'Ref "%s" does not exist in this repository.', $ref); throw id(new DiffusionRefNotFoundException($message)) ->setRef($ref); } if (count($matches) > 1) { $match = $this->chooseBestRefMatch($ref, $matches); } else { $match = head($matches); } $this->stableCommit = $match['identifier']; $this->symbolicType = $match['type']; } public function getRefAlternatives() { // Make sure we've resolved the reference into a stable commit first. $this->getStableCommit(); return $this->refAlternatives; } private function chooseBestRefMatch($ref, array $results) { // First, filter out less-desirable matches. $candidates = array(); foreach ($results as $result) { // Exclude closed heads. if ($result['type'] == 'branch') { if (idx($result, 'closed')) { continue; } } $candidates[] = $result; } // If we filtered everything, undo the filtering. if (!$candidates) { $candidates = $results; } // TODO: Do a better job of selecting the best match? $match = head($candidates); // After choosing the best alternative, save all the alternatives so the // UI can show them to the user. if (count($candidates) > 1) { $this->refAlternatives = $candidates; } return $match; } protected function getResolvableBranchName($branch) { return $branch; } - private function resolveRefs(array $refs) { + private function resolveRefs(array $refs, array $types) { // First, try to resolve refs from fast cache sources. - $cached_results = id(new DiffusionCachedResolveRefsQuery()) + $cached_query = id(new DiffusionCachedResolveRefsQuery()) ->setRepository($this->getRepository()) - ->withRefs($refs) - ->execute(); + ->withRefs($refs); + + if ($types) { + $cached_query->withTypes($types); + } + + $cached_results = $cached_query->execute(); // Throw away all the refs we resolved. Hopefully, we'll throw away // everything here. foreach ($refs as $key => $ref) { if (isset($cached_results[$ref])) { unset($refs[$key]); } } // If we couldn't pull everything out of the cache, execute the underlying // VCS operation. if ($refs) { $vcs_results = DiffusionQuery::callConduitWithDiffusionRequest( $this->getUser(), $this, 'diffusion.resolverefs', array( + 'types' => $types, 'refs' => $refs, )); } else { $vcs_results = array(); } return $vcs_results + $cached_results; } public function setIsClusterRequest($is_cluster_request) { $this->isClusterRequest = $is_cluster_request; return $this; } public function getIsClusterRequest() { return $this->isClusterRequest; } }