@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
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.
7 *
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.
12 */
13final class DiffusionLowLevelResolveRefsQuery
14 extends DiffusionLowLevelQuery {
15
16 private $refs;
17 private $types;
18
19 public function withRefs(array $refs) {
20 $this->refs = $refs;
21 return $this;
22 }
23
24 public function withTypes(array $types) {
25 $this->types = $types;
26 return $this;
27 }
28
29 protected function executeQuery() {
30 if (!$this->refs) {
31 return array();
32 }
33
34 $repository = $this->getRepository();
35 if (!$repository->hasLocalWorkingCopy()) {
36 return array();
37 }
38
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!'));
51 }
52
53 if ($this->types !== null) {
54 $result = $this->filterRefsByType($result, $this->types);
55 }
56
57 return $result;
58 }
59
60 private function resolveGitRefs() {
61 $repository = $this->getRepository();
62
63 $unresolved = array_fuse($this->refs);
64 $results = array();
65
66 $possible_symbols = array();
67 foreach ($unresolved as $ref) {
68
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.
74
75 if (preg_match('(^[a-f0-9]{40}\z)', $ref)) {
76 continue;
77 }
78
79 $possible_symbols[$ref] = $ref;
80 }
81
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,
90 ))
91 ->execute();
92 $ref_map = mgroup($ref_map, 'getShortName');
93
94 $tag_prefix = 'refs/tags/';
95 foreach ($possible_symbols as $ref) {
96 if (empty($ref_map[$ref])) {
97 continue;
98 }
99
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';
107 }
108
109 $info = array(
110 'type' => $type,
111 'identifier' => $result->getCommitIdentifier(),
112 );
113
114 if ($type == 'tag') {
115 $alternate = idx($fields, 'objectname');
116 if ($alternate) {
117 $info['alternate'] = $alternate;
118 }
119 }
120
121 $results[$ref][] = $info;
122 }
123
124 unset($unresolved[$ref]);
125 }
126 }
127
128 // If we resolved everything, we're done.
129 if (!$unresolved) {
130 return $results;
131 }
132
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();
138
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` in %s!',
144 'git cat-file',
145 $repository->getMonogram()));
146 }
147
148 $hits = array();
149 $tags = array();
150
151 $lines = array_combine($unresolved, $lines);
152 foreach ($lines as $ref => $line) {
153 $parts = explode(' ', $line);
154 if (count($parts) < 2) {
155 throw new Exception(
156 pht(
157 'Failed to parse `%s` output in %s: %s',
158 'git cat-file',
159 $repository->getMonogram(),
160 $line));
161 }
162 list($identifier, $type) = $parts;
163
164 if ($type == 'missing') {
165 // This is either an ambiguous reference which resolves to several
166 // objects, or an invalid reference. For now, always treat it as
167 // invalid. It would be nice to resolve all possibilities for
168 // ambiguous references at some point, although the strategy for doing
169 // so isn't clear to me.
170 continue;
171 }
172
173 switch ($type) {
174 case 'commit':
175 break;
176 case 'tag':
177 $tags[] = $identifier;
178 break;
179 default:
180 throw new Exception(
181 pht(
182 'Unexpected object type from `%s` in %s: %s',
183 'git cat-file',
184 $repository->getMonogram(),
185 $line));
186 }
187
188 $hits[] = array(
189 'ref' => $ref,
190 'type' => $type,
191 'identifier' => $identifier,
192 );
193 }
194
195 $tag_map = array();
196 if ($tags) {
197 // If some of the refs were tags, just load every tag in order to figure
198 // out which commits they map to. This might be somewhat inefficient in
199 // repositories with a huge number of tags.
200 $tag_refs = id(new DiffusionLowLevelGitRefQuery())
201 ->setRepository($repository)
202 ->withRefTypes(
203 array(
204 PhabricatorRepositoryRefCursor::TYPE_TAG,
205 ))
206 ->executeQuery();
207 foreach ($tag_refs as $tag_ref) {
208 $tag_map[$tag_ref->getShortName()] = $tag_ref->getCommitIdentifier();
209 }
210 }
211
212 $results = array();
213 foreach ($hits as $hit) {
214 $type = $hit['type'];
215 $ref = $hit['ref'];
216
217 $alternate = null;
218 if ($type == 'tag') {
219 $tag_identifier = idx($tag_map, $ref);
220 if ($tag_identifier === null) {
221 // This can happen when we're asked to resolve the hash of a "tag"
222 // object created with "git tag --annotate" that isn't currently
223 // reachable from any ref. Just leave things as they are.
224 } else {
225 // Otherwise, we have a normal named tag.
226 $alternate = $identifier;
227 $identifier = $tag_identifier;
228 }
229 }
230
231 $result = array(
232 'type' => $type,
233 'identifier' => $identifier,
234 );
235
236 if ($alternate !== null) {
237 $result['alternate'] = $alternate;
238 }
239
240 $results[$ref][] = $result;
241 }
242
243 return $results;
244 }
245
246 private function resolveMercurialRefs() {
247 $repository = $this->getRepository();
248
249 // First, pull all of the branch heads in the repository. Doing this in
250 // bulk is much faster than querying each individual head if we're
251 // checking even a small number of refs.
252 $branches = id(new DiffusionLowLevelMercurialBranchesQuery())
253 ->setRepository($repository)
254 ->executeQuery();
255
256 $branches = mgroup($branches, 'getShortName');
257
258 $results = array();
259 $unresolved = $this->refs;
260 foreach ($unresolved as $key => $ref) {
261 if (empty($branches[$ref])) {
262 continue;
263 }
264
265 foreach ($branches[$ref] as $branch) {
266 $fields = $branch->getRawFields();
267
268 $results[$ref][] = array(
269 'type' => 'branch',
270 'identifier' => $branch->getCommitIdentifier(),
271 'closed' => idx($fields, 'closed', false),
272 );
273 }
274
275 unset($unresolved[$key]);
276 }
277
278 if (!$unresolved) {
279 return $results;
280 }
281
282 // If some of the refs look like hashes, try to bulk resolve them. This
283 // workflow happens via RefEngine and bulk resolution is dramatically
284 // faster than individual resolution. See PHI158.
285
286 $hashlike = array();
287 foreach ($unresolved as $key => $ref) {
288 if (preg_match('/^[a-f0-9]{40}\z/', $ref)) {
289 $hashlike[$key] = $ref;
290 }
291 }
292
293 if (count($hashlike) > 1) {
294 $hashlike_map = array();
295
296 $hashlike_groups = array_chunk($hashlike, 64, true);
297 foreach ($hashlike_groups as $hashlike_group) {
298 $hashlike_arg = array();
299 foreach ($hashlike_group as $hashlike_ref) {
300 $hashlike_arg[] = hgsprintf('%s', $hashlike_ref);
301 }
302 $hashlike_arg = '('.implode(' or ', $hashlike_arg).')';
303
304 list($err, $refs) = $repository->execLocalCommand(
305 'log --template=%s --rev %s',
306 '{node}\n',
307 $hashlike_arg);
308 if ($err) {
309 // NOTE: If any ref fails to resolve, Mercurial will exit with an
310 // error. We just give up on the whole group and resolve it
311 // individually below. In theory, we could split it into subgroups
312 // but the pathway where this bulk resolution matters rarely tries
313 // to resolve missing refs (see PHI158).
314 continue;
315 }
316
317 $refs = phutil_split_lines($refs, false);
318
319 foreach ($refs as $ref) {
320 $hashlike_map[$ref] = true;
321 }
322 }
323
324 foreach ($unresolved as $key => $ref) {
325 if (!isset($hashlike_map[$ref])) {
326 continue;
327 }
328
329 $results[$ref][] = array(
330 'type' => 'commit',
331 'identifier' => $ref,
332 );
333
334 unset($unresolved[$key]);
335 }
336 }
337
338 if (!$unresolved) {
339 return $results;
340 }
341
342 // If we still have unresolved refs (which might be things like "tip"),
343 // try to resolve them individually.
344
345 $futures = array();
346 foreach ($unresolved as $ref) {
347 $futures[$ref] = $repository->getLocalCommandFuture(
348 'log --template=%s --rev %s',
349 '{node}',
350 hgsprintf('%s', $ref));
351 }
352
353 foreach (new FutureIterator($futures) as $ref => $future) {
354 try {
355 list($stdout) = $future->resolvex();
356 } catch (CommandException $ex) {
357 if (preg_match('/ambiguous identifier/', $ex->getStderr())) {
358 // This indicates that the ref ambiguously matched several things.
359 // Eventually, it would be nice to return all of them, but it is
360 // unclear how to best do that. For now, treat it as a miss instead.
361 continue;
362 }
363 if (preg_match('/unknown revision/', $ex->getStderr())) {
364 // No matches for this ref.
365 continue;
366 }
367 throw $ex;
368 }
369
370 // It doesn't look like we can figure out the type (commit/branch/rev)
371 // from this output very easily. For now, just call everything a commit.
372 $type = 'commit';
373
374 $results[$ref][] = array(
375 'type' => $type,
376 'identifier' => trim($stdout),
377 );
378 }
379
380 return $results;
381 }
382
383 private function resolveSubversionRefs() {
384 // We don't have any VCS logic for Subversion, so just use the cached
385 // query.
386 return id(new DiffusionCachedResolveRefsQuery())
387 ->setRepository($this->getRepository())
388 ->withRefs($this->refs)
389 ->execute();
390 }
391
392}