Correct a parameter order swap in "diffusion.historyquery" for Mercurial
[phabricator.git] / src / applications / diffusion / query / lowlevel / DiffusionLowLevelResolveRefsQuery.php
blob0bbba1dc181a9ca1d47f351b7af002302bf07133
1 <?php
3 /**
4 * Resolves references (like short commit names, branch names, tag names, etc.)
5 * into canonical, stable commit identifiers. This query works for all
6 * repository types.
8 * This query will always resolve refs which can be resolved, but may need to
9 * perform VCS operations. A faster (but less complete) counterpart query is
10 * available in @{class:DiffusionCachedResolveRefsQuery}; that query can
11 * resolve most refs without VCS operations.
13 final class DiffusionLowLevelResolveRefsQuery
14 extends DiffusionLowLevelQuery {
16 private $refs;
17 private $types;
19 public function withRefs(array $refs) {
20 $this->refs = $refs;
21 return $this;
24 public function withTypes(array $types) {
25 $this->types = $types;
26 return $this;
29 protected function executeQuery() {
30 if (!$this->refs) {
31 return array();
34 $repository = $this->getRepository();
35 if (!$repository->hasLocalWorkingCopy()) {
36 return array();
39 switch ($this->getRepository()->getVersionControlSystem()) {
40 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
41 $result = $this->resolveGitRefs();
42 break;
43 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
44 $result = $this->resolveMercurialRefs();
45 break;
46 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
47 $result = $this->resolveSubversionRefs();
48 break;
49 default:
50 throw new Exception(pht('Unsupported repository type!'));
53 if ($this->types !== null) {
54 $result = $this->filterRefsByType($result, $this->types);
57 return $result;
60 private function resolveGitRefs() {
61 $repository = $this->getRepository();
63 $unresolved = array_fuse($this->refs);
64 $results = array();
66 $possible_symbols = array();
67 foreach ($unresolved as $ref) {
69 // See T13647. If this symbol is exactly 40 hex characters long, it may
70 // never resolve as a branch or tag name. Filter these symbols out for
71 // consistency with Git behavior -- and to avoid an expensive
72 // "git for-each-ref" when resolving only commit hashes, which happens
73 // during repository updates.
75 if (preg_match('(^[a-f0-9]{40}\z)', $ref)) {
76 continue;
79 $possible_symbols[$ref] = $ref;
82 // First, resolve branches and tags.
83 if ($possible_symbols) {
84 $ref_map = id(new DiffusionLowLevelGitRefQuery())
85 ->setRepository($repository)
86 ->withRefTypes(
87 array(
88 PhabricatorRepositoryRefCursor::TYPE_BRANCH,
89 PhabricatorRepositoryRefCursor::TYPE_TAG,
91 ->execute();
92 $ref_map = mgroup($ref_map, 'getShortName');
94 $tag_prefix = 'refs/tags/';
95 foreach ($possible_symbols as $ref) {
96 if (empty($ref_map[$ref])) {
97 continue;
100 foreach ($ref_map[$ref] as $result) {
101 $fields = $result->getRawFields();
102 $objectname = idx($fields, 'refname');
103 if (!strncmp($objectname, $tag_prefix, strlen($tag_prefix))) {
104 $type = 'tag';
105 } else {
106 $type = 'branch';
109 $info = array(
110 'type' => $type,
111 'identifier' => $result->getCommitIdentifier(),
114 if ($type == 'tag') {
115 $alternate = idx($fields, 'objectname');
116 if ($alternate) {
117 $info['alternate'] = $alternate;
121 $results[$ref][] = $info;
124 unset($unresolved[$ref]);
128 // If we resolved everything, we're done.
129 if (!$unresolved) {
130 return $results;
133 // Try to resolve anything else. This stuff either doesn't exist or is
134 // some ref like "HEAD^^^".
135 $future = $repository->getLocalCommandFuture('cat-file --batch-check');
136 $future->write(implode("\n", $unresolved));
137 list($stdout) = $future->resolvex();
139 $lines = explode("\n", rtrim($stdout, "\n"));
140 if (count($lines) !== count($unresolved)) {
141 throw new Exception(
142 pht(
143 'Unexpected line count from `%s`!',
144 'git cat-file'));
147 $hits = array();
148 $tags = array();
150 $lines = array_combine($unresolved, $lines);
151 foreach ($lines as $ref => $line) {
152 $parts = explode(' ', $line);
153 if (count($parts) < 2) {
154 throw new Exception(
155 pht(
156 'Failed to parse `%s` output: %s',
157 'git cat-file',
158 $line));
160 list($identifier, $type) = $parts;
162 if ($type == 'missing') {
163 // This is either an ambiguous reference which resolves to several
164 // objects, or an invalid reference. For now, always treat it as
165 // invalid. It would be nice to resolve all possibilities for
166 // ambiguous references at some point, although the strategy for doing
167 // so isn't clear to me.
168 continue;
171 switch ($type) {
172 case 'commit':
173 break;
174 case 'tag':
175 $tags[] = $identifier;
176 break;
177 default:
178 throw new Exception(
179 pht(
180 'Unexpected object type from `%s`: %s',
181 'git cat-file',
182 $line));
185 $hits[] = array(
186 'ref' => $ref,
187 'type' => $type,
188 'identifier' => $identifier,
192 $tag_map = array();
193 if ($tags) {
194 // If some of the refs were tags, just load every tag in order to figure
195 // out which commits they map to. This might be somewhat inefficient in
196 // repositories with a huge number of tags.
197 $tag_refs = id(new DiffusionLowLevelGitRefQuery())
198 ->setRepository($repository)
199 ->withRefTypes(
200 array(
201 PhabricatorRepositoryRefCursor::TYPE_TAG,
203 ->executeQuery();
204 foreach ($tag_refs as $tag_ref) {
205 $tag_map[$tag_ref->getShortName()] = $tag_ref->getCommitIdentifier();
209 $results = array();
210 foreach ($hits as $hit) {
211 $type = $hit['type'];
212 $ref = $hit['ref'];
214 $alternate = null;
215 if ($type == 'tag') {
216 $tag_identifier = idx($tag_map, $ref);
217 if ($tag_identifier === null) {
218 // This can happen when we're asked to resolve the hash of a "tag"
219 // object created with "git tag --annotate" that isn't currently
220 // reachable from any ref. Just leave things as they are.
221 } else {
222 // Otherwise, we have a normal named tag.
223 $alternate = $identifier;
224 $identifier = $tag_identifier;
228 $result = array(
229 'type' => $type,
230 'identifier' => $identifier,
233 if ($alternate !== null) {
234 $result['alternate'] = $alternate;
237 $results[$ref][] = $result;
240 return $results;
243 private function resolveMercurialRefs() {
244 $repository = $this->getRepository();
246 // First, pull all of the branch heads in the repository. Doing this in
247 // bulk is much faster than querying each individual head if we're
248 // checking even a small number of refs.
249 $branches = id(new DiffusionLowLevelMercurialBranchesQuery())
250 ->setRepository($repository)
251 ->executeQuery();
253 $branches = mgroup($branches, 'getShortName');
255 $results = array();
256 $unresolved = $this->refs;
257 foreach ($unresolved as $key => $ref) {
258 if (empty($branches[$ref])) {
259 continue;
262 foreach ($branches[$ref] as $branch) {
263 $fields = $branch->getRawFields();
265 $results[$ref][] = array(
266 'type' => 'branch',
267 'identifier' => $branch->getCommitIdentifier(),
268 'closed' => idx($fields, 'closed', false),
272 unset($unresolved[$key]);
275 if (!$unresolved) {
276 return $results;
279 // If some of the refs look like hashes, try to bulk resolve them. This
280 // workflow happens via RefEngine and bulk resolution is dramatically
281 // faster than individual resolution. See PHI158.
283 $hashlike = array();
284 foreach ($unresolved as $key => $ref) {
285 if (preg_match('/^[a-f0-9]{40}\z/', $ref)) {
286 $hashlike[$key] = $ref;
290 if (count($hashlike) > 1) {
291 $hashlike_map = array();
293 $hashlike_groups = array_chunk($hashlike, 64, true);
294 foreach ($hashlike_groups as $hashlike_group) {
295 $hashlike_arg = array();
296 foreach ($hashlike_group as $hashlike_ref) {
297 $hashlike_arg[] = hgsprintf('%s', $hashlike_ref);
299 $hashlike_arg = '('.implode(' or ', $hashlike_arg).')';
301 list($err, $refs) = $repository->execLocalCommand(
302 'log --template=%s --rev %s',
303 '{node}\n',
304 $hashlike_arg);
305 if ($err) {
306 // NOTE: If any ref fails to resolve, Mercurial will exit with an
307 // error. We just give up on the whole group and resolve it
308 // individually below. In theory, we could split it into subgroups
309 // but the pathway where this bulk resolution matters rarely tries
310 // to resolve missing refs (see PHI158).
311 continue;
314 $refs = phutil_split_lines($refs, false);
316 foreach ($refs as $ref) {
317 $hashlike_map[$ref] = true;
321 foreach ($unresolved as $key => $ref) {
322 if (!isset($hashlike_map[$ref])) {
323 continue;
326 $results[$ref][] = array(
327 'type' => 'commit',
328 'identifier' => $ref,
331 unset($unresolved[$key]);
335 if (!$unresolved) {
336 return $results;
339 // If we still have unresolved refs (which might be things like "tip"),
340 // try to resolve them individually.
342 $futures = array();
343 foreach ($unresolved as $ref) {
344 $futures[$ref] = $repository->getLocalCommandFuture(
345 'log --template=%s --rev %s',
346 '{node}',
347 hgsprintf('%s', $ref));
350 foreach (new FutureIterator($futures) as $ref => $future) {
351 try {
352 list($stdout) = $future->resolvex();
353 } catch (CommandException $ex) {
354 if (preg_match('/ambiguous identifier/', $ex->getStderr())) {
355 // This indicates that the ref ambiguously matched several things.
356 // Eventually, it would be nice to return all of them, but it is
357 // unclear how to best do that. For now, treat it as a miss instead.
358 continue;
360 if (preg_match('/unknown revision/', $ex->getStderr())) {
361 // No matches for this ref.
362 continue;
364 throw $ex;
367 // It doesn't look like we can figure out the type (commit/branch/rev)
368 // from this output very easily. For now, just call everything a commit.
369 $type = 'commit';
371 $results[$ref][] = array(
372 'type' => $type,
373 'identifier' => trim($stdout),
377 return $results;
380 private function resolveSubversionRefs() {
381 // We don't have any VCS logic for Subversion, so just use the cached
382 // query.
383 return id(new DiffusionCachedResolveRefsQuery())
384 ->setRepository($this->getRepository())
385 ->withRefs($this->refs)
386 ->execute();