Path: blob/master/src/applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php
12242 views
<?php12/**3* Resolves references (like short commit names, branch names, tag names, etc.)4* into canonical, stable commit identifiers. This query works for all5* repository types.6*7* This query will always resolve refs which can be resolved, but may need to8* perform VCS operations. A faster (but less complete) counterpart query is9* available in @{class:DiffusionCachedResolveRefsQuery}; that query can10* resolve most refs without VCS operations.11*/12final class DiffusionLowLevelResolveRefsQuery13extends DiffusionLowLevelQuery {1415private $refs;16private $types;1718public function withRefs(array $refs) {19$this->refs = $refs;20return $this;21}2223public function withTypes(array $types) {24$this->types = $types;25return $this;26}2728protected function executeQuery() {29if (!$this->refs) {30return array();31}3233$repository = $this->getRepository();34if (!$repository->hasLocalWorkingCopy()) {35return array();36}3738switch ($this->getRepository()->getVersionControlSystem()) {39case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:40$result = $this->resolveGitRefs();41break;42case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:43$result = $this->resolveMercurialRefs();44break;45case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:46$result = $this->resolveSubversionRefs();47break;48default:49throw new Exception(pht('Unsupported repository type!'));50}5152if ($this->types !== null) {53$result = $this->filterRefsByType($result, $this->types);54}5556return $result;57}5859private function resolveGitRefs() {60$repository = $this->getRepository();6162$unresolved = array_fuse($this->refs);63$results = array();6465$possible_symbols = array();66foreach ($unresolved as $ref) {6768// See T13647. If this symbol is exactly 40 hex characters long, it may69// never resolve as a branch or tag name. Filter these symbols out for70// consistency with Git behavior -- and to avoid an expensive71// "git for-each-ref" when resolving only commit hashes, which happens72// during repository updates.7374if (preg_match('(^[a-f0-9]{40}\z)', $ref)) {75continue;76}7778$possible_symbols[$ref] = $ref;79}8081// First, resolve branches and tags.82if ($possible_symbols) {83$ref_map = id(new DiffusionLowLevelGitRefQuery())84->setRepository($repository)85->withRefTypes(86array(87PhabricatorRepositoryRefCursor::TYPE_BRANCH,88PhabricatorRepositoryRefCursor::TYPE_TAG,89))90->execute();91$ref_map = mgroup($ref_map, 'getShortName');9293$tag_prefix = 'refs/tags/';94foreach ($possible_symbols as $ref) {95if (empty($ref_map[$ref])) {96continue;97}9899foreach ($ref_map[$ref] as $result) {100$fields = $result->getRawFields();101$objectname = idx($fields, 'refname');102if (!strncmp($objectname, $tag_prefix, strlen($tag_prefix))) {103$type = 'tag';104} else {105$type = 'branch';106}107108$info = array(109'type' => $type,110'identifier' => $result->getCommitIdentifier(),111);112113if ($type == 'tag') {114$alternate = idx($fields, 'objectname');115if ($alternate) {116$info['alternate'] = $alternate;117}118}119120$results[$ref][] = $info;121}122123unset($unresolved[$ref]);124}125}126127// If we resolved everything, we're done.128if (!$unresolved) {129return $results;130}131132// Try to resolve anything else. This stuff either doesn't exist or is133// some ref like "HEAD^^^".134$future = $repository->getLocalCommandFuture('cat-file --batch-check');135$future->write(implode("\n", $unresolved));136list($stdout) = $future->resolvex();137138$lines = explode("\n", rtrim($stdout, "\n"));139if (count($lines) !== count($unresolved)) {140throw new Exception(141pht(142'Unexpected line count from `%s`!',143'git cat-file'));144}145146$hits = array();147$tags = array();148149$lines = array_combine($unresolved, $lines);150foreach ($lines as $ref => $line) {151$parts = explode(' ', $line);152if (count($parts) < 2) {153throw new Exception(154pht(155'Failed to parse `%s` output: %s',156'git cat-file',157$line));158}159list($identifier, $type) = $parts;160161if ($type == 'missing') {162// This is either an ambiguous reference which resolves to several163// objects, or an invalid reference. For now, always treat it as164// invalid. It would be nice to resolve all possibilities for165// ambiguous references at some point, although the strategy for doing166// so isn't clear to me.167continue;168}169170switch ($type) {171case 'commit':172break;173case 'tag':174$tags[] = $identifier;175break;176default:177throw new Exception(178pht(179'Unexpected object type from `%s`: %s',180'git cat-file',181$line));182}183184$hits[] = array(185'ref' => $ref,186'type' => $type,187'identifier' => $identifier,188);189}190191$tag_map = array();192if ($tags) {193// If some of the refs were tags, just load every tag in order to figure194// out which commits they map to. This might be somewhat inefficient in195// repositories with a huge number of tags.196$tag_refs = id(new DiffusionLowLevelGitRefQuery())197->setRepository($repository)198->withRefTypes(199array(200PhabricatorRepositoryRefCursor::TYPE_TAG,201))202->executeQuery();203foreach ($tag_refs as $tag_ref) {204$tag_map[$tag_ref->getShortName()] = $tag_ref->getCommitIdentifier();205}206}207208$results = array();209foreach ($hits as $hit) {210$type = $hit['type'];211$ref = $hit['ref'];212213$alternate = null;214if ($type == 'tag') {215$tag_identifier = idx($tag_map, $ref);216if ($tag_identifier === null) {217// This can happen when we're asked to resolve the hash of a "tag"218// object created with "git tag --annotate" that isn't currently219// reachable from any ref. Just leave things as they are.220} else {221// Otherwise, we have a normal named tag.222$alternate = $identifier;223$identifier = $tag_identifier;224}225}226227$result = array(228'type' => $type,229'identifier' => $identifier,230);231232if ($alternate !== null) {233$result['alternate'] = $alternate;234}235236$results[$ref][] = $result;237}238239return $results;240}241242private function resolveMercurialRefs() {243$repository = $this->getRepository();244245// First, pull all of the branch heads in the repository. Doing this in246// bulk is much faster than querying each individual head if we're247// checking even a small number of refs.248$branches = id(new DiffusionLowLevelMercurialBranchesQuery())249->setRepository($repository)250->executeQuery();251252$branches = mgroup($branches, 'getShortName');253254$results = array();255$unresolved = $this->refs;256foreach ($unresolved as $key => $ref) {257if (empty($branches[$ref])) {258continue;259}260261foreach ($branches[$ref] as $branch) {262$fields = $branch->getRawFields();263264$results[$ref][] = array(265'type' => 'branch',266'identifier' => $branch->getCommitIdentifier(),267'closed' => idx($fields, 'closed', false),268);269}270271unset($unresolved[$key]);272}273274if (!$unresolved) {275return $results;276}277278// If some of the refs look like hashes, try to bulk resolve them. This279// workflow happens via RefEngine and bulk resolution is dramatically280// faster than individual resolution. See PHI158.281282$hashlike = array();283foreach ($unresolved as $key => $ref) {284if (preg_match('/^[a-f0-9]{40}\z/', $ref)) {285$hashlike[$key] = $ref;286}287}288289if (count($hashlike) > 1) {290$hashlike_map = array();291292$hashlike_groups = array_chunk($hashlike, 64, true);293foreach ($hashlike_groups as $hashlike_group) {294$hashlike_arg = array();295foreach ($hashlike_group as $hashlike_ref) {296$hashlike_arg[] = hgsprintf('%s', $hashlike_ref);297}298$hashlike_arg = '('.implode(' or ', $hashlike_arg).')';299300list($err, $refs) = $repository->execLocalCommand(301'log --template=%s --rev %s',302'{node}\n',303$hashlike_arg);304if ($err) {305// NOTE: If any ref fails to resolve, Mercurial will exit with an306// error. We just give up on the whole group and resolve it307// individually below. In theory, we could split it into subgroups308// but the pathway where this bulk resolution matters rarely tries309// to resolve missing refs (see PHI158).310continue;311}312313$refs = phutil_split_lines($refs, false);314315foreach ($refs as $ref) {316$hashlike_map[$ref] = true;317}318}319320foreach ($unresolved as $key => $ref) {321if (!isset($hashlike_map[$ref])) {322continue;323}324325$results[$ref][] = array(326'type' => 'commit',327'identifier' => $ref,328);329330unset($unresolved[$key]);331}332}333334if (!$unresolved) {335return $results;336}337338// If we still have unresolved refs (which might be things like "tip"),339// try to resolve them individually.340341$futures = array();342foreach ($unresolved as $ref) {343$futures[$ref] = $repository->getLocalCommandFuture(344'log --template=%s --rev %s',345'{node}',346hgsprintf('%s', $ref));347}348349foreach (new FutureIterator($futures) as $ref => $future) {350try {351list($stdout) = $future->resolvex();352} catch (CommandException $ex) {353if (preg_match('/ambiguous identifier/', $ex->getStderr())) {354// This indicates that the ref ambiguously matched several things.355// Eventually, it would be nice to return all of them, but it is356// unclear how to best do that. For now, treat it as a miss instead.357continue;358}359if (preg_match('/unknown revision/', $ex->getStderr())) {360// No matches for this ref.361continue;362}363throw $ex;364}365366// It doesn't look like we can figure out the type (commit/branch/rev)367// from this output very easily. For now, just call everything a commit.368$type = 'commit';369370$results[$ref][] = array(371'type' => $type,372'identifier' => trim($stdout),373);374}375376return $results;377}378379private function resolveSubversionRefs() {380// We don't have any VCS logic for Subversion, so just use the cached381// query.382return id(new DiffusionCachedResolveRefsQuery())383->setRepository($this->getRepository())384->withRefs($this->refs)385->execute();386}387388}389390391