Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/drydock/operation/DrydockLandRepositoryOperation.php
12256 views
1
<?php
2
3
final class DrydockLandRepositoryOperation
4
extends DrydockRepositoryOperationType {
5
6
const OPCONST = 'land';
7
8
const PHASE_PUSH = 'op.land.push';
9
const PHASE_COMMIT = 'op.land.commit';
10
11
public function getOperationDescription(
12
DrydockRepositoryOperation $operation,
13
PhabricatorUser $viewer) {
14
return pht('Land Revision');
15
}
16
17
public function getOperationCurrentStatus(
18
DrydockRepositoryOperation $operation,
19
PhabricatorUser $viewer) {
20
21
$target = $operation->getRepositoryTarget();
22
$repository = $operation->getRepository();
23
switch ($operation->getOperationState()) {
24
case DrydockRepositoryOperation::STATE_WAIT:
25
return pht(
26
'Waiting to land revision into %s on %s...',
27
$repository->getMonogram(),
28
$target);
29
case DrydockRepositoryOperation::STATE_WORK:
30
return pht(
31
'Landing revision into %s on %s...',
32
$repository->getMonogram(),
33
$target);
34
case DrydockRepositoryOperation::STATE_DONE:
35
return pht(
36
'Revision landed into %s.',
37
$repository->getMonogram());
38
}
39
}
40
41
public function getWorkingCopyMerges(DrydockRepositoryOperation $operation) {
42
$repository = $operation->getRepository();
43
$merges = array();
44
45
$object = $operation->getObject();
46
if ($object instanceof DifferentialRevision) {
47
$diff = $this->loadDiff($operation);
48
$merges[] = array(
49
'src.uri' => $repository->getStagingURI(),
50
'src.ref' => $diff->getStagingRef(),
51
);
52
} else {
53
throw new Exception(
54
pht(
55
'Invalid or unknown object ("%s") for land operation, expected '.
56
'Differential Revision.',
57
$operation->getObjectPHID()));
58
}
59
60
return $merges;
61
}
62
63
public function applyOperation(
64
DrydockRepositoryOperation $operation,
65
DrydockInterface $interface) {
66
$viewer = $this->getViewer();
67
$repository = $operation->getRepository();
68
69
$cmd = array();
70
$arg = array();
71
72
$object = $operation->getObject();
73
if ($object instanceof DifferentialRevision) {
74
$revision = $object;
75
76
$diff = $this->loadDiff($operation);
77
78
$dict = $diff->getDiffAuthorshipDict();
79
$author_name = idx($dict, 'authorName');
80
$author_email = idx($dict, 'authorEmail');
81
82
$api_method = 'differential.getcommitmessage';
83
$api_params = array(
84
'revision_id' => $revision->getID(),
85
);
86
87
$commit_message = id(new ConduitCall($api_method, $api_params))
88
->setUser($viewer)
89
->execute();
90
} else {
91
throw new Exception(
92
pht(
93
'Invalid or unknown object ("%s") for land operation, expected '.
94
'Differential Revision.',
95
$operation->getObjectPHID()));
96
}
97
98
$target = $operation->getRepositoryTarget();
99
list($type, $name) = explode(':', $target, 2);
100
switch ($type) {
101
case 'branch':
102
$push_dst = 'refs/heads/'.$name;
103
break;
104
default:
105
throw new Exception(
106
pht(
107
'Unknown repository operation target type "%s" (in target "%s").',
108
$type,
109
$target));
110
}
111
112
$committer_info = $this->getCommitterInfo($operation);
113
114
// NOTE: We're doing this commit with "-F -" so we don't run into trouble
115
// with enormous commit messages which might otherwise exceed the maximum
116
// size of a command.
117
118
$future = $interface->getExecFuture(
119
'git -c user.name=%s -c user.email=%s commit --author %s -F - --',
120
$committer_info['name'],
121
$committer_info['email'],
122
"{$author_name} <{$author_email}>");
123
124
$future->write($commit_message);
125
126
try {
127
$future->resolvex();
128
} catch (CommandException $ex) {
129
$display_command = csprintf('git commit');
130
131
// TODO: One reason this can fail is if the changes have already been
132
// merged. We could try to detect that.
133
134
$error = DrydockCommandError::newFromCommandException($ex)
135
->setPhase(self::PHASE_COMMIT)
136
->setDisplayCommand($display_command);
137
138
$operation->setCommandError($error->toDictionary());
139
140
throw $ex;
141
}
142
143
try {
144
$interface->execx(
145
'git push origin -- %s:%s',
146
'HEAD',
147
$push_dst);
148
} catch (CommandException $ex) {
149
$display_command = csprintf(
150
'git push origin %R:%R',
151
'HEAD',
152
$push_dst);
153
154
$error = DrydockCommandError::newFromCommandException($ex)
155
->setPhase(self::PHASE_PUSH)
156
->setDisplayCommand($display_command);
157
158
$operation->setCommandError($error->toDictionary());
159
160
throw $ex;
161
}
162
}
163
164
private function getCommitterInfo(DrydockRepositoryOperation $operation) {
165
$viewer = $this->getViewer();
166
167
$committer_name = null;
168
169
$author_phid = $operation->getAuthorPHID();
170
$object = id(new PhabricatorObjectQuery())
171
->setViewer($viewer)
172
->withPHIDs(array($author_phid))
173
->executeOne();
174
175
if ($object) {
176
if ($object instanceof PhabricatorUser) {
177
$committer_name = $object->getUsername();
178
}
179
}
180
181
if (!strlen($committer_name)) {
182
$committer_name = pht('autocommitter');
183
}
184
185
// TODO: Probably let users choose a VCS email address in settings. For
186
// now just make something up so we don't leak anyone's stuff.
187
188
return array(
189
'name' => $committer_name,
190
'email' => '[email protected]',
191
);
192
}
193
194
private function loadDiff(DrydockRepositoryOperation $operation) {
195
$viewer = $this->getViewer();
196
$revision = $operation->getObject();
197
198
$diff_phid = $operation->getProperty('differential.diffPHID');
199
200
$diff = id(new DifferentialDiffQuery())
201
->setViewer($viewer)
202
->withPHIDs(array($diff_phid))
203
->executeOne();
204
if (!$diff) {
205
throw new Exception(
206
pht(
207
'Unable to load diff "%s".',
208
$diff_phid));
209
}
210
211
$diff_revid = $diff->getRevisionID();
212
$revision_id = $revision->getID();
213
if ($diff_revid != $revision_id) {
214
throw new Exception(
215
pht(
216
'Diff ("%s") has wrong revision ID ("%s", expected "%s").',
217
$diff_phid,
218
$diff_revid,
219
$revision_id));
220
}
221
222
return $diff;
223
}
224
225
public function getBarrierToLanding(
226
PhabricatorUser $viewer,
227
DifferentialRevision $revision) {
228
229
$repository = $revision->getRepository();
230
if (!$repository) {
231
return array(
232
'title' => pht('No Repository'),
233
'body' => pht(
234
'This revision is not associated with a known repository. Only '.
235
'revisions associated with a tracked repository can be landed '.
236
'automatically.'),
237
);
238
}
239
240
if (!$repository->canPerformAutomation()) {
241
return array(
242
'title' => pht('No Repository Automation'),
243
'body' => pht(
244
'The repository this revision is associated with ("%s") is not '.
245
'configured to support automation. Configure automation for the '.
246
'repository to enable revisions to be landed automatically.',
247
$repository->getMonogram()),
248
);
249
}
250
251
// Check if this diff was pushed to a staging area.
252
$diff = id(new DifferentialDiffQuery())
253
->setViewer($viewer)
254
->withIDs(array($revision->getActiveDiff()->getID()))
255
->needProperties(true)
256
->executeOne();
257
258
// Older diffs won't have this property. They may still have been pushed.
259
// At least for now, assume staging changes are present if the property
260
// is missing. This should smooth the transition to the more formal
261
// approach.
262
$has_staging = $diff->hasDiffProperty('arc.staging');
263
if ($has_staging) {
264
$staging = $diff->getProperty('arc.staging');
265
if (!is_array($staging)) {
266
$staging = array();
267
}
268
$status = idx($staging, 'status');
269
if ($status != ArcanistDiffWorkflow::STAGING_PUSHED) {
270
return $this->getBarrierToLandingFromStagingStatus($status);
271
}
272
}
273
274
// TODO: At some point we should allow installs to give "land reviewed
275
// code" permission to more users than "push any commit", because it is
276
// a much less powerful operation. For now, just require push so this
277
// doesn't do anything users can't do on their own.
278
$can_push = PhabricatorPolicyFilter::hasCapability(
279
$viewer,
280
$repository,
281
DiffusionPushCapability::CAPABILITY);
282
if (!$can_push) {
283
return array(
284
'title' => pht('Unable to Push'),
285
'body' => pht(
286
'You do not have permission to push to the repository this '.
287
'revision is associated with ("%s"), so you can not land it.',
288
$repository->getMonogram()),
289
);
290
}
291
292
if ($revision->isAccepted()) {
293
// We can land accepted revisions, so continue below. Otherwise, raise
294
// an error with tailored messaging for the most common cases.
295
} else if ($revision->isAbandoned()) {
296
return array(
297
'title' => pht('Revision Abandoned'),
298
'body' => pht(
299
'This revision has been abandoned. Only accepted revisions '.
300
'may land.'),
301
);
302
} else if ($revision->isClosed()) {
303
return array(
304
'title' => pht('Revision Closed'),
305
'body' => pht(
306
'This revision has already been closed. Only open, accepted '.
307
'revisions may land.'),
308
);
309
} else {
310
return array(
311
'title' => pht('Revision Not Accepted'),
312
'body' => pht(
313
'This revision is still under review. Only revisions which '.
314
'have been accepted may land.'),
315
);
316
}
317
318
// Check for other operations. Eventually this should probably be more
319
// general (e.g., it's OK to land to multiple different branches
320
// simultaneously) but just put this in as a sanity check for now.
321
$other_operations = id(new DrydockRepositoryOperationQuery())
322
->setViewer($viewer)
323
->withObjectPHIDs(array($revision->getPHID()))
324
->withOperationTypes(
325
array(
326
$this->getOperationConstant(),
327
))
328
->withOperationStates(
329
array(
330
DrydockRepositoryOperation::STATE_WAIT,
331
DrydockRepositoryOperation::STATE_WORK,
332
DrydockRepositoryOperation::STATE_DONE,
333
))
334
->execute();
335
336
if ($other_operations) {
337
$any_done = false;
338
foreach ($other_operations as $operation) {
339
if ($operation->isDone()) {
340
$any_done = true;
341
break;
342
}
343
}
344
345
if ($any_done) {
346
return array(
347
'title' => pht('Already Complete'),
348
'body' => pht('This revision has already landed.'),
349
);
350
} else {
351
return array(
352
'title' => pht('Already In Flight'),
353
'body' => pht('This revision is already landing.'),
354
);
355
}
356
}
357
358
return null;
359
}
360
361
private function getBarrierToLandingFromStagingStatus($status) {
362
switch ($status) {
363
case ArcanistDiffWorkflow::STAGING_USER_SKIP:
364
return array(
365
'title' => pht('Staging Area Skipped'),
366
'body' => pht(
367
'The diff author used the %s flag to skip pushing this change to '.
368
'staging. Changes must be pushed to staging before they can be '.
369
'landed from the web.',
370
phutil_tag('tt', array(), '--skip-staging')),
371
);
372
case ArcanistDiffWorkflow::STAGING_DIFF_RAW:
373
return array(
374
'title' => pht('Raw Diff Source'),
375
'body' => pht(
376
'The diff was generated from a raw input source, so the change '.
377
'could not be pushed to staging. Changes must be pushed to '.
378
'staging before they can be landed from the web.'),
379
);
380
case ArcanistDiffWorkflow::STAGING_REPOSITORY_UNKNOWN:
381
return array(
382
'title' => pht('Unknown Repository'),
383
'body' => pht(
384
'When the diff was generated, the client was not able to '.
385
'determine which repository it belonged to, so the change '.
386
'was not pushed to staging. Changes must be pushed to staging '.
387
'before they can be landed from the web.'),
388
);
389
case ArcanistDiffWorkflow::STAGING_REPOSITORY_UNAVAILABLE:
390
return array(
391
'title' => pht('Staging Unavailable'),
392
'body' => pht(
393
'When this diff was generated, the server was running an older '.
394
'version of the software which did not support staging areas, so '.
395
'the change was not pushed to staging. Changes must be pushed '.
396
'to staging before they can be landed from the web.'),
397
);
398
case ArcanistDiffWorkflow::STAGING_REPOSITORY_UNSUPPORTED:
399
return array(
400
'title' => pht('Repository Unsupported'),
401
'body' => pht(
402
'When this diff was generated, the server was running an older '.
403
'version of the software which did not support staging areas for '.
404
'this version control system, so the change was not pushed to '.
405
'staging. Changes must be pushed to staging before they can be '.
406
'landed from the web.'),
407
);
408
409
case ArcanistDiffWorkflow::STAGING_REPOSITORY_UNCONFIGURED:
410
return array(
411
'title' => pht('Repository Unconfigured'),
412
'body' => pht(
413
'When this diff was generated, the repository was not configured '.
414
'with a staging area, so the change was not pushed to staging. '.
415
'Changes must be pushed to staging before they can be landed '.
416
'from the web.'),
417
);
418
case ArcanistDiffWorkflow::STAGING_CLIENT_UNSUPPORTED:
419
return array(
420
'title' => pht('Client Support Unavailable'),
421
'body' => pht(
422
'When this diff was generated, the client did not support '.
423
'staging areas for this version control system, so the change '.
424
'was not pushed to staging. Changes must be pushed to staging '.
425
'before they can be landed from the web. Updating the client '.
426
'may resolve this issue.'),
427
);
428
default:
429
return array(
430
'title' => pht('Unknown Error'),
431
'body' => pht(
432
'When this diff was generated, it was not pushed to staging for '.
433
'an unknown reason (the status code was "%s"). Changes must be '.
434
'pushed to staging before they can be landed from the web. '.
435
'The server may be running an out-of-date version of this '.
436
'software, and updating may provide more information about this '.
437
'error.',
438
$status),
439
);
440
}
441
}
442
443
}
444
445