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