Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php
12242 views
1
<?php
2
3
final class DiffusionBrowseQueryConduitAPIMethod
4
extends DiffusionQueryConduitAPIMethod {
5
6
public function getAPIMethodName() {
7
return 'diffusion.browsequery';
8
}
9
10
public function getMethodDescription() {
11
return pht(
12
'File(s) information for a repository at an (optional) path and '.
13
'(optional) commit.');
14
}
15
16
protected function defineReturnType() {
17
return 'array';
18
}
19
20
protected function defineCustomParamTypes() {
21
return array(
22
'path' => 'optional string',
23
'commit' => 'optional string',
24
'needValidityOnly' => 'optional bool',
25
'limit' => 'optional int',
26
'offset' => 'optional int',
27
);
28
}
29
30
protected function getResult(ConduitAPIRequest $request) {
31
$result = parent::getResult($request);
32
return $result->toDictionary();
33
}
34
35
protected function getGitResult(ConduitAPIRequest $request) {
36
$drequest = $this->getDiffusionRequest();
37
$repository = $drequest->getRepository();
38
39
$path = $request->getValue('path');
40
if ($path === null || !strlen($path) || $path === '/') {
41
$path = null;
42
}
43
44
$commit = $request->getValue('commit');
45
$offset = (int)$request->getValue('offset');
46
$limit = (int)$request->getValue('limit');
47
$result = $this->getEmptyResultSet();
48
49
if ($path === null) {
50
// Fast path to improve the performance of the repository view; we know
51
// the root is always a tree at any commit and always exists.
52
$path_type = 'tree';
53
} else {
54
try {
55
list($stdout) = $repository->execxLocalCommand(
56
'cat-file -t -- %s',
57
sprintf('%s:%s', $commit, $path));
58
$path_type = trim($stdout);
59
} catch (CommandException $e) {
60
// The "cat-file" command may fail if the path legitimately does not
61
// exist, but it may also fail if the path is a submodule. This can
62
// produce either "Not a valid object name" or "could not get object
63
// info".
64
65
// To detect if we have a submodule, use `git ls-tree`. If the path
66
// is a submodule, we'll get a "160000" mode mask with type "commit".
67
68
list($sub_err, $sub_stdout) = $repository->execLocalCommand(
69
'ls-tree %s -- %s',
70
gitsprintf('%s', $commit),
71
$path);
72
if (!$sub_err) {
73
// If the path failed "cat-file" but "ls-tree" worked, we assume it
74
// must be a submodule. If it is, the output will look something
75
// like this:
76
//
77
// 160000 commit <hash> <path>
78
//
79
// We make sure it has the 160000 mode mask to confirm that it's
80
// definitely a submodule.
81
$mode = (int)$sub_stdout;
82
if ($mode & 160000) {
83
$submodule_reason = DiffusionBrowseResultSet::REASON_IS_SUBMODULE;
84
$result
85
->setReasonForEmptyResultSet($submodule_reason);
86
return $result;
87
}
88
}
89
90
$stderr = $e->getStderr();
91
if (preg_match('/^fatal: Not a valid object name/', $stderr)) {
92
// Grab two logs, since the first one is when the object was deleted.
93
list($stdout) = $repository->execxLocalCommand(
94
'log -n2 %s %s -- %s',
95
'--format=%H',
96
gitsprintf('%s', $commit),
97
$path);
98
$stdout = trim($stdout);
99
if ($stdout) {
100
$commits = explode("\n", $stdout);
101
$result
102
->setReasonForEmptyResultSet(
103
DiffusionBrowseResultSet::REASON_IS_DELETED)
104
->setDeletedAtCommit(idx($commits, 0))
105
->setExistedAtCommit(idx($commits, 1));
106
return $result;
107
}
108
109
$result->setReasonForEmptyResultSet(
110
DiffusionBrowseResultSet::REASON_IS_NONEXISTENT);
111
return $result;
112
} else {
113
throw $e;
114
}
115
}
116
}
117
118
if ($path_type === 'blob') {
119
$result->setReasonForEmptyResultSet(
120
DiffusionBrowseResultSet::REASON_IS_FILE);
121
return $result;
122
}
123
124
$result->setIsValidResults(true);
125
if ($this->shouldOnlyTestValidity($request)) {
126
return $result;
127
}
128
129
if ($path === null) {
130
list($stdout) = $repository->execxLocalCommand(
131
'ls-tree -z -l %s --',
132
gitsprintf('%s', $commit));
133
} else {
134
if ($path_type === 'tree') {
135
$path = rtrim($path, '/').'/';
136
} else {
137
$path = rtrim($path, '/');
138
}
139
140
list($stdout) = $repository->execxLocalCommand(
141
'ls-tree -z -l %s -- %s',
142
gitsprintf('%s', $commit),
143
$path);
144
}
145
146
$submodules = array();
147
148
$count = 0;
149
$results = array();
150
$lines = empty($stdout)
151
? array()
152
: explode("\0", rtrim($stdout));
153
154
foreach ($lines as $line) {
155
// NOTE: Limit to 5 components so we parse filenames with spaces in them
156
// correctly.
157
// NOTE: The output uses a mixture of tabs and one-or-more spaces to
158
// delimit fields.
159
$parts = preg_split('/\s+/', $line, 5);
160
if (count($parts) < 5) {
161
throw new Exception(
162
pht(
163
'Expected "<mode> <type> <hash> <size>\t<name>", for ls-tree of '.
164
'"%s:%s", got: %s',
165
$commit,
166
$path,
167
$line));
168
}
169
170
list($mode, $type, $hash, $size, $full_path) = $parts;
171
172
$path_result = new DiffusionRepositoryPath();
173
174
if ($type == 'tree') {
175
$file_type = DifferentialChangeType::FILE_DIRECTORY;
176
} else if ($type == 'commit') {
177
$file_type = DifferentialChangeType::FILE_SUBMODULE;
178
$submodules[] = $path_result;
179
} else {
180
$mode = intval($mode, 8);
181
if (($mode & 0120000) == 0120000) {
182
$file_type = DifferentialChangeType::FILE_SYMLINK;
183
} else {
184
$file_type = DifferentialChangeType::FILE_NORMAL;
185
}
186
}
187
188
if ($path === null) {
189
$local_path = $full_path;
190
} else {
191
$local_path = basename($full_path);
192
}
193
194
$path_result->setFullPath($full_path);
195
$path_result->setPath($local_path);
196
$path_result->setHash($hash);
197
$path_result->setFileType($file_type);
198
$path_result->setFileSize($size);
199
200
if ($count >= $offset) {
201
$results[] = $path_result;
202
}
203
204
$count++;
205
206
if ($limit && $count >= ($offset + $limit)) {
207
break;
208
}
209
}
210
211
// If we identified submodules, lookup the module info at this commit to
212
// find their source URIs.
213
214
if ($submodules) {
215
216
// NOTE: We need to read the file out of git and write it to a temporary
217
// location because "git config -f" doesn't accept a "commit:path"-style
218
// argument.
219
220
// NOTE: This file may not exist, e.g. because the commit author removed
221
// it when they added the submodule. See T1448. If it's not present, just
222
// show the submodule without enriching it. If ".gitmodules" was removed
223
// it seems to partially break submodules, but the repository as a whole
224
// continues to work fine and we've seen at least two cases of this in
225
// the wild.
226
227
list($err, $contents) = $repository->execLocalCommand(
228
'cat-file blob -- %s:.gitmodules',
229
$commit);
230
231
if (!$err) {
232
233
// NOTE: After T13673, the user executing "git" may not be the same
234
// as the user this process is running as (usually the webserver user),
235
// so we can't reliably use a temporary file: the daemon user may not
236
// be able to use it.
237
238
// Use "--file -" to read from stdin instead. If this fails in some
239
// older versions of Git, we could exempt this particular command from
240
// sudoing to the daemon user.
241
242
$future = $repository->getLocalCommandFuture('config -l --file - --');
243
$future->write($contents);
244
list($module_info) = $future->resolvex();
245
246
$dict = array();
247
$lines = explode("\n", trim($module_info));
248
foreach ($lines as $line) {
249
list($key, $value) = explode('=', $line, 2);
250
$parts = explode('.', $key);
251
$dict[$key] = $value;
252
}
253
254
foreach ($submodules as $submodule_path) {
255
$full_path = $submodule_path->getFullPath();
256
$key = 'submodule.'.$full_path.'.url';
257
if (isset($dict[$key])) {
258
$submodule_path->setExternalURI($dict[$key]);
259
}
260
}
261
}
262
}
263
264
return $result->setPaths($results);
265
}
266
267
protected function getMercurialResult(ConduitAPIRequest $request) {
268
$drequest = $this->getDiffusionRequest();
269
$repository = $drequest->getRepository();
270
$path = $request->getValue('path');
271
$commit = $request->getValue('commit');
272
$offset = (int)$request->getValue('offset');
273
$limit = (int)$request->getValue('limit');
274
$result = $this->getEmptyResultSet();
275
276
277
$entire_manifest = id(new DiffusionLowLevelMercurialPathsQuery())
278
->setRepository($repository)
279
->withCommit($commit)
280
->withPath($path)
281
->execute();
282
283
$results = array();
284
285
if ($path !== null) {
286
$match_against = trim($path, '/');
287
$match_len = strlen($match_against);
288
} else {
289
$match_against = '';
290
$match_len = 0;
291
}
292
293
// For the root, don't trim. For other paths, trim the "/" after we match.
294
// We need this because Mercurial's canonical paths have no leading "/",
295
// but ours do.
296
$trim_len = $match_len ? $match_len + 1 : 0;
297
298
$count = 0;
299
foreach ($entire_manifest as $path) {
300
if (strncmp($path, $match_against, $match_len)) {
301
continue;
302
}
303
if ($path === null || !strlen($path)) {
304
continue;
305
}
306
$remainder = substr($path, $trim_len);
307
if (!strlen($remainder)) {
308
// There is a file with this exact name in the manifest, so clearly
309
// it's a file.
310
$result->setReasonForEmptyResultSet(
311
DiffusionBrowseResultSet::REASON_IS_FILE);
312
return $result;
313
}
314
315
$parts = explode('/', $remainder);
316
$name = reset($parts);
317
318
// If we've already seen this path component, we're looking at a file
319
// inside a directory we already processed. Just move on.
320
if (isset($results[$name])) {
321
continue;
322
}
323
324
if (count($parts) == 1) {
325
$type = DifferentialChangeType::FILE_NORMAL;
326
} else {
327
$type = DifferentialChangeType::FILE_DIRECTORY;
328
}
329
330
if ($count >= $offset) {
331
$results[$name] = $type;
332
}
333
334
$count++;
335
336
if ($limit && ($count >= ($offset + $limit))) {
337
break;
338
}
339
}
340
341
foreach ($results as $key => $type) {
342
$path_result = new DiffusionRepositoryPath();
343
$path_result->setPath($key);
344
$path_result->setFileType($type);
345
$path_result->setFullPath(ltrim($match_against.'/', '/').$key);
346
347
$results[$key] = $path_result;
348
}
349
350
$valid_results = true;
351
if (empty($results)) {
352
// TODO: Detect "deleted" by issuing "hg log"?
353
$result->setReasonForEmptyResultSet(
354
DiffusionBrowseResultSet::REASON_IS_NONEXISTENT);
355
$valid_results = false;
356
}
357
358
return $result
359
->setPaths($results)
360
->setIsValidResults($valid_results);
361
}
362
363
protected function getSVNResult(ConduitAPIRequest $request) {
364
$drequest = $this->getDiffusionRequest();
365
$repository = $drequest->getRepository();
366
$path = $request->getValue('path');
367
$commit = $request->getValue('commit');
368
$offset = (int)$request->getValue('offset');
369
$limit = (int)$request->getValue('limit');
370
$result = $this->getEmptyResultSet();
371
372
$subpath = $repository->getDetail('svn-subpath');
373
if ($subpath && strncmp($subpath, $path, strlen($subpath))) {
374
// If we have a subpath and the path isn't a child of it, it (almost
375
// certainly) won't exist since we don't track commits which affect
376
// it. (Even if it exists, return a consistent result.)
377
$result->setReasonForEmptyResultSet(
378
DiffusionBrowseResultSet::REASON_IS_UNTRACKED_PARENT);
379
return $result;
380
}
381
382
$conn_r = $repository->establishConnection('r');
383
384
$parent_path = DiffusionPathIDQuery::getParentPath($path);
385
$path_query = new DiffusionPathIDQuery(
386
array(
387
$path,
388
$parent_path,
389
));
390
$path_map = $path_query->loadPathIDs();
391
392
$path_id = $path_map[$path];
393
$parent_path_id = $path_map[$parent_path];
394
395
if (empty($path_id)) {
396
$result->setReasonForEmptyResultSet(
397
DiffusionBrowseResultSet::REASON_IS_NONEXISTENT);
398
return $result;
399
}
400
401
if ($commit) {
402
$slice_clause = qsprintf($conn_r, 'AND svnCommit <= %d', $commit);
403
} else {
404
$slice_clause = qsprintf($conn_r, '');
405
}
406
407
$index = queryfx_all(
408
$conn_r,
409
'SELECT pathID, max(svnCommit) maxCommit FROM %T WHERE
410
repositoryID = %d AND parentID = %d
411
%Q GROUP BY pathID',
412
PhabricatorRepository::TABLE_FILESYSTEM,
413
$repository->getID(),
414
$path_id,
415
$slice_clause);
416
417
if (!$index) {
418
if ($path == '/') {
419
$result->setReasonForEmptyResultSet(
420
DiffusionBrowseResultSet::REASON_IS_EMPTY);
421
} else {
422
423
// NOTE: The parent path ID is included so this query can take
424
// advantage of the table's primary key; it is uniquely determined by
425
// the pathID but if we don't do the lookup ourselves MySQL doesn't have
426
// the information it needs to avoid a table scan.
427
428
$reasons = queryfx_all(
429
$conn_r,
430
'SELECT * FROM %T WHERE repositoryID = %d
431
AND parentID = %d
432
AND pathID = %d
433
%Q ORDER BY svnCommit DESC LIMIT 2',
434
PhabricatorRepository::TABLE_FILESYSTEM,
435
$repository->getID(),
436
$parent_path_id,
437
$path_id,
438
$slice_clause);
439
440
$reason = reset($reasons);
441
442
if (!$reason) {
443
$result->setReasonForEmptyResultSet(
444
DiffusionBrowseResultSet::REASON_IS_NONEXISTENT);
445
} else {
446
$file_type = $reason['fileType'];
447
if (empty($reason['existed'])) {
448
$result->setReasonForEmptyResultSet(
449
DiffusionBrowseResultSet::REASON_IS_DELETED);
450
$result->setDeletedAtCommit($reason['svnCommit']);
451
if (!empty($reasons[1])) {
452
$result->setExistedAtCommit($reasons[1]['svnCommit']);
453
}
454
} else if ($file_type == DifferentialChangeType::FILE_DIRECTORY) {
455
$result->setReasonForEmptyResultSet(
456
DiffusionBrowseResultSet::REASON_IS_EMPTY);
457
} else {
458
$result->setReasonForEmptyResultSet(
459
DiffusionBrowseResultSet::REASON_IS_FILE);
460
}
461
}
462
}
463
return $result;
464
}
465
466
$result->setIsValidResults(true);
467
if ($this->shouldOnlyTestValidity($request)) {
468
return $result;
469
}
470
471
$sql = array();
472
foreach ($index as $row) {
473
$sql[] = qsprintf(
474
$conn_r,
475
'(pathID = %d AND svnCommit = %d)',
476
$row['pathID'],
477
$row['maxCommit']);
478
}
479
480
$browse = queryfx_all(
481
$conn_r,
482
'SELECT *, p.path pathName
483
FROM %T f JOIN %T p ON f.pathID = p.id
484
WHERE repositoryID = %d
485
AND parentID = %d
486
AND existed = 1
487
AND (%LO)
488
ORDER BY pathName',
489
PhabricatorRepository::TABLE_FILESYSTEM,
490
PhabricatorRepository::TABLE_PATH,
491
$repository->getID(),
492
$path_id,
493
$sql);
494
495
$loadable_commits = array();
496
foreach ($browse as $key => $file) {
497
// We need to strip out directories because we don't store last-modified
498
// in the filesystem table.
499
if ($file['fileType'] != DifferentialChangeType::FILE_DIRECTORY) {
500
$loadable_commits[] = $file['svnCommit'];
501
$browse[$key]['hasCommit'] = true;
502
}
503
}
504
505
$commits = array();
506
$commit_data = array();
507
if ($loadable_commits) {
508
// NOTE: Even though these are integers, use '%Ls' because MySQL doesn't
509
// use the second part of the key otherwise!
510
$commits = id(new PhabricatorRepositoryCommit())->loadAllWhere(
511
'repositoryID = %d AND commitIdentifier IN (%Ls)',
512
$repository->getID(),
513
$loadable_commits);
514
$commits = mpull($commits, null, 'getCommitIdentifier');
515
if ($commits) {
516
$commit_data = id(new PhabricatorRepositoryCommitData())->loadAllWhere(
517
'commitID in (%Ld)',
518
mpull($commits, 'getID'));
519
$commit_data = mpull($commit_data, null, 'getCommitID');
520
} else {
521
$commit_data = array();
522
}
523
}
524
525
$path_normal = DiffusionPathIDQuery::normalizePath($path);
526
527
$results = array();
528
$count = 0;
529
foreach ($browse as $file) {
530
531
$full_path = $file['pathName'];
532
$file_path = ltrim(substr($full_path, strlen($path_normal)), '/');
533
$full_path = ltrim($full_path, '/');
534
535
$result_path = new DiffusionRepositoryPath();
536
$result_path->setPath($file_path);
537
$result_path->setFullPath($full_path);
538
$result_path->setFileType($file['fileType']);
539
540
if (!empty($file['hasCommit'])) {
541
$commit = idx($commits, $file['svnCommit']);
542
if ($commit) {
543
$data = idx($commit_data, $commit->getID());
544
$result_path->setLastModifiedCommit($commit);
545
$result_path->setLastCommitData($data);
546
}
547
}
548
549
if ($count >= $offset) {
550
$results[] = $result_path;
551
}
552
553
$count++;
554
555
if ($limit && ($count >= ($offset + $limit))) {
556
break;
557
}
558
}
559
560
if (empty($results)) {
561
$result->setReasonForEmptyResultSet(
562
DiffusionBrowseResultSet::REASON_IS_EMPTY);
563
}
564
565
return $result->setPaths($results);
566
}
567
568
private function getEmptyResultSet() {
569
return id(new DiffusionBrowseResultSet())
570
->setPaths(array())
571
->setReasonForEmptyResultSet(null)
572
->setIsValidResults(false);
573
}
574
575
private function shouldOnlyTestValidity(ConduitAPIRequest $request) {
576
return $request->getValue('needValidityOnly', false);
577
}
578
579
}
580
581