Correct a parameter order swap in "diffusion.historyquery" for Mercurial
[phabricator.git] / src / applications / drydock / blueprint / DrydockWorkingCopyBlueprintImplementation.php
blobd82f5c2c150f3e26f8baf12e22bb2ea351a571a1
1 <?php
3 final class DrydockWorkingCopyBlueprintImplementation
4 extends DrydockBlueprintImplementation {
6 const PHASE_SQUASHMERGE = 'squashmerge';
7 const PHASE_REMOTEFETCH = 'blueprint.workingcopy.fetch.remote';
8 const PHASE_MERGEFETCH = 'blueprint.workingcopy.fetch.staging';
10 public function isEnabled() {
11 return true;
14 public function getBlueprintName() {
15 return pht('Working Copy');
18 public function getBlueprintIcon() {
19 return 'fa-folder-open';
22 public function getDescription() {
23 return pht('Allows Drydock to check out working copies of repositories.');
26 public function canAnyBlueprintEverAllocateResourceForLease(
27 DrydockLease $lease) {
28 return true;
31 public function canEverAllocateResourceForLease(
32 DrydockBlueprint $blueprint,
33 DrydockLease $lease) {
34 return true;
37 public function canAllocateResourceForLease(
38 DrydockBlueprint $blueprint,
39 DrydockLease $lease) {
40 $viewer = $this->getViewer();
42 if ($this->shouldLimitAllocatingPoolSize($blueprint)) {
43 return false;
46 // TODO: If we have a pending resource which is compatible with the
47 // configuration for this lease, prevent a new allocation? Otherwise the
48 // queue can fill up with copies of requests from the same lease. But
49 // maybe we can deal with this with "pre-leasing"?
51 return true;
54 public function canAcquireLeaseOnResource(
55 DrydockBlueprint $blueprint,
56 DrydockResource $resource,
57 DrydockLease $lease) {
59 // Don't hand out leases on working copies which have not activated, since
60 // it may take an arbitrarily long time for them to acquire a host.
61 if (!$resource->isActive()) {
62 return false;
65 $need_map = $lease->getAttribute('repositories.map');
66 if (!is_array($need_map)) {
67 return false;
70 $have_map = $resource->getAttribute('repositories.map');
71 if (!is_array($have_map)) {
72 return false;
75 $have_as = ipull($have_map, 'phid');
76 $need_as = ipull($need_map, 'phid');
78 foreach ($need_as as $need_directory => $need_phid) {
79 if (empty($have_as[$need_directory])) {
80 // This resource is missing a required working copy.
81 return false;
84 if ($have_as[$need_directory] != $need_phid) {
85 // This resource has a required working copy, but it contains
86 // the wrong repository.
87 return false;
90 unset($have_as[$need_directory]);
93 if ($have_as && $lease->getAttribute('repositories.strict')) {
94 // This resource has extra repositories, but the lease is strict about
95 // which repositories are allowed to exist.
96 return false;
99 if (!DrydockSlotLock::isLockFree($this->getLeaseSlotLock($resource))) {
100 return false;
103 return true;
106 public function acquireLease(
107 DrydockBlueprint $blueprint,
108 DrydockResource $resource,
109 DrydockLease $lease) {
111 $lease
112 ->needSlotLock($this->getLeaseSlotLock($resource))
113 ->acquireOnResource($resource);
116 private function getLeaseSlotLock(DrydockResource $resource) {
117 $resource_phid = $resource->getPHID();
118 return "workingcopy.lease({$resource_phid})";
121 public function allocateResource(
122 DrydockBlueprint $blueprint,
123 DrydockLease $lease) {
125 $resource = $this->newResourceTemplate($blueprint);
127 $resource_phid = $resource->getPHID();
129 $blueprint_phids = $blueprint->getFieldValue('blueprintPHIDs');
131 $host_lease = $this->newLease($blueprint)
132 ->setResourceType('host')
133 ->setOwnerPHID($resource_phid)
134 ->setAttribute('workingcopy.resourcePHID', $resource_phid)
135 ->setAllowedBlueprintPHIDs($blueprint_phids);
136 $resource->setAttribute('host.leasePHID', $host_lease->getPHID());
138 $map = $lease->getAttribute('repositories.map');
139 foreach ($map as $key => $value) {
140 $map[$key] = array_select_keys(
141 $value,
142 array(
143 'phid',
146 $resource->setAttribute('repositories.map', $map);
148 $slot_lock = $this->getConcurrentResourceLimitSlotLock($blueprint);
149 if ($slot_lock !== null) {
150 $resource->needSlotLock($slot_lock);
153 $resource->allocateResource();
155 $host_lease->queueForActivation();
157 return $resource;
160 public function activateResource(
161 DrydockBlueprint $blueprint,
162 DrydockResource $resource) {
164 $lease = $this->loadHostLease($resource);
165 $this->requireActiveLease($lease);
167 $command_type = DrydockCommandInterface::INTERFACE_TYPE;
168 $interface = $lease->getInterface($command_type);
170 // TODO: Make this configurable.
171 $resource_id = $resource->getID();
172 $root = "/var/drydock/workingcopy-{$resource_id}";
174 $map = $resource->getAttribute('repositories.map');
176 $futures = array();
177 $repositories = $this->loadRepositories(ipull($map, 'phid'));
178 foreach ($map as $directory => $spec) {
179 // TODO: Validate directory isn't goofy like "/etc" or "../../lol"
180 // somewhere?
182 $repository = $repositories[$spec['phid']];
183 $path = "{$root}/repo/{$directory}/";
185 $future = $interface->getExecFuture(
186 'git clone -- %s %s',
187 (string)$repository->getCloneURIObject(),
188 $path);
190 $future->setTimeout($repository->getEffectiveCopyTimeLimit());
192 $futures[$directory] = $future;
195 foreach (new FutureIterator($futures) as $key => $future) {
196 $future->resolvex();
199 $resource
200 ->setAttribute('workingcopy.root', $root)
201 ->activateResource();
204 public function destroyResource(
205 DrydockBlueprint $blueprint,
206 DrydockResource $resource) {
208 try {
209 $lease = $this->loadHostLease($resource);
210 } catch (Exception $ex) {
211 // If we can't load the lease, assume we don't need to take any actions
212 // to destroy it.
213 return;
216 // Destroy the lease on the host.
217 $lease->setReleaseOnDestruction(true);
219 if ($lease->isActive()) {
220 // Destroy the working copy on disk.
221 $command_type = DrydockCommandInterface::INTERFACE_TYPE;
222 $interface = $lease->getInterface($command_type);
224 $root_key = 'workingcopy.root';
225 $root = $resource->getAttribute($root_key);
226 if (strlen($root)) {
227 $interface->execx('rm -rf -- %s', $root);
232 public function getResourceName(
233 DrydockBlueprint $blueprint,
234 DrydockResource $resource) {
235 return pht('Working Copy');
239 public function activateLease(
240 DrydockBlueprint $blueprint,
241 DrydockResource $resource,
242 DrydockLease $lease) {
244 $host_lease = $this->loadHostLease($resource);
245 $command_type = DrydockCommandInterface::INTERFACE_TYPE;
246 $interface = $host_lease->getInterface($command_type);
248 $map = $lease->getAttribute('repositories.map');
249 $root = $resource->getAttribute('workingcopy.root');
251 $repositories = $this->loadRepositories(ipull($map, 'phid'));
253 $default = null;
254 foreach ($map as $directory => $spec) {
255 $repository = $repositories[$spec['phid']];
257 $interface->pushWorkingDirectory("{$root}/repo/{$directory}/");
259 $cmd = array();
260 $arg = array();
262 $cmd[] = 'git clean -d --force';
263 $cmd[] = 'git fetch';
265 $commit = idx($spec, 'commit');
266 $branch = idx($spec, 'branch');
268 $ref = idx($spec, 'ref');
270 // Reset things first, in case previous builds left anything staged or
271 // dirty. Note that we don't reset to "HEAD" because that does not work
272 // in empty repositories.
273 $cmd[] = 'git reset --hard';
275 if ($commit !== null) {
276 $cmd[] = 'git checkout %s --';
277 $arg[] = $commit;
278 } else if ($branch !== null) {
279 $cmd[] = 'git checkout %s --';
280 $arg[] = $branch;
282 $cmd[] = 'git reset --hard origin/%s';
283 $arg[] = $branch;
286 $this->newExecvFuture($interface, $cmd, $arg)
287 ->setTimeout($repository->getEffectiveCopyTimeLimit())
288 ->resolvex();
290 if (idx($spec, 'default')) {
291 $default = $directory;
294 // If we're fetching a ref from a remote, do that separately so we can
295 // raise a more tailored error.
296 if ($ref) {
297 $cmd = array();
298 $arg = array();
300 $ref_uri = $ref['uri'];
301 $ref_ref = $ref['ref'];
303 $cmd[] = 'git fetch --no-tags -- %s +%s:%s';
304 $arg[] = $ref_uri;
305 $arg[] = $ref_ref;
306 $arg[] = $ref_ref;
308 $cmd[] = 'git checkout %s --';
309 $arg[] = $ref_ref;
311 try {
312 $this->newExecvFuture($interface, $cmd, $arg)
313 ->setTimeout($repository->getEffectiveCopyTimeLimit())
314 ->resolvex();
315 } catch (CommandException $ex) {
316 $display_command = csprintf(
317 'git fetch %R %R',
318 $ref_uri,
319 $ref_ref);
321 $error = DrydockCommandError::newFromCommandException($ex)
322 ->setPhase(self::PHASE_REMOTEFETCH)
323 ->setDisplayCommand($display_command);
325 $lease->setAttribute(
326 'workingcopy.vcs.error',
327 $error->toDictionary());
329 throw $ex;
333 $merges = idx($spec, 'merges');
334 if ($merges) {
335 foreach ($merges as $merge) {
336 $this->applyMerge($lease, $interface, $merge);
340 $interface->popWorkingDirectory();
343 if ($default === null) {
344 $default = head_key($map);
347 // TODO: Use working storage?
348 $lease->setAttribute('workingcopy.default', "{$root}/repo/{$default}/");
350 $lease->activateOnResource($resource);
353 public function didReleaseLease(
354 DrydockBlueprint $blueprint,
355 DrydockResource $resource,
356 DrydockLease $lease) {
357 // We leave working copies around even if there are no leases on them,
358 // since the cost to maintain them is nearly zero but rebuilding them is
359 // moderately expensive and it's likely that they'll be reused.
360 return;
363 public function destroyLease(
364 DrydockBlueprint $blueprint,
365 DrydockResource $resource,
366 DrydockLease $lease) {
367 // When we activate a lease we just reset the working copy state and do
368 // not create any new state, so we don't need to do anything special when
369 // destroying a lease.
370 return;
373 public function getType() {
374 return 'working-copy';
377 public function getInterface(
378 DrydockBlueprint $blueprint,
379 DrydockResource $resource,
380 DrydockLease $lease,
381 $type) {
383 switch ($type) {
384 case DrydockCommandInterface::INTERFACE_TYPE:
385 $host_lease = $this->loadHostLease($resource);
386 $command_interface = $host_lease->getInterface($type);
388 $path = $lease->getAttribute('workingcopy.default');
389 $command_interface->pushWorkingDirectory($path);
391 return $command_interface;
395 private function loadRepositories(array $phids) {
396 $viewer = $this->getViewer();
398 $repositories = id(new PhabricatorRepositoryQuery())
399 ->setViewer($viewer)
400 ->withPHIDs($phids)
401 ->execute();
402 $repositories = mpull($repositories, null, 'getPHID');
404 foreach ($phids as $phid) {
405 if (empty($repositories[$phid])) {
406 throw new Exception(
407 pht(
408 'Repository PHID "%s" does not exist.',
409 $phid));
413 foreach ($repositories as $repository) {
414 $repository_vcs = $repository->getVersionControlSystem();
415 switch ($repository_vcs) {
416 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
417 break;
418 default:
419 throw new Exception(
420 pht(
421 'Repository ("%s") has unsupported VCS ("%s").',
422 $repository->getPHID(),
423 $repository_vcs));
427 return $repositories;
430 private function loadHostLease(DrydockResource $resource) {
431 $viewer = $this->getViewer();
433 $lease_phid = $resource->getAttribute('host.leasePHID');
435 $lease = id(new DrydockLeaseQuery())
436 ->setViewer($viewer)
437 ->withPHIDs(array($lease_phid))
438 ->executeOne();
439 if (!$lease) {
440 throw new Exception(
441 pht(
442 'Unable to load lease ("%s").',
443 $lease_phid));
446 return $lease;
449 protected function getCustomFieldSpecifications() {
450 return array(
451 'blueprintPHIDs' => array(
452 'name' => pht('Use Blueprints'),
453 'type' => 'blueprints',
454 'required' => true,
459 protected function shouldUseConcurrentResourceLimit() {
460 return true;
463 private function applyMerge(
464 DrydockLease $lease,
465 DrydockCommandInterface $interface,
466 array $merge) {
468 $src_uri = $merge['src.uri'];
469 $src_ref = $merge['src.ref'];
472 try {
473 $interface->execx(
474 'git fetch --no-tags -- %s +%s:%s',
475 $src_uri,
476 $src_ref,
477 $src_ref);
478 } catch (CommandException $ex) {
479 $display_command = csprintf(
480 'git fetch %R +%R:%R',
481 $src_uri,
482 $src_ref,
483 $src_ref);
485 $error = DrydockCommandError::newFromCommandException($ex)
486 ->setPhase(self::PHASE_MERGEFETCH)
487 ->setDisplayCommand($display_command);
489 $lease->setAttribute('workingcopy.vcs.error', $error->toDictionary());
491 throw $ex;
495 // NOTE: This can never actually generate a commit because we pass
496 // "--squash", but git sometimes runs code to check that a username and
497 // email are configured anyway.
498 $real_command = csprintf(
499 'git -c user.name=%s -c user.email=%s merge --no-stat --squash -- %R',
500 'drydock',
501 'drydock@phabricator',
502 $src_ref);
504 try {
505 $interface->execx('%C', $real_command);
506 } catch (CommandException $ex) {
507 $display_command = csprintf(
508 'git merge --squash %R',
509 $src_ref);
511 $error = DrydockCommandError::newFromCommandException($ex)
512 ->setPhase(self::PHASE_SQUASHMERGE)
513 ->setDisplayCommand($display_command);
515 $lease->setAttribute('workingcopy.vcs.error', $error->toDictionary());
516 throw $ex;
520 public function getCommandError(DrydockLease $lease) {
521 return $lease->getAttribute('workingcopy.vcs.error');
524 private function execxv(
525 DrydockCommandInterface $interface,
526 array $commands,
527 array $arguments) {
528 return $this->newExecvFuture($interface, $commands, $arguments)->resolvex();
531 private function newExecvFuture(
532 DrydockCommandInterface $interface,
533 array $commands,
534 array $arguments) {
536 $commands = implode(' && ', $commands);
537 $argv = array_merge(array($commands), $arguments);
539 return call_user_func_array(array($interface, 'getExecFuture'), $argv);