Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php
12242 views
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
*/
13
final 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`!',
144
'git cat-file'));
145
}
146
147
$hits = array();
148
$tags = array();
149
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));
159
}
160
list($identifier, $type) = $parts;
161
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;
169
}
170
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));
183
}
184
185
$hits[] = array(
186
'ref' => $ref,
187
'type' => $type,
188
'identifier' => $identifier,
189
);
190
}
191
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,
202
))
203
->executeQuery();
204
foreach ($tag_refs as $tag_ref) {
205
$tag_map[$tag_ref->getShortName()] = $tag_ref->getCommitIdentifier();
206
}
207
}
208
209
$results = array();
210
foreach ($hits as $hit) {
211
$type = $hit['type'];
212
$ref = $hit['ref'];
213
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;
225
}
226
}
227
228
$result = array(
229
'type' => $type,
230
'identifier' => $identifier,
231
);
232
233
if ($alternate !== null) {
234
$result['alternate'] = $alternate;
235
}
236
237
$results[$ref][] = $result;
238
}
239
240
return $results;
241
}
242
243
private function resolveMercurialRefs() {
244
$repository = $this->getRepository();
245
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();
252
253
$branches = mgroup($branches, 'getShortName');
254
255
$results = array();
256
$unresolved = $this->refs;
257
foreach ($unresolved as $key => $ref) {
258
if (empty($branches[$ref])) {
259
continue;
260
}
261
262
foreach ($branches[$ref] as $branch) {
263
$fields = $branch->getRawFields();
264
265
$results[$ref][] = array(
266
'type' => 'branch',
267
'identifier' => $branch->getCommitIdentifier(),
268
'closed' => idx($fields, 'closed', false),
269
);
270
}
271
272
unset($unresolved[$key]);
273
}
274
275
if (!$unresolved) {
276
return $results;
277
}
278
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.
282
283
$hashlike = array();
284
foreach ($unresolved as $key => $ref) {
285
if (preg_match('/^[a-f0-9]{40}\z/', $ref)) {
286
$hashlike[$key] = $ref;
287
}
288
}
289
290
if (count($hashlike) > 1) {
291
$hashlike_map = array();
292
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);
298
}
299
$hashlike_arg = '('.implode(' or ', $hashlike_arg).')';
300
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;
312
}
313
314
$refs = phutil_split_lines($refs, false);
315
316
foreach ($refs as $ref) {
317
$hashlike_map[$ref] = true;
318
}
319
}
320
321
foreach ($unresolved as $key => $ref) {
322
if (!isset($hashlike_map[$ref])) {
323
continue;
324
}
325
326
$results[$ref][] = array(
327
'type' => 'commit',
328
'identifier' => $ref,
329
);
330
331
unset($unresolved[$key]);
332
}
333
}
334
335
if (!$unresolved) {
336
return $results;
337
}
338
339
// If we still have unresolved refs (which might be things like "tip"),
340
// try to resolve them individually.
341
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));
348
}
349
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;
359
}
360
if (preg_match('/unknown revision/', $ex->getStderr())) {
361
// No matches for this ref.
362
continue;
363
}
364
throw $ex;
365
}
366
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';
370
371
$results[$ref][] = array(
372
'type' => $type,
373
'identifier' => trim($stdout),
374
);
375
}
376
377
return $results;
378
}
379
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();
387
}
388
389
}
390
391