@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3/**
4 * @task config Configuring the Hook Engine
5 * @task hook Hook Execution
6 * @task git Git Hooks
7 * @task hg Mercurial Hooks
8 * @task svn Subversion Hooks
9 * @task internal Internals
10 */
11final class DiffusionCommitHookEngine extends Phobject {
12
13 const ENV_REPOSITORY = 'PHABRICATOR_REPOSITORY';
14 const ENV_USER = 'PHABRICATOR_USER';
15 const ENV_REQUEST = 'PHABRICATOR_REQUEST';
16 const ENV_REMOTE_ADDRESS = 'PHABRICATOR_REMOTE_ADDRESS';
17 const ENV_REMOTE_PROTOCOL = 'PHABRICATOR_REMOTE_PROTOCOL';
18
19 const EMPTY_HASH = '0000000000000000000000000000000000000000';
20
21 private $viewer;
22 private $repository;
23 private $stdin;
24 private $originalArgv;
25 private $subversionTransaction;
26 private $subversionRepository;
27 private $remoteAddress;
28 private $remoteProtocol;
29 private $requestIdentifier;
30 private $transactionKey;
31 private $mercurialHook;
32 private $mercurialCommits = array();
33 private $gitCommits = array();
34 private $startTime;
35
36 private $heraldViewerProjects;
37 private $rejectCode = PhabricatorRepositoryPushLog::REJECT_BROKEN;
38 private $rejectDetails;
39 private $emailPHIDs = array();
40 private $changesets = array();
41 private $changesetsSize = 0;
42 private $filesizeCache = array();
43
44
45/* -( Config )------------------------------------------------------------- */
46
47
48 public function setRemoteProtocol($remote_protocol) {
49 $this->remoteProtocol = $remote_protocol;
50 return $this;
51 }
52
53 public function getRemoteProtocol() {
54 return $this->remoteProtocol;
55 }
56
57 public function setRemoteAddress($remote_address) {
58 $this->remoteAddress = $remote_address;
59 return $this;
60 }
61
62 public function getRemoteAddress() {
63 return $this->remoteAddress;
64 }
65
66 public function setRequestIdentifier($request_identifier) {
67 $this->requestIdentifier = $request_identifier;
68 return $this;
69 }
70
71 public function getRequestIdentifier() {
72 return $this->requestIdentifier;
73 }
74
75 public function setStartTime($start_time) {
76 $this->startTime = $start_time;
77 return $this;
78 }
79
80 public function getStartTime() {
81 return $this->startTime;
82 }
83
84 public function setSubversionTransactionInfo($transaction, $repository) {
85 $this->subversionTransaction = $transaction;
86 $this->subversionRepository = $repository;
87 return $this;
88 }
89
90 public function setStdin($stdin) {
91 $this->stdin = $stdin;
92 return $this;
93 }
94
95 public function getStdin() {
96 return $this->stdin;
97 }
98
99 public function setOriginalArgv(array $original_argv) {
100 $this->originalArgv = $original_argv;
101 return $this;
102 }
103
104 public function getOriginalArgv() {
105 return $this->originalArgv;
106 }
107
108 public function setRepository(PhabricatorRepository $repository) {
109 $this->repository = $repository;
110 return $this;
111 }
112
113 public function getRepository() {
114 return $this->repository;
115 }
116
117 public function setViewer(PhabricatorUser $viewer) {
118 $this->viewer = $viewer;
119 return $this;
120 }
121
122 public function getViewer() {
123 return $this->viewer;
124 }
125
126 public function setMercurialHook($mercurial_hook) {
127 $this->mercurialHook = $mercurial_hook;
128 return $this;
129 }
130
131 public function getMercurialHook() {
132 return $this->mercurialHook;
133 }
134
135
136/* -( Hook Execution )----------------------------------------------------- */
137
138
139 public function execute() {
140 $ref_updates = $this->findRefUpdates();
141 $all_updates = $ref_updates;
142
143 $caught = null;
144 try {
145
146 try {
147 $this->rejectDangerousChanges($ref_updates);
148 } catch (DiffusionCommitHookRejectException $ex) {
149 // If we're rejecting dangerous changes, flag everything that we've
150 // seen as rejected so it's clear that none of it was accepted.
151 $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_DANGEROUS;
152 throw $ex;
153 }
154
155 $content_updates = $this->findContentUpdates($ref_updates);
156 $all_updates = array_merge($ref_updates, $content_updates);
157
158 // If this is an "initial import" (a sizable push to a previously empty
159 // repository) we'll allow enormous changes and disable Herald rules.
160 // These rulesets can consume a large amount of time and memory and are
161 // generally not relevant when importing repository history.
162 $is_initial_import = $this->isInitialImport($all_updates);
163
164 if (!$is_initial_import) {
165 $this->applyHeraldRefRules($ref_updates);
166 }
167
168 try {
169 if (!$is_initial_import) {
170 $this->rejectOversizedFiles($content_updates);
171 }
172 } catch (DiffusionCommitHookRejectException $ex) {
173 // If we're rejecting oversized files, flag everything.
174 $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_OVERSIZED;
175 throw $ex;
176 }
177
178 try {
179 if (!$is_initial_import) {
180 $this->rejectCommitsAffectingTooManyPaths($content_updates);
181 }
182 } catch (DiffusionCommitHookRejectException $ex) {
183 $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_TOUCHES;
184 throw $ex;
185 }
186
187 try {
188 if (!$is_initial_import) {
189 $this->rejectEnormousChanges($content_updates);
190 }
191 } catch (DiffusionCommitHookRejectException $ex) {
192 // If we're rejecting enormous changes, flag everything.
193 $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_ENORMOUS;
194 throw $ex;
195 }
196
197 if (!$is_initial_import) {
198 $this->applyHeraldContentRules($content_updates);
199 }
200
201 // Run custom scripts in `hook.d/` directories.
202 $this->applyCustomHooks($all_updates);
203
204 // If we make it this far, we're accepting these changes. Mark all the
205 // logs as accepted.
206 $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_ACCEPT;
207 } catch (Exception $ex) {
208 // We'll throw this again in a minute, but we want to save all the logs
209 // first.
210 $caught = $ex;
211 }
212
213 // Save all the logs no matter what the outcome was.
214 $event = $this->newPushEvent();
215
216 $event->setRejectCode($this->rejectCode);
217 $event->setRejectDetails($this->rejectDetails);
218
219 $event->saveWithLogs($all_updates);
220
221 if ($caught) {
222 throw $caught;
223 }
224
225 // If this went through cleanly and was an import, set the importing flag
226 // on the repository. It will be cleared once we fully process everything.
227
228 if ($is_initial_import) {
229 $repository = $this->getRepository();
230 $repository->markImporting();
231 }
232
233 if ($this->emailPHIDs) {
234 // If Herald rules triggered email to users, queue a worker to send the
235 // mail. We do this out-of-process so that we block pushes as briefly
236 // as possible.
237
238 // (We do need to pull some commit info here because the commit objects
239 // may not exist yet when this worker runs, which could be immediately.)
240
241 PhabricatorWorker::scheduleTask(
242 'PhabricatorRepositoryPushMailWorker',
243 array(
244 'eventPHID' => $event->getPHID(),
245 'emailPHIDs' => array_values($this->emailPHIDs),
246 'info' => $this->loadCommitInfoForWorker($all_updates),
247 ),
248 array(
249 'priority' => PhabricatorWorker::PRIORITY_ALERTS,
250 ));
251 }
252
253 return 0;
254 }
255
256 private function findRefUpdates() {
257 $type = $this->getRepository()->getVersionControlSystem();
258 switch ($type) {
259 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
260 return $this->findGitRefUpdates();
261 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
262 return $this->findMercurialRefUpdates();
263 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
264 return $this->findSubversionRefUpdates();
265 default:
266 throw new Exception(pht('Unsupported repository type "%s"!', $type));
267 }
268 }
269
270 /**
271 * @param array<PhabricatorRepositoryPushLog> $ref_updates
272 */
273 private function rejectDangerousChanges(array $ref_updates) {
274 assert_instances_of($ref_updates, PhabricatorRepositoryPushLog::class);
275
276 $repository = $this->getRepository();
277 if ($repository->shouldAllowDangerousChanges()) {
278 return;
279 }
280
281 $flag_dangerous = PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS;
282
283 foreach ($ref_updates as $ref_update) {
284 if (!$ref_update->hasChangeFlags($flag_dangerous)) {
285 // This is not a dangerous change.
286 continue;
287 }
288
289 // We either have a branch deletion or a non fast-forward branch update.
290 // Format a message and reject the push.
291
292 $message = pht(
293 "DANGEROUS CHANGE: %s\n".
294 "Dangerous change protection is enabled for this repository.\n".
295 "Edit the repository configuration before making dangerous changes.",
296 $ref_update->getDangerousChangeDescription());
297
298 throw new DiffusionCommitHookRejectException($message);
299 }
300 }
301
302 /**
303 * @param array<PhabricatorRepositoryPushLog> $ref_updates
304 */
305 private function findContentUpdates(array $ref_updates) {
306 assert_instances_of($ref_updates, PhabricatorRepositoryPushLog::class);
307
308 $type = $this->getRepository()->getVersionControlSystem();
309 switch ($type) {
310 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
311 return $this->findGitContentUpdates($ref_updates);
312 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
313 return $this->findMercurialContentUpdates($ref_updates);
314 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
315 return $this->findSubversionContentUpdates($ref_updates);
316 default:
317 throw new Exception(pht('Unsupported repository type "%s"!', $type));
318 }
319 }
320
321
322/* -( Herald )------------------------------------------------------------- */
323
324 private function applyHeraldRefRules(array $ref_updates) {
325 $this->applyHeraldRules(
326 $ref_updates,
327 new HeraldPreCommitRefAdapter());
328 }
329
330 private function applyHeraldContentRules(array $content_updates) {
331 $this->applyHeraldRules(
332 $content_updates,
333 new HeraldPreCommitContentAdapter());
334 }
335
336 private function applyHeraldRules(
337 array $updates,
338 HeraldAdapter $adapter_template) {
339
340 if (!$updates) {
341 return;
342 }
343
344 $viewer = $this->getViewer();
345
346 $adapter_template
347 ->setHookEngine($this)
348 ->setActingAsPHID($viewer->getPHID());
349
350 $engine = new HeraldEngine();
351 $rules = null;
352 $blocking_effect = null;
353 $blocked_update = null;
354 $blocking_xscript = null;
355 foreach ($updates as $update) {
356 $adapter = id(clone $adapter_template)
357 ->setPushLog($update);
358
359 if ($rules === null) {
360 $rules = $engine->loadRulesForAdapter($adapter);
361 }
362
363 $effects = $engine->applyRules($rules, $adapter);
364 $engine->applyEffects($effects, $adapter, $rules);
365 $xscript = $engine->getTranscript();
366
367 // Store any PHIDs we want to send email to for later.
368 foreach ($adapter->getEmailPHIDs() as $email_phid) {
369 $this->emailPHIDs[$email_phid] = $email_phid;
370 }
371
372 $block_action = DiffusionBlockHeraldAction::ACTIONCONST;
373
374 if ($blocking_effect === null) {
375 foreach ($effects as $effect) {
376 if ($effect->getAction() == $block_action) {
377 $blocking_effect = $effect;
378 $blocked_update = $update;
379 $blocking_xscript = $xscript;
380 break;
381 }
382 }
383 }
384 }
385
386 if ($blocking_effect) {
387 $rule = $blocking_effect->getRule();
388
389 $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_HERALD;
390 $this->rejectDetails = $rule->getPHID();
391
392 $message = $blocking_effect->getTarget();
393 if (!strlen($message)) {
394 $message = pht('(None.)');
395 }
396
397 $blocked_ref_name = coalesce(
398 $blocked_update->getRefName(),
399 $blocked_update->getRefNewShort());
400 $blocked_name = $blocked_update->getRefType().'/'.$blocked_ref_name;
401
402 throw new DiffusionCommitHookRejectException(
403 pht(
404 "This push was rejected by Herald push rule %s.\n".
405 " Change: %s\n".
406 " Rule: %s\n".
407 " Reason: %s\n".
408 "Transcript: %s",
409 $rule->getMonogram(),
410 $blocked_name,
411 $rule->getName(),
412 $message,
413 PhabricatorEnv::getProductionURI(
414 '/herald/transcript/'.$blocking_xscript->getID().'/')));
415 }
416 }
417
418 public function loadViewerProjectPHIDsForHerald() {
419 // This just caches the viewer's projects so we don't need to load them
420 // over and over again when applying Herald rules.
421 if ($this->heraldViewerProjects === null) {
422 $this->heraldViewerProjects = id(new PhabricatorProjectQuery())
423 ->setViewer($this->getViewer())
424 ->withMemberPHIDs(array($this->getViewer()->getPHID()))
425 ->execute();
426 }
427
428 return mpull($this->heraldViewerProjects, 'getPHID');
429 }
430
431
432/* -( Git )---------------------------------------------------------------- */
433
434
435 private function findGitRefUpdates() {
436 $ref_updates = array();
437
438 // First, parse stdin, which lists all the ref changes. The input looks
439 // like this:
440 //
441 // <old hash> <new hash> <ref>
442
443 $stdin = $this->getStdin();
444 $lines = phutil_split_lines($stdin, $retain_endings = false);
445 foreach ($lines as $line) {
446 $parts = explode(' ', $line, 3);
447 if (count($parts) != 3) {
448 throw new Exception(pht('Expected "old new ref", got "%s".', $line));
449 }
450
451 $ref_old = $parts[0];
452 $ref_new = $parts[1];
453 $ref_raw = $parts[2];
454
455 if (preg_match('(^refs/heads/)', $ref_raw)) {
456 $ref_type = PhabricatorRepositoryPushLog::REFTYPE_BRANCH;
457 $ref_raw = substr($ref_raw, strlen('refs/heads/'));
458 } else if (preg_match('(^refs/tags/)', $ref_raw)) {
459 $ref_type = PhabricatorRepositoryPushLog::REFTYPE_TAG;
460 $ref_raw = substr($ref_raw, strlen('refs/tags/'));
461 } else {
462 $ref_type = PhabricatorRepositoryPushLog::REFTYPE_REF;
463 }
464
465 $ref_update = $this->newPushLog()
466 ->setRefType($ref_type)
467 ->setRefName($ref_raw)
468 ->setRefOld($ref_old)
469 ->setRefNew($ref_new);
470
471 $ref_updates[] = $ref_update;
472 }
473
474 $this->findGitMergeBases($ref_updates);
475 $this->findGitChangeFlags($ref_updates);
476
477 return $ref_updates;
478 }
479
480 /**
481 * @param array<PhabricatorRepositoryPushLog> $ref_updates
482 */
483 private function findGitMergeBases(array $ref_updates) {
484 assert_instances_of($ref_updates, PhabricatorRepositoryPushLog::class);
485
486 $futures = array();
487 foreach ($ref_updates as $key => $ref_update) {
488 // If the old hash is "00000...", the ref is being created (either a new
489 // branch, or a new tag). If the new hash is "00000...", the ref is being
490 // deleted. If both are nonempty, the ref is being updated. For updates,
491 // we'll figure out the `merge-base` of the old and new objects here. This
492 // lets us reject non-FF changes cheaply; later, we'll figure out exactly
493 // which commits are new.
494 $ref_old = $ref_update->getRefOld();
495 $ref_new = $ref_update->getRefNew();
496
497 if (($ref_old === self::EMPTY_HASH) ||
498 ($ref_new === self::EMPTY_HASH)) {
499 continue;
500 }
501
502 $futures[$key] = $this->getRepository()->getLocalCommandFuture(
503 'merge-base %s %s',
504 $ref_old,
505 $ref_new);
506 }
507
508 $futures = id(new FutureIterator($futures))
509 ->limit(8);
510 foreach ($futures as $key => $future) {
511
512 // If 'old' and 'new' have no common ancestors (for example, a force push
513 // which completely rewrites a ref), `git merge-base` will exit with
514 // an error and no output. It would be nice to find a positive test
515 // for this instead, but I couldn't immediately come up with one. See
516 // T4224. Assume this means there are no ancestors.
517
518 list($err, $stdout) = $future->resolve();
519
520 if ($err) {
521 $merge_base = null;
522 } else {
523 $merge_base = rtrim($stdout, "\n");
524 }
525
526 $ref_update = $ref_updates[$key];
527 $ref_update->setMergeBase($merge_base);
528 }
529
530 return $ref_updates;
531 }
532
533 /**
534 * @param array<PhabricatorRepositoryPushLog> $ref_updates
535 */
536 private function findGitChangeFlags(array $ref_updates) {
537 assert_instances_of($ref_updates, PhabricatorRepositoryPushLog::class);
538
539 foreach ($ref_updates as $key => $ref_update) {
540 $ref_old = $ref_update->getRefOld();
541 $ref_new = $ref_update->getRefNew();
542 $ref_type = $ref_update->getRefType();
543
544 $ref_flags = 0;
545 $dangerous = null;
546
547 if (($ref_old === self::EMPTY_HASH) && ($ref_new === self::EMPTY_HASH)) {
548 // This happens if you try to delete a tag or branch which does not
549 // exist by pushing directly to the ref. Git will warn about it but
550 // allow it. Just call it a delete, without flagging it as dangerous.
551 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE;
552 } else if ($ref_old === self::EMPTY_HASH) {
553 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD;
554 } else if ($ref_new === self::EMPTY_HASH) {
555 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE;
556 if ($ref_type == PhabricatorRepositoryPushLog::REFTYPE_BRANCH) {
557 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS;
558 $dangerous = pht(
559 "The change you're attempting to push deletes the branch '%s'.",
560 $ref_update->getRefName());
561 }
562 } else {
563 $merge_base = $ref_update->getMergeBase();
564 if ($merge_base == $ref_old) {
565 // This is a fast-forward update to an existing branch.
566 // These are safe.
567 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND;
568 } else {
569 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE;
570
571 // For now, we don't consider deleting or moving tags to be a
572 // "dangerous" update. It's way harder to get wrong and should be easy
573 // to recover from once we have better logging. Only add the dangerous
574 // flag if this ref is a branch.
575
576 if ($ref_type == PhabricatorRepositoryPushLog::REFTYPE_BRANCH) {
577 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS;
578
579 $dangerous = pht(
580 "The change you're attempting to push updates the branch '%s' ".
581 "from '%s' to '%s', but this is not a fast-forward. Pushes ".
582 "which rewrite published branch history are dangerous.",
583 $ref_update->getRefName(),
584 $ref_update->getRefOldShort(),
585 $ref_update->getRefNewShort());
586 }
587 }
588 }
589
590 $ref_update->setChangeFlags($ref_flags);
591 if ($dangerous !== null) {
592 $ref_update->attachDangerousChangeDescription($dangerous);
593 }
594 }
595
596 return $ref_updates;
597 }
598
599
600 private function findGitContentUpdates(array $ref_updates) {
601 $flag_delete = PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE;
602
603 $futures = array();
604 foreach ($ref_updates as $key => $ref_update) {
605 if ($ref_update->hasChangeFlags($flag_delete)) {
606 // Deleting a branch or tag can never create any new commits.
607 continue;
608 }
609
610 // NOTE: This piece of magic finds all new commits, by walking backward
611 // from the new value to the value of *any* existing ref in the
612 // repository. Particularly, this will cover the cases of a new branch, a
613 // completely moved tag, etc.
614 $futures[$key] = $this->getRepository()->getLocalCommandFuture(
615 'log %s %s --not --all --',
616 '--format=%H',
617 gitsprintf('%s', $ref_update->getRefNew()));
618 }
619
620 $content_updates = array();
621 $futures = id(new FutureIterator($futures))
622 ->limit(8);
623 foreach ($futures as $key => $future) {
624 list($stdout) = $future->resolvex();
625
626 if (!strlen(trim($stdout))) {
627 // This change doesn't have any new commits. One common case of this
628 // is creating a new tag which points at an existing commit.
629 continue;
630 }
631
632 $commits = phutil_split_lines($stdout, $retain_newlines = false);
633
634 // If we're looking at a branch, mark all of the new commits as on that
635 // branch. It's only possible for these commits to be on updated branches,
636 // since any other branch heads are necessarily behind them.
637 $branch_name = null;
638 $ref_update = $ref_updates[$key];
639 $type_branch = PhabricatorRepositoryPushLog::REFTYPE_BRANCH;
640 if ($ref_update->getRefType() == $type_branch) {
641 $branch_name = $ref_update->getRefName();
642 }
643
644 foreach ($commits as $commit) {
645 if ($branch_name) {
646 $this->gitCommits[$commit][] = $branch_name;
647 }
648 $content_updates[$commit] = $this->newPushLog()
649 ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT)
650 ->setRefNew($commit)
651 ->setChangeFlags(PhabricatorRepositoryPushLog::CHANGEFLAG_ADD);
652 }
653 }
654
655 return $content_updates;
656 }
657
658/* -( Custom )------------------------------------------------------------- */
659
660 private function applyCustomHooks(array $updates) {
661 $args = $this->getOriginalArgv();
662 $stdin = $this->getStdin();
663 $console = PhutilConsole::getConsole();
664
665 $env = array(
666 self::ENV_REPOSITORY => $this->getRepository()->getPHID(),
667 self::ENV_USER => $this->getViewer()->getUsername(),
668 self::ENV_REQUEST => $this->getRequestIdentifier(),
669 self::ENV_REMOTE_PROTOCOL => $this->getRemoteProtocol(),
670 self::ENV_REMOTE_ADDRESS => $this->getRemoteAddress(),
671 );
672
673 $repository = $this->getRepository();
674
675 $env += $repository->getPassthroughEnvironmentalVariables();
676
677 $directories = $repository->getHookDirectories();
678 foreach ($directories as $directory) {
679 $hooks = $this->getExecutablesInDirectory($directory);
680 sort($hooks);
681 foreach ($hooks as $hook) {
682 // NOTE: We're explicitly running the hooks in sequential order to
683 // make this more predictable.
684 $future = id(new ExecFuture('%s %Ls', $hook, $args))
685 ->setEnv($env, $wipe_process_env = false)
686 ->write($stdin);
687
688 list($err, $stdout, $stderr) = $future->resolve();
689 if (!$err) {
690 // This hook ran OK, but echo its output in case there was something
691 // informative.
692 $console->writeOut('%s', $stdout);
693 $console->writeErr('%s', $stderr);
694 continue;
695 }
696
697 $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_EXTERNAL;
698 $this->rejectDetails = basename($hook);
699
700 throw new DiffusionCommitHookRejectException(
701 pht(
702 "This push was rejected by custom hook script '%s':\n\n%s%s",
703 basename($hook),
704 $stdout,
705 $stderr));
706 }
707 }
708 }
709
710 private function getExecutablesInDirectory($directory) {
711 $executables = array();
712
713 if (!Filesystem::pathExists($directory)) {
714 return $executables;
715 }
716
717 foreach (Filesystem::listDirectory($directory) as $path) {
718 $full_path = $directory.DIRECTORY_SEPARATOR.$path;
719 if (!is_executable($full_path)) {
720 // Don't include non-executable files.
721 continue;
722 }
723
724 if (basename($full_path) == 'README') {
725 // Don't include README, even if it is marked as executable. It almost
726 // certainly got caught in the crossfire of a sweeping `chmod`, since
727 // users do this with some frequency.
728 continue;
729 }
730
731 $executables[] = $full_path;
732 }
733
734 return $executables;
735 }
736
737
738/* -( Mercurial )---------------------------------------------------------- */
739
740
741 private function findMercurialRefUpdates() {
742 $hook = $this->getMercurialHook();
743 switch ($hook) {
744 case 'pretxnchangegroup':
745 return $this->findMercurialChangegroupRefUpdates();
746 case 'prepushkey':
747 return $this->findMercurialPushKeyRefUpdates();
748 default:
749 throw new Exception(pht('Unrecognized hook "%s"!', $hook));
750 }
751 }
752
753 private function findMercurialChangegroupRefUpdates() {
754 $hg_node = getenv('HG_NODE');
755 if (!$hg_node) {
756 throw new Exception(
757 pht(
758 'Expected %s in environment!',
759 'HG_NODE'));
760 }
761
762 // NOTE: We need to make sure this is passed to subprocesses, or they won't
763 // be able to see new commits. Mercurial uses this as a marker to determine
764 // whether the pending changes are visible or not.
765 $_ENV['HG_PENDING'] = getenv('HG_PENDING');
766 $repository = $this->getRepository();
767
768 $futures = array();
769
770 foreach (array('old', 'new') as $key) {
771 $futures[$key] = $repository->getLocalCommandFuture(
772 'heads --template %s',
773 '{node}\1{branch}\2');
774 }
775 // Wipe HG_PENDING out of the old environment so we see the pre-commit
776 // state of the repository.
777 $futures['old']->updateEnv('HG_PENDING', null);
778
779 $futures['commits'] = $repository->getLocalCommandFuture(
780 'log --rev %s --template %s',
781 hgsprintf('%s:%s', $hg_node, 'tip'),
782 '{node}\1{branch}\2');
783
784 // Resolve all of the futures now. We don't need the 'commits' future yet,
785 // but it simplifies the logic to just get it out of the way.
786 foreach (new FutureIterator($futures) as $future) {
787 $future->resolve();
788 }
789
790 list($commit_raw) = $futures['commits']->resolvex();
791 $commit_map = $this->parseMercurialCommits($commit_raw);
792 $this->mercurialCommits = $commit_map;
793
794 // NOTE: `hg heads` exits with an error code and no output if the repository
795 // has no heads. Most commonly this happens on a new repository. We know
796 // we can run `hg` successfully since the `hg log` above didn't error, so
797 // just ignore the error code.
798
799 list($err, $old_raw) = $futures['old']->resolve();
800 $old_refs = $this->parseMercurialHeads($old_raw);
801
802 list($err, $new_raw) = $futures['new']->resolve();
803 $new_refs = $this->parseMercurialHeads($new_raw);
804
805 $all_refs = array_keys($old_refs + $new_refs);
806
807 $ref_updates = array();
808 foreach ($all_refs as $ref) {
809 $old_heads = idx($old_refs, $ref, array());
810 $new_heads = idx($new_refs, $ref, array());
811
812 sort($old_heads);
813 sort($new_heads);
814
815 if (!$old_heads && !$new_heads) {
816 // This should never be possible, as it makes no sense. Explode.
817 throw new Exception(
818 pht(
819 'Mercurial repository has no new or old heads for branch "%s" '.
820 'after push. This makes no sense; rejecting change.',
821 $ref));
822 }
823
824 if ($old_heads === $new_heads) {
825 // No changes to this branch, so skip it.
826 continue;
827 }
828
829 $stray_heads = array();
830 $head_map = array();
831
832 if ($old_heads && !$new_heads) {
833 // This is a branch deletion with "--close-branch".
834 foreach ($old_heads as $old_head) {
835 $head_map[$old_head] = array(self::EMPTY_HASH);
836 }
837 } else if (count($old_heads) > 1) {
838 // HORRIBLE: In Mercurial, branches can have multiple heads. If the
839 // old branch had multiple heads, we need to figure out which new
840 // heads descend from which old heads, so we can tell whether you're
841 // actively creating new heads (dangerous) or just working in a
842 // repository that's already full of garbage (strongly discouraged but
843 // not as inherently dangerous). These cases should be very uncommon.
844
845 // NOTE: We're only looking for heads on the same branch. The old
846 // tip of the branch may be the branchpoint for other branches, but that
847 // is OK.
848
849 $dfutures = array();
850 foreach ($old_heads as $old_head) {
851 $dfutures[$old_head] = $repository->getLocalCommandFuture(
852 'log --branch %s --rev %s --template %s',
853 $ref,
854 hgsprintf('(descendants(%s) and head())', $old_head),
855 '{node}\1');
856 }
857
858 foreach (new FutureIterator($dfutures) as $future_head => $dfuture) {
859 list($stdout) = $dfuture->resolvex();
860 $descendant_heads = array_filter(explode("\1", $stdout));
861 if ($descendant_heads) {
862 // This old head has at least one descendant in the push.
863 $head_map[$future_head] = $descendant_heads;
864 } else {
865 // This old head has no descendants, so it is being deleted.
866 $head_map[$future_head] = array(self::EMPTY_HASH);
867 }
868 }
869
870 // Now, find all the new stray heads this push creates, if any. These
871 // are new heads which do not descend from the old heads.
872 $seen = array_fuse(array_mergev($head_map));
873 foreach ($new_heads as $new_head) {
874 if ($new_head === self::EMPTY_HASH) {
875 // If a branch head is being deleted, don't insert it as an add.
876 continue;
877 }
878 if (empty($seen[$new_head])) {
879 $head_map[self::EMPTY_HASH][] = $new_head;
880 }
881 }
882 } else if ($old_heads) {
883 $head_map[head($old_heads)] = $new_heads;
884 } else {
885 $head_map[self::EMPTY_HASH] = $new_heads;
886 }
887
888 foreach ($head_map as $old_head => $child_heads) {
889 foreach ($child_heads as $new_head) {
890 if ($new_head === $old_head) {
891 continue;
892 }
893
894 $ref_flags = 0;
895 $dangerous = null;
896 if ($old_head == self::EMPTY_HASH) {
897 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD;
898 } else {
899 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND;
900 }
901
902
903 $deletes_existing_head = ($new_head == self::EMPTY_HASH);
904 $splits_existing_head = (count($child_heads) > 1);
905 $creates_duplicate_head = ($old_head == self::EMPTY_HASH) &&
906 (count($head_map) > 1);
907
908 if ($splits_existing_head || $creates_duplicate_head) {
909 $readable_child_heads = array();
910 foreach ($child_heads as $child_head) {
911 $readable_child_heads[] = substr($child_head, 0, 12);
912 }
913
914 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS;
915
916 if ($splits_existing_head) {
917 // We're splitting an existing head into two or more heads.
918 // This is dangerous, and a super bad idea. Note that we're only
919 // raising this if you're actively splitting a branch head. If a
920 // head split in the past, we don't consider appends to it
921 // to be dangerous.
922 $dangerous = pht(
923 "The change you're attempting to push splits the head of ".
924 "branch '%s' into multiple heads: %s. This is inadvisable ".
925 "and dangerous.",
926 $ref,
927 implode(', ', $readable_child_heads));
928 } else {
929 // We're adding a second (or more) head to a branch. The new
930 // head is not a descendant of any old head.
931 $dangerous = pht(
932 "The change you're attempting to push creates new, divergent ".
933 "heads for the branch '%s': %s. This is inadvisable and ".
934 "dangerous.",
935 $ref,
936 implode(', ', $readable_child_heads));
937 }
938 }
939
940 if ($deletes_existing_head) {
941 // TODO: Somewhere in here we should be setting CHANGEFLAG_REWRITE
942 // if we are also creating at least one other head to replace
943 // this one.
944
945 // NOTE: In Git, this is a dangerous change, but it is not dangerous
946 // in Mercurial. Mercurial branches are version controlled, and
947 // Mercurial does not prompt you for any special flags when pushing
948 // a `--close-branch` commit by default.
949
950 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE;
951 }
952
953 $ref_update = $this->newPushLog()
954 ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_BRANCH)
955 ->setRefName($ref)
956 ->setRefOld($old_head)
957 ->setRefNew($new_head)
958 ->setChangeFlags($ref_flags);
959
960 if ($dangerous !== null) {
961 $ref_update->attachDangerousChangeDescription($dangerous);
962 }
963
964 $ref_updates[] = $ref_update;
965 }
966 }
967 }
968
969 return $ref_updates;
970 }
971
972 private function findMercurialPushKeyRefUpdates() {
973 $key_namespace = getenv('HG_NAMESPACE');
974
975 if ($key_namespace === 'phases') {
976 // Mercurial changes commit phases as part of normal push operations. We
977 // just ignore these, as they don't seem to represent anything
978 // interesting.
979 return array();
980 }
981
982 $key_name = getenv('HG_KEY');
983
984 $key_old = getenv('HG_OLD');
985 if (!$key_old || !strlen($key_old)) {
986 $key_old = null;
987 }
988
989 $key_new = getenv('HG_NEW');
990 if (!$key_new || !strlen($key_new)) {
991 $key_new = null;
992 }
993
994 if ($key_namespace !== 'bookmarks') {
995 throw new Exception(
996 pht(
997 "Unknown Mercurial key namespace '%s', with key '%s' (%s -> %s). ".
998 "Rejecting push.",
999 $key_namespace,
1000 $key_name,
1001 coalesce($key_old, pht('null')),
1002 coalesce($key_new, pht('null'))));
1003 }
1004
1005 if ($key_old === $key_new) {
1006 // We get a callback when the bookmark doesn't change. Just ignore this,
1007 // as it's a no-op.
1008 return array();
1009 }
1010
1011 $ref_flags = 0;
1012 $merge_base = null;
1013 if ($key_old === null) {
1014 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD;
1015 } else if ($key_new === null) {
1016 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE;
1017 } else {
1018 list($merge_base_raw) = $this->getRepository()->execxLocalCommand(
1019 'log --template %s --rev %s',
1020 '{node}',
1021 hgsprintf('ancestor(%s, %s)', $key_old, $key_new));
1022
1023 if (strlen(trim($merge_base_raw))) {
1024 $merge_base = trim($merge_base_raw);
1025 }
1026
1027 if ($merge_base && ($merge_base === $key_old)) {
1028 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND;
1029 } else {
1030 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE;
1031 }
1032 }
1033
1034 $ref_update = $this->newPushLog()
1035 ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_BOOKMARK)
1036 ->setRefName($key_name)
1037 ->setRefOld(coalesce($key_old, self::EMPTY_HASH))
1038 ->setRefNew(coalesce($key_new, self::EMPTY_HASH))
1039 ->setChangeFlags($ref_flags);
1040
1041 return array($ref_update);
1042 }
1043
1044 private function findMercurialContentUpdates(array $ref_updates) {
1045 $content_updates = array();
1046
1047 foreach ($this->mercurialCommits as $commit => $branches) {
1048 $content_updates[$commit] = $this->newPushLog()
1049 ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT)
1050 ->setRefNew($commit)
1051 ->setChangeFlags(PhabricatorRepositoryPushLog::CHANGEFLAG_ADD);
1052 }
1053
1054 return $content_updates;
1055 }
1056
1057 private function parseMercurialCommits($raw) {
1058 $commits_lines = explode("\2", $raw);
1059 $commits_lines = array_filter($commits_lines);
1060 $commit_map = array();
1061 foreach ($commits_lines as $commit_line) {
1062 list($node, $branch) = explode("\1", $commit_line);
1063 $commit_map[$node] = array($branch);
1064 }
1065
1066 return $commit_map;
1067 }
1068
1069 private function parseMercurialHeads($raw) {
1070 $heads_map = $this->parseMercurialCommits($raw);
1071
1072 $heads = array();
1073 foreach ($heads_map as $commit => $branches) {
1074 foreach ($branches as $branch) {
1075 $heads[$branch][] = $commit;
1076 }
1077 }
1078
1079 return $heads;
1080 }
1081
1082
1083/* -( Subversion )--------------------------------------------------------- */
1084
1085
1086 private function findSubversionRefUpdates() {
1087 // Subversion doesn't have any kind of mutable ref metadata.
1088 return array();
1089 }
1090
1091 private function findSubversionContentUpdates(array $ref_updates) {
1092 list($youngest) = execx(
1093 'svnlook youngest %s',
1094 $this->subversionRepository);
1095 $ref_new = (int)$youngest + 1;
1096
1097 $ref_flags = 0;
1098 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD;
1099 $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND;
1100
1101 $ref_content = $this->newPushLog()
1102 ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT)
1103 ->setRefNew($ref_new)
1104 ->setChangeFlags($ref_flags);
1105
1106 return array($ref_content);
1107 }
1108
1109
1110/* -( Internals )---------------------------------------------------------- */
1111
1112
1113 private function newPushLog() {
1114 // NOTE: We generate PHIDs up front so the Herald transcripts can pick them
1115 // up.
1116 $phid = id(new PhabricatorRepositoryPushLog())->generatePHID();
1117
1118 $device = AlmanacKeys::getLiveDevice();
1119 if ($device) {
1120 $device_phid = $device->getPHID();
1121 } else {
1122 $device_phid = null;
1123 }
1124
1125 return PhabricatorRepositoryPushLog::initializeNewLog($this->getViewer())
1126 ->setPHID($phid)
1127 ->setDevicePHID($device_phid)
1128 ->setRepositoryPHID($this->getRepository()->getPHID())
1129 ->attachRepository($this->getRepository())
1130 ->setEpoch(PhabricatorTime::getNow());
1131 }
1132
1133 private function newPushEvent() {
1134 $viewer = $this->getViewer();
1135
1136 $hook_start = $this->getStartTime();
1137
1138 $event = PhabricatorRepositoryPushEvent::initializeNewEvent($viewer)
1139 ->setRepositoryPHID($this->getRepository()->getPHID())
1140 ->setRemoteAddress($this->getRemoteAddress())
1141 ->setRemoteProtocol($this->getRemoteProtocol())
1142 ->setEpoch(PhabricatorTime::getNow())
1143 ->setHookWait(phutil_microseconds_since($hook_start));
1144
1145 $identifier = $this->getRequestIdentifier();
1146 if ($identifier !== null && strlen($identifier)) {
1147 $event->setRequestIdentifier($identifier);
1148 }
1149
1150 return $event;
1151 }
1152
1153 private function rejectEnormousChanges(array $content_updates) {
1154 $repository = $this->getRepository();
1155 if ($repository->shouldAllowEnormousChanges()) {
1156 return;
1157 }
1158
1159 // See T13142. Don't cache more than 64MB of changesets. For normal small
1160 // pushes, caching everything here can let us hit the cache from Herald if
1161 // we need to run content rules, which speeds things up a bit. For large
1162 // pushes, we may not be able to hold everything in memory.
1163 $cache_limit = 1024 * 1024 * 64;
1164
1165 foreach ($content_updates as $update) {
1166 $identifier = $update->getRefNew();
1167 try {
1168 $info = $this->loadChangesetsForCommit($identifier);
1169 list($changesets, $size) = $info;
1170
1171 if ($this->changesetsSize + $size <= $cache_limit) {
1172 $this->changesets[$identifier] = $changesets;
1173 $this->changesetsSize += $size;
1174 }
1175 } catch (Exception $ex) {
1176 $this->changesets[$identifier] = $ex;
1177
1178 $message = pht(
1179 'ENORMOUS CHANGE'.
1180 "\n".
1181 'Enormous change protection is enabled for this repository, but '.
1182 'you are pushing an enormous change ("%s"). Edit the repository '.
1183 'configuration before making enormous changes.'.
1184 "\n\n".
1185 "Content Exception: %s",
1186 $identifier,
1187 $ex->getMessage());
1188
1189 throw new DiffusionCommitHookRejectException($message);
1190 }
1191 }
1192 }
1193
1194 private function loadChangesetsForCommit($identifier) {
1195 $byte_limit = HeraldCommitAdapter::getEnormousByteLimit();
1196 $time_limit = HeraldCommitAdapter::getEnormousTimeLimit();
1197
1198 $vcs = $this->getRepository()->getVersionControlSystem();
1199 switch ($vcs) {
1200 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
1201 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
1202 // For git and hg, we can use normal commands.
1203 $drequest = DiffusionRequest::newFromDictionary(
1204 array(
1205 'repository' => $this->getRepository(),
1206 'user' => $this->getViewer(),
1207 'commit' => $identifier,
1208 ));
1209
1210 $raw_diff = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest)
1211 ->setTimeout($time_limit)
1212 ->setByteLimit($byte_limit)
1213 ->setLinesOfContext(0)
1214 ->executeInline();
1215 break;
1216 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
1217 // TODO: This diff has 3 lines of context, which produces slightly
1218 // incorrect "added file content" and "removed file content" results.
1219 // This may also choke on binaries, but "svnlook diff" does not support
1220 // the "--diff-cmd" flag.
1221
1222 // For subversion, we need to use `svnlook`.
1223 $future = new ExecFuture(
1224 'svnlook diff -t %s %s',
1225 $this->subversionTransaction,
1226 $this->subversionRepository);
1227
1228 $future->setTimeout($time_limit);
1229 $future->setStdoutSizeLimit($byte_limit);
1230 $future->setStderrSizeLimit($byte_limit);
1231
1232 list($raw_diff) = $future->resolvex();
1233 break;
1234 default:
1235 throw new Exception(pht("Unknown VCS '%s!'", $vcs));
1236 }
1237
1238 if (strlen($raw_diff) >= $byte_limit) {
1239 throw new Exception(
1240 pht(
1241 'The raw text of this change ("%s") is enormous (larger than %s '.
1242 'bytes).',
1243 $identifier,
1244 new PhutilNumber($byte_limit)));
1245 }
1246
1247 if (!strlen($raw_diff)) {
1248 // If the commit is actually empty, just return no changesets.
1249 return array(array(), 0);
1250 }
1251
1252 $parser = new ArcanistDiffParser();
1253 $changes = $parser->parseDiff($raw_diff);
1254 $diff = DifferentialDiff::newEphemeralFromRawChanges(
1255 $changes);
1256
1257 $changesets = $diff->getChangesets();
1258 $size = strlen($raw_diff);
1259
1260 return array($changesets, $size);
1261 }
1262
1263 public function getChangesetsForCommit($identifier) {
1264 if (isset($this->changesets[$identifier])) {
1265 $cached = $this->changesets[$identifier];
1266
1267 if ($cached instanceof Exception) {
1268 throw $cached;
1269 }
1270
1271 return $cached;
1272 }
1273
1274 $info = $this->loadChangesetsForCommit($identifier);
1275 list($changesets, $size) = $info;
1276 return $changesets;
1277 }
1278
1279 private function rejectOversizedFiles(array $content_updates) {
1280 $repository = $this->getRepository();
1281
1282 $limit = $repository->getFilesizeLimit();
1283 if (!$limit) {
1284 return;
1285 }
1286
1287 foreach ($content_updates as $update) {
1288 $identifier = $update->getRefNew();
1289
1290 $sizes = $this->getFileSizesForCommit($identifier);
1291
1292 foreach ($sizes as $path => $size) {
1293 if ($size <= $limit) {
1294 continue;
1295 }
1296
1297 $message = pht(
1298 'OVERSIZED FILE'.
1299 "\n".
1300 'This repository ("%s") is configured with a maximum individual '.
1301 'file size limit, but you are pushing a change ("%s") which causes '.
1302 'the size of a file ("%s") to exceed the limit. The commit makes '.
1303 'the file %s bytes long, but the limit for this repository is '.
1304 '%s bytes.',
1305 $repository->getDisplayName(),
1306 $identifier,
1307 $path,
1308 new PhutilNumber($size),
1309 new PhutilNumber($limit));
1310
1311 throw new DiffusionCommitHookRejectException($message);
1312 }
1313 }
1314 }
1315
1316 private function rejectCommitsAffectingTooManyPaths(array $content_updates) {
1317 $repository = $this->getRepository();
1318
1319 $limit = $repository->getTouchLimit();
1320 if (!$limit) {
1321 return;
1322 }
1323
1324 foreach ($content_updates as $update) {
1325 $identifier = $update->getRefNew();
1326
1327 $sizes = $this->getFileSizesForCommit($identifier);
1328 if (count($sizes) > $limit) {
1329 $message = pht(
1330 'COMMIT AFFECTS TOO MANY PATHS'.
1331 "\n".
1332 'This repository ("%s") is configured with a touched files limit '.
1333 'that caps the maximum number of paths any single commit may '.
1334 'affect. You are pushing a change ("%s") which exceeds this '.
1335 'limit: it affects %s paths, but the largest number of paths any '.
1336 'commit may affect is %s paths.',
1337 $repository->getDisplayName(),
1338 $identifier,
1339 phutil_count($sizes),
1340 new PhutilNumber($limit));
1341
1342 throw new DiffusionCommitHookRejectException($message);
1343 }
1344 }
1345 }
1346
1347 public function getFileSizesForCommit($identifier) {
1348 if (!isset($this->filesizeCache[$identifier])) {
1349 $file_sizes = $this->loadFileSizesForCommit($identifier);
1350 $this->filesizeCache[$identifier] = $file_sizes;
1351 }
1352
1353 return $this->filesizeCache[$identifier];
1354 }
1355
1356 private function loadFileSizesForCommit($identifier) {
1357 $repository = $this->getRepository();
1358
1359 return id(new DiffusionLowLevelFilesizeQuery())
1360 ->setRepository($repository)
1361 ->withIdentifier($identifier)
1362 ->execute();
1363 }
1364
1365 public function loadCommitRefForCommit($identifier) {
1366 $repository = $this->getRepository();
1367 $vcs = $repository->getVersionControlSystem();
1368 switch ($vcs) {
1369 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
1370 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
1371 return id(new DiffusionLowLevelCommitQuery())
1372 ->setRepository($repository)
1373 ->withIdentifier($identifier)
1374 ->execute();
1375 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
1376 // For subversion, we need to use `svnlook`.
1377 list($message) = execx(
1378 'svnlook log -t %s %s',
1379 $this->subversionTransaction,
1380 $this->subversionRepository);
1381
1382 return id(new DiffusionCommitRef())
1383 ->setMessage($message);
1384 default:
1385 throw new Exception(pht("Unknown VCS '%s!'", $vcs));
1386 }
1387 }
1388
1389 public function loadBranches($identifier) {
1390 $repository = $this->getRepository();
1391 $vcs = $repository->getVersionControlSystem();
1392 switch ($vcs) {
1393 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
1394 return idx($this->gitCommits, $identifier, array());
1395 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
1396 // NOTE: This will be "the branch the commit was made to", not
1397 // "a list of all branch heads which descend from the commit".
1398 // This is consistent with Mercurial, but possibly confusing.
1399 return idx($this->mercurialCommits, $identifier, array());
1400 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
1401 // Subversion doesn't have branches.
1402 return array();
1403 }
1404 }
1405
1406 private function loadCommitInfoForWorker(array $all_updates) {
1407 $type_commit = PhabricatorRepositoryPushLog::REFTYPE_COMMIT;
1408
1409 $map = array();
1410 foreach ($all_updates as $update) {
1411 if ($update->getRefType() != $type_commit) {
1412 continue;
1413 }
1414 $map[$update->getRefNew()] = array();
1415 }
1416
1417 foreach ($map as $identifier => $info) {
1418 $ref = $this->loadCommitRefForCommit($identifier);
1419 $map[$identifier] += array(
1420 'summary' => $ref->getSummary(),
1421 'branches' => $this->loadBranches($identifier),
1422 );
1423 }
1424
1425 return $map;
1426 }
1427
1428 private function isInitialImport(array $all_updates) {
1429 $repository = $this->getRepository();
1430
1431 $vcs = $repository->getVersionControlSystem();
1432 switch ($vcs) {
1433 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
1434 // There is no meaningful way to import history into Subversion by
1435 // pushing.
1436 return false;
1437 default:
1438 break;
1439 }
1440
1441 // Now, apply a heuristic to guess whether this is a normal commit or
1442 // an initial import. We guess something is an initial import if:
1443 //
1444 // - the repository is currently empty; and
1445 // - it pushes more than 7 commits at once.
1446 //
1447 // The number "7" is chosen arbitrarily as seeming reasonable. We could
1448 // also look at author data (do the commits come from multiple different
1449 // authors?) and commit date data (is the oldest commit more than 48 hours
1450 // old), but we don't have immediate access to those and this simple
1451 // heuristic might be good enough.
1452
1453 $commit_count = 0;
1454 $type_commit = PhabricatorRepositoryPushLog::REFTYPE_COMMIT;
1455 foreach ($all_updates as $update) {
1456 if ($update->getRefType() != $type_commit) {
1457 continue;
1458 }
1459 $commit_count++;
1460 }
1461
1462 if ($commit_count <= PhabricatorRepository::IMPORT_THRESHOLD) {
1463 // If this pushes a very small number of commits, assume it's an
1464 // initial commit or stack of a few initial commits.
1465 return false;
1466 }
1467
1468 $any_commits = id(new DiffusionCommitQuery())
1469 ->setViewer($this->getViewer())
1470 ->withRepository($repository)
1471 ->setLimit(1)
1472 ->execute();
1473
1474 if ($any_commits) {
1475 // If the repository already has commits, this isn't an import.
1476 return false;
1477 }
1478
1479 return true;
1480 }
1481
1482}