Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/diffusion/request/DiffusionRequest.php
13401 views
1
<?php
2
3
/**
4
* Contains logic to parse Diffusion requests, which have a complicated URI
5
* structure.
6
*
7
* @task new Creating Requests
8
* @task uri Managing Diffusion URIs
9
*/
10
abstract class DiffusionRequest extends Phobject {
11
12
protected $path;
13
protected $line;
14
protected $branch;
15
protected $lint;
16
17
protected $symbolicCommit;
18
protected $symbolicType;
19
protected $stableCommit;
20
21
protected $repository;
22
protected $repositoryCommit;
23
protected $repositoryCommitData;
24
25
private $isClusterRequest = false;
26
private $initFromConduit = true;
27
private $user;
28
private $branchObject = false;
29
private $refAlternatives;
30
31
final public function supportsBranches() {
32
return $this->getRepository()->supportsRefs();
33
}
34
35
abstract protected function isStableCommit($symbol);
36
37
protected function didInitialize() {
38
return null;
39
}
40
41
42
/* -( Creating Requests )-------------------------------------------------- */
43
44
45
/**
46
* Create a new synthetic request from a parameter dictionary. If you need
47
* a @{class:DiffusionRequest} object in order to issue a DiffusionQuery, you
48
* can use this method to build one.
49
*
50
* Parameters are:
51
*
52
* - `repository` Repository object or identifier.
53
* - `user` Viewing user. Required if `repository` is an identifier.
54
* - `branch` Optional, branch name.
55
* - `path` Optional, file path.
56
* - `commit` Optional, commit identifier.
57
* - `line` Optional, line range.
58
*
59
* @param map See documentation.
60
* @return DiffusionRequest New request object.
61
* @task new
62
*/
63
final public static function newFromDictionary(array $data) {
64
$repository_key = 'repository';
65
$identifier_key = 'callsign';
66
$viewer_key = 'user';
67
68
$repository = idx($data, $repository_key);
69
$identifier = idx($data, $identifier_key);
70
71
$have_repository = ($repository !== null);
72
$have_identifier = ($identifier !== null);
73
74
if ($have_repository && $have_identifier) {
75
throw new Exception(
76
pht(
77
'Specify "%s" or "%s", but not both.',
78
$repository_key,
79
$identifier_key));
80
}
81
82
if (!$have_repository && !$have_identifier) {
83
throw new Exception(
84
pht(
85
'One of "%s" and "%s" is required.',
86
$repository_key,
87
$identifier_key));
88
}
89
90
if ($have_repository) {
91
if (!($repository instanceof PhabricatorRepository)) {
92
if (empty($data[$viewer_key])) {
93
throw new Exception(
94
pht(
95
'Parameter "%s" is required if "%s" is provided.',
96
$viewer_key,
97
$identifier_key));
98
}
99
100
$identifier = $repository;
101
$repository = null;
102
}
103
}
104
105
if ($identifier !== null) {
106
$object = self::newFromIdentifier(
107
$identifier,
108
$data[$viewer_key],
109
idx($data, 'edit'));
110
} else {
111
$object = self::newFromRepository($repository);
112
}
113
114
if (!$object) {
115
return null;
116
}
117
118
$object->initializeFromDictionary($data);
119
120
return $object;
121
}
122
123
/**
124
* Internal.
125
*
126
* @task new
127
*/
128
private function __construct() {
129
// <private>
130
}
131
132
133
/**
134
* Internal. Use @{method:newFromDictionary}, not this method.
135
*
136
* @param string Repository identifier.
137
* @param PhabricatorUser Viewing user.
138
* @return DiffusionRequest New request object.
139
* @task new
140
*/
141
private static function newFromIdentifier(
142
$identifier,
143
PhabricatorUser $viewer,
144
$need_edit = false) {
145
146
$query = id(new PhabricatorRepositoryQuery())
147
->setViewer($viewer)
148
->withIdentifiers(array($identifier))
149
->needProfileImage(true)
150
->needURIs(true);
151
152
if ($need_edit) {
153
$query->requireCapabilities(
154
array(
155
PhabricatorPolicyCapability::CAN_VIEW,
156
PhabricatorPolicyCapability::CAN_EDIT,
157
));
158
}
159
160
$repository = $query->executeOne();
161
162
if (!$repository) {
163
return null;
164
}
165
166
return self::newFromRepository($repository);
167
}
168
169
170
/**
171
* Internal. Use @{method:newFromDictionary}, not this method.
172
*
173
* @param PhabricatorRepository Repository object.
174
* @return DiffusionRequest New request object.
175
* @task new
176
*/
177
private static function newFromRepository(
178
PhabricatorRepository $repository) {
179
180
$map = array(
181
PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'DiffusionGitRequest',
182
PhabricatorRepositoryType::REPOSITORY_TYPE_SVN => 'DiffusionSvnRequest',
183
PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL =>
184
'DiffusionMercurialRequest',
185
);
186
187
$class = idx($map, $repository->getVersionControlSystem());
188
189
if (!$class) {
190
throw new Exception(pht('Unknown version control system!'));
191
}
192
193
$object = new $class();
194
195
$object->repository = $repository;
196
197
return $object;
198
}
199
200
201
/**
202
* Internal. Use @{method:newFromDictionary}, not this method.
203
*
204
* @param map Map of parsed data.
205
* @return void
206
* @task new
207
*/
208
private function initializeFromDictionary(array $data) {
209
$blob = idx($data, 'blob');
210
if (phutil_nonempty_string($blob)) {
211
$blob = self::parseRequestBlob($blob, $this->supportsBranches());
212
$data = $blob + $data;
213
}
214
215
$this->path = idx($data, 'path');
216
$this->line = idx($data, 'line');
217
$this->initFromConduit = idx($data, 'initFromConduit', true);
218
$this->lint = idx($data, 'lint');
219
220
$this->symbolicCommit = idx($data, 'commit');
221
if ($this->supportsBranches()) {
222
$this->branch = idx($data, 'branch');
223
}
224
225
if (!$this->getUser()) {
226
$user = idx($data, 'user');
227
if (!$user) {
228
throw new Exception(
229
pht(
230
'You must provide a %s in the dictionary!',
231
'PhabricatorUser'));
232
}
233
$this->setUser($user);
234
}
235
236
$this->didInitialize();
237
}
238
239
final public function setUser(PhabricatorUser $user) {
240
$this->user = $user;
241
return $this;
242
}
243
final public function getUser() {
244
return $this->user;
245
}
246
247
public function getRepository() {
248
return $this->repository;
249
}
250
251
public function setPath($path) {
252
$this->path = $path;
253
return $this;
254
}
255
256
public function getPath() {
257
return $this->path;
258
}
259
260
public function getLine() {
261
return $this->line;
262
}
263
264
public function getCommit() {
265
266
// TODO: Probably remove all of this.
267
268
if ($this->getSymbolicCommit() !== null) {
269
return $this->getSymbolicCommit();
270
}
271
272
return $this->getStableCommit();
273
}
274
275
/**
276
* Get the symbolic commit associated with this request.
277
*
278
* A symbolic commit may be a commit hash, an abbreviated commit hash, a
279
* branch name, a tag name, or an expression like "HEAD^^^". The symbolic
280
* commit may also be absent.
281
*
282
* This method always returns the symbol present in the original request,
283
* in unmodified form.
284
*
285
* See also @{method:getStableCommit}.
286
*
287
* @return string|null Symbolic commit, if one was present in the request.
288
*/
289
public function getSymbolicCommit() {
290
return $this->symbolicCommit;
291
}
292
293
294
/**
295
* Modify the request to move the symbolic commit elsewhere.
296
*
297
* @param string New symbolic commit.
298
* @return this
299
*/
300
public function updateSymbolicCommit($symbol) {
301
$this->symbolicCommit = $symbol;
302
$this->symbolicType = null;
303
$this->stableCommit = null;
304
return $this;
305
}
306
307
308
/**
309
* Get the ref type (`commit` or `tag`) of the location associated with this
310
* request.
311
*
312
* If a symbolic commit is present in the request, this method identifies
313
* the type of the symbol. Otherwise, it identifies the type of symbol of
314
* the location the request is implicitly associated with. This will probably
315
* always be `commit`.
316
*
317
* @return string Symbolic commit type (`commit` or `tag`).
318
*/
319
public function getSymbolicType() {
320
if ($this->symbolicType === null) {
321
// As a side effect, this resolves the symbolic type.
322
$this->getStableCommit();
323
}
324
return $this->symbolicType;
325
}
326
327
328
/**
329
* Retrieve the stable, permanent commit name identifying the repository
330
* location associated with this request.
331
*
332
* This returns a non-symbolic identifier for the current commit: in Git and
333
* Mercurial, a 40-character SHA1; in SVN, a revision number.
334
*
335
* See also @{method:getSymbolicCommit}.
336
*
337
* @return string Stable commit name, like a git hash or SVN revision. Not
338
* a symbolic commit reference.
339
*/
340
public function getStableCommit() {
341
if (!$this->stableCommit) {
342
if ($this->isStableCommit($this->symbolicCommit)) {
343
$this->stableCommit = $this->symbolicCommit;
344
$this->symbolicType = 'commit';
345
} else {
346
$this->queryStableCommit();
347
}
348
}
349
return $this->stableCommit;
350
}
351
352
353
public function getBranch() {
354
return $this->branch;
355
}
356
357
public function getLint() {
358
return $this->lint;
359
}
360
361
protected function getArcanistBranch() {
362
return $this->getBranch();
363
}
364
365
public function loadBranch() {
366
// TODO: Get rid of this and do real Queries on real objects.
367
368
if ($this->branchObject === false) {
369
$this->branchObject = PhabricatorRepositoryBranch::loadBranch(
370
$this->getRepository()->getID(),
371
$this->getArcanistBranch());
372
}
373
374
return $this->branchObject;
375
}
376
377
public function loadCoverage() {
378
// TODO: This should also die.
379
$branch = $this->loadBranch();
380
if (!$branch) {
381
return;
382
}
383
384
$path = $this->getPath();
385
$path_map = id(new DiffusionPathIDQuery(array($path)))->loadPathIDs();
386
387
$coverage_row = queryfx_one(
388
id(new PhabricatorRepository())->establishConnection('r'),
389
'SELECT * FROM %T WHERE branchID = %d AND pathID = %d
390
ORDER BY commitID DESC LIMIT 1',
391
'repository_coverage',
392
$branch->getID(),
393
$path_map[$path]);
394
395
if (!$coverage_row) {
396
return null;
397
}
398
399
return idx($coverage_row, 'coverage');
400
}
401
402
403
public function loadCommit() {
404
if (empty($this->repositoryCommit)) {
405
$repository = $this->getRepository();
406
407
$commit = id(new DiffusionCommitQuery())
408
->setViewer($this->getUser())
409
->withRepository($repository)
410
->withIdentifiers(array($this->getStableCommit()))
411
->executeOne();
412
if ($commit) {
413
$commit->attachRepository($repository);
414
}
415
$this->repositoryCommit = $commit;
416
}
417
return $this->repositoryCommit;
418
}
419
420
public function loadCommitData() {
421
if (empty($this->repositoryCommitData)) {
422
$commit = $this->loadCommit();
423
$data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
424
'commitID = %d',
425
$commit->getID());
426
if (!$data) {
427
$data = new PhabricatorRepositoryCommitData();
428
$data->setCommitMessage(
429
pht('(This commit has not been fully parsed yet.)'));
430
}
431
$this->repositoryCommitData = $data;
432
}
433
return $this->repositoryCommitData;
434
}
435
436
/* -( Managing Diffusion URIs )-------------------------------------------- */
437
438
439
public function generateURI(array $params) {
440
if (empty($params['stable'])) {
441
$default_commit = $this->getSymbolicCommit();
442
} else {
443
$default_commit = $this->getStableCommit();
444
}
445
446
$defaults = array(
447
'path' => $this->getPath(),
448
'branch' => $this->getBranch(),
449
'commit' => $default_commit,
450
'lint' => idx($params, 'lint', $this->getLint()),
451
);
452
453
foreach ($defaults as $key => $val) {
454
if (!isset($params[$key])) { // Overwrite NULL.
455
$params[$key] = $val;
456
}
457
}
458
459
return $this->getRepository()->generateURI($params);
460
}
461
462
/**
463
* Internal. Public only for unit tests.
464
*
465
* Parse the request URI into components.
466
*
467
* @param string URI blob.
468
* @param bool True if this VCS supports branches.
469
* @return map Parsed URI.
470
*
471
* @task uri
472
*/
473
public static function parseRequestBlob($blob, $supports_branches) {
474
$result = array(
475
'branch' => null,
476
'path' => null,
477
'commit' => null,
478
'line' => null,
479
);
480
481
$matches = null;
482
483
if ($supports_branches) {
484
// Consume the front part of the URI, up to the first "/". This is the
485
// path-component encoded branch name.
486
if (preg_match('@^([^/]+)/@', $blob, $matches)) {
487
$result['branch'] = phutil_unescape_uri_path_component($matches[1]);
488
$blob = substr($blob, strlen($matches[1]) + 1);
489
}
490
}
491
492
// Consume the back part of the URI, up to the first "$". Use a negative
493
// lookbehind to prevent matching '$$'. We double the '$' symbol when
494
// encoding so that files with names like "money/$100" will survive.
495
$pattern = '@(?:(?:^|[^$])(?:[$][$])*)[$]([\d,-]+)$@';
496
if (preg_match($pattern, $blob, $matches)) {
497
$result['line'] = $matches[1];
498
$blob = substr($blob, 0, -(strlen($matches[1]) + 1));
499
}
500
501
// We've consumed the line number if it exists, so unescape "$" in the
502
// rest of the string.
503
$blob = str_replace('$$', '$', $blob);
504
505
// Consume the commit name, stopping on ';;'. We allow any character to
506
// appear in commits names, as they can sometimes be symbolic names (like
507
// tag names or refs).
508
if (preg_match('@(?:(?:^|[^;])(?:;;)*);([^;].*)$@', $blob, $matches)) {
509
$result['commit'] = $matches[1];
510
$blob = substr($blob, 0, -(strlen($matches[1]) + 1));
511
}
512
513
// We've consumed the commit if it exists, so unescape ";" in the rest
514
// of the string.
515
$blob = str_replace(';;', ';', $blob);
516
517
if (strlen($blob)) {
518
$result['path'] = $blob;
519
}
520
521
if ($result['path'] !== null) {
522
$parts = explode('/', $result['path']);
523
foreach ($parts as $part) {
524
// Prevent any hyjinx since we're ultimately shipping this to the
525
// filesystem under a lot of workflows.
526
if ($part == '..') {
527
throw new Exception(pht('Invalid path URI.'));
528
}
529
}
530
}
531
532
return $result;
533
}
534
535
/**
536
* Check that the working copy of the repository is present and readable.
537
*
538
* @param string Path to the working copy.
539
*/
540
protected function validateWorkingCopy($path) {
541
if (!is_readable(dirname($path))) {
542
$this->raisePermissionException();
543
}
544
545
if (!Filesystem::pathExists($path)) {
546
$this->raiseCloneException();
547
}
548
}
549
550
protected function raisePermissionException() {
551
$host = php_uname('n');
552
throw new DiffusionSetupException(
553
pht(
554
'The clone of this repository ("%s") on the local machine ("%s") '.
555
'could not be read. Ensure that the repository is in a '.
556
'location where the web server has read permissions.',
557
$this->getRepository()->getDisplayName(),
558
$host));
559
}
560
561
protected function raiseCloneException() {
562
$host = php_uname('n');
563
throw new DiffusionSetupException(
564
pht(
565
'The working copy for this repository ("%s") has not been cloned yet '.
566
'on this machine ("%s"). Make sure you have started the '.
567
'daemons. If this problem persists for longer than a clone should '.
568
'take, check the daemon logs (in the Daemon Console) to see if there '.
569
'were errors cloning the repository. Consult the "Diffusion User '.
570
'Guide" in the documentation for help setting up repositories.',
571
$this->getRepository()->getDisplayName(),
572
$host));
573
}
574
575
private function queryStableCommit() {
576
$types = array();
577
if ($this->symbolicCommit) {
578
$ref = $this->symbolicCommit;
579
} else {
580
if ($this->supportsBranches()) {
581
$ref = $this->getBranch();
582
$types = array(
583
PhabricatorRepositoryRefCursor::TYPE_BRANCH,
584
);
585
} else {
586
$ref = 'HEAD';
587
}
588
}
589
590
$results = $this->resolveRefs(array($ref), $types);
591
592
$matches = idx($results, $ref, array());
593
if (!$matches) {
594
$message = pht(
595
'Ref "%s" does not exist in this repository.',
596
$ref);
597
throw id(new DiffusionRefNotFoundException($message))
598
->setRef($ref);
599
}
600
601
if (count($matches) > 1) {
602
$match = $this->chooseBestRefMatch($ref, $matches);
603
} else {
604
$match = head($matches);
605
}
606
607
$this->stableCommit = $match['identifier'];
608
$this->symbolicType = $match['type'];
609
}
610
611
public function getRefAlternatives() {
612
// Make sure we've resolved the reference into a stable commit first.
613
try {
614
$this->getStableCommit();
615
} catch (DiffusionRefNotFoundException $ex) {
616
// If we have a bad reference, just return the empty set of
617
// alternatives.
618
}
619
return $this->refAlternatives;
620
}
621
622
private function chooseBestRefMatch($ref, array $results) {
623
// First, filter out less-desirable matches.
624
$candidates = array();
625
foreach ($results as $result) {
626
// Exclude closed heads.
627
if ($result['type'] == 'branch') {
628
if (idx($result, 'closed')) {
629
continue;
630
}
631
}
632
633
$candidates[] = $result;
634
}
635
636
// If we filtered everything, undo the filtering.
637
if (!$candidates) {
638
$candidates = $results;
639
}
640
641
// TODO: Do a better job of selecting the best match?
642
$match = head($candidates);
643
644
// After choosing the best alternative, save all the alternatives so the
645
// UI can show them to the user.
646
if (count($candidates) > 1) {
647
$this->refAlternatives = $candidates;
648
}
649
650
return $match;
651
}
652
653
public function resolveRefs(array $refs, array $types = array()) {
654
// First, try to resolve refs from fast cache sources.
655
$cached_query = id(new DiffusionCachedResolveRefsQuery())
656
->setRepository($this->getRepository())
657
->withRefs($refs);
658
659
if ($types) {
660
$cached_query->withTypes($types);
661
}
662
663
$cached_results = $cached_query->execute();
664
665
// Throw away all the refs we resolved. Hopefully, we'll throw away
666
// everything here.
667
foreach ($refs as $key => $ref) {
668
if (isset($cached_results[$ref])) {
669
unset($refs[$key]);
670
}
671
}
672
673
// If we couldn't pull everything out of the cache, execute the underlying
674
// VCS operation.
675
if ($refs) {
676
$vcs_results = DiffusionQuery::callConduitWithDiffusionRequest(
677
$this->getUser(),
678
$this,
679
'diffusion.resolverefs',
680
array(
681
'types' => $types,
682
'refs' => $refs,
683
));
684
} else {
685
$vcs_results = array();
686
}
687
688
return $vcs_results + $cached_results;
689
}
690
691
public function setIsClusterRequest($is_cluster_request) {
692
$this->isClusterRequest = $is_cluster_request;
693
return $this;
694
}
695
696
public function getIsClusterRequest() {
697
return $this->isClusterRequest;
698
}
699
700
}
701
702