Something went wrong. Try again.
@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
Something went wrong. Try again.
49 kB · 1482 lines
PHP
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483<?php
/** * @task config Configuring the Hook Engine * @task hook Hook Execution * @task git Git Hooks * @task hg Mercurial Hooks * @task svn Subversion Hooks * @task internal Internals */final class DiffusionCommitHookEngine extends Phobject {
const ENV_REPOSITORY = 'PHABRICATOR_REPOSITORY'; const ENV_USER = 'PHABRICATOR_USER'; const ENV_REQUEST = 'PHABRICATOR_REQUEST'; const ENV_REMOTE_ADDRESS = 'PHABRICATOR_REMOTE_ADDRESS'; const ENV_REMOTE_PROTOCOL = 'PHABRICATOR_REMOTE_PROTOCOL';
const EMPTY_HASH = '0000000000000000000000000000000000000000';
private $viewer; private $repository; private $stdin; private $originalArgv; private $subversionTransaction; private $subversionRepository; private $remoteAddress; private $remoteProtocol; private $requestIdentifier; private $transactionKey; private $mercurialHook; private $mercurialCommits = array(); private $gitCommits = array(); private $startTime;
private $heraldViewerProjects; private $rejectCode = PhabricatorRepositoryPushLog::REJECT_BROKEN; private $rejectDetails; private $emailPHIDs = array(); private $changesets = array(); private $changesetsSize = 0; private $filesizeCache = array();
/* -( Config )------------------------------------------------------------- */
public function setRemoteProtocol($remote_protocol) { $this->remoteProtocol = $remote_protocol; return $this; }
public function getRemoteProtocol() { return $this->remoteProtocol; }
public function setRemoteAddress($remote_address) { $this->remoteAddress = $remote_address; return $this; }
public function getRemoteAddress() { return $this->remoteAddress; }
public function setRequestIdentifier($request_identifier) { $this->requestIdentifier = $request_identifier; return $this; }
public function getRequestIdentifier() { return $this->requestIdentifier; }
public function setStartTime($start_time) { $this->startTime = $start_time; return $this; }
public function getStartTime() { return $this->startTime; }
public function setSubversionTransactionInfo($transaction, $repository) { $this->subversionTransaction = $transaction; $this->subversionRepository = $repository; return $this; }
public function setStdin($stdin) { $this->stdin = $stdin; return $this; }
public function getStdin() { return $this->stdin; }
public function setOriginalArgv(array $original_argv) { $this->originalArgv = $original_argv; return $this; }
public function getOriginalArgv() { return $this->originalArgv; }
public function setRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; }
public function getRepository() { return $this->repository; }
public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; }
public function getViewer() { return $this->viewer; }
public function setMercurialHook($mercurial_hook) { $this->mercurialHook = $mercurial_hook; return $this; }
public function getMercurialHook() { return $this->mercurialHook; }
/* -( Hook Execution )----------------------------------------------------- */
public function execute() { $ref_updates = $this->findRefUpdates(); $all_updates = $ref_updates;
$caught = null; try {
try { $this->rejectDangerousChanges($ref_updates); } catch (DiffusionCommitHookRejectException $ex) { // If we're rejecting dangerous changes, flag everything that we've // seen as rejected so it's clear that none of it was accepted. $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_DANGEROUS; throw $ex; }
$content_updates = $this->findContentUpdates($ref_updates); $all_updates = array_merge($ref_updates, $content_updates);
// If this is an "initial import" (a sizable push to a previously empty // repository) we'll allow enormous changes and disable Herald rules. // These rulesets can consume a large amount of time and memory and are // generally not relevant when importing repository history. $is_initial_import = $this->isInitialImport($all_updates);
if (!$is_initial_import) { $this->applyHeraldRefRules($ref_updates); }
try { if (!$is_initial_import) { $this->rejectOversizedFiles($content_updates); } } catch (DiffusionCommitHookRejectException $ex) { // If we're rejecting oversized files, flag everything. $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_OVERSIZED; throw $ex; }
try { if (!$is_initial_import) { $this->rejectCommitsAffectingTooManyPaths($content_updates); } } catch (DiffusionCommitHookRejectException $ex) { $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_TOUCHES; throw $ex; }
try { if (!$is_initial_import) { $this->rejectEnormousChanges($content_updates); } } catch (DiffusionCommitHookRejectException $ex) { // If we're rejecting enormous changes, flag everything. $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_ENORMOUS; throw $ex; }
if (!$is_initial_import) { $this->applyHeraldContentRules($content_updates); }
// Run custom scripts in `hook.d/` directories. $this->applyCustomHooks($all_updates);
// If we make it this far, we're accepting these changes. Mark all the // logs as accepted. $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_ACCEPT; } catch (Exception $ex) { // We'll throw this again in a minute, but we want to save all the logs // first. $caught = $ex; }
// Save all the logs no matter what the outcome was. $event = $this->newPushEvent();
$event->setRejectCode($this->rejectCode); $event->setRejectDetails($this->rejectDetails);
$event->saveWithLogs($all_updates);
if ($caught) { throw $caught; }
// If this went through cleanly and was an import, set the importing flag // on the repository. It will be cleared once we fully process everything.
if ($is_initial_import) { $repository = $this->getRepository(); $repository->markImporting(); }
if ($this->emailPHIDs) { // If Herald rules triggered email to users, queue a worker to send the // mail. We do this out-of-process so that we block pushes as briefly // as possible.
// (We do need to pull some commit info here because the commit objects // may not exist yet when this worker runs, which could be immediately.)
PhabricatorWorker::scheduleTask( 'PhabricatorRepositoryPushMailWorker', array( 'eventPHID' => $event->getPHID(), 'emailPHIDs' => array_values($this->emailPHIDs), 'info' => $this->loadCommitInfoForWorker($all_updates), ), array( 'priority' => PhabricatorWorker::PRIORITY_ALERTS, )); }
return 0; }
private function findRefUpdates() { $type = $this->getRepository()->getVersionControlSystem(); switch ($type) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: return $this->findGitRefUpdates(); case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: return $this->findMercurialRefUpdates(); case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: return $this->findSubversionRefUpdates(); default: throw new Exception(pht('Unsupported repository type "%s"!', $type)); } }
/** * @param array<PhabricatorRepositoryPushLog> $ref_updates */ private function rejectDangerousChanges(array $ref_updates) { assert_instances_of($ref_updates, PhabricatorRepositoryPushLog::class);
$repository = $this->getRepository(); if ($repository->shouldAllowDangerousChanges()) { return; }
$flag_dangerous = PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS;
foreach ($ref_updates as $ref_update) { if (!$ref_update->hasChangeFlags($flag_dangerous)) { // This is not a dangerous change. continue; }
// We either have a branch deletion or a non fast-forward branch update. // Format a message and reject the push.
$message = pht( "DANGEROUS CHANGE: %s\n". "Dangerous change protection is enabled for this repository.\n". "Edit the repository configuration before making dangerous changes.", $ref_update->getDangerousChangeDescription());
throw new DiffusionCommitHookRejectException($message); } }
/** * @param array<PhabricatorRepositoryPushLog> $ref_updates */ private function findContentUpdates(array $ref_updates) { assert_instances_of($ref_updates, PhabricatorRepositoryPushLog::class);
$type = $this->getRepository()->getVersionControlSystem(); switch ($type) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: return $this->findGitContentUpdates($ref_updates); case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: return $this->findMercurialContentUpdates($ref_updates); case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: return $this->findSubversionContentUpdates($ref_updates); default: throw new Exception(pht('Unsupported repository type "%s"!', $type)); } }
/* -( Herald )------------------------------------------------------------- */
private function applyHeraldRefRules(array $ref_updates) { $this->applyHeraldRules( $ref_updates, new HeraldPreCommitRefAdapter()); }
private function applyHeraldContentRules(array $content_updates) { $this->applyHeraldRules( $content_updates, new HeraldPreCommitContentAdapter()); }
private function applyHeraldRules( array $updates, HeraldAdapter $adapter_template) {
if (!$updates) { return; }
$viewer = $this->getViewer();
$adapter_template ->setHookEngine($this) ->setActingAsPHID($viewer->getPHID());
$engine = new HeraldEngine(); $rules = null; $blocking_effect = null; $blocked_update = null; $blocking_xscript = null; foreach ($updates as $update) { $adapter = id(clone $adapter_template) ->setPushLog($update);
if ($rules === null) { $rules = $engine->loadRulesForAdapter($adapter); }
$effects = $engine->applyRules($rules, $adapter); $engine->applyEffects($effects, $adapter, $rules); $xscript = $engine->getTranscript();
// Store any PHIDs we want to send email to for later. foreach ($adapter->getEmailPHIDs() as $email_phid) { $this->emailPHIDs[$email_phid] = $email_phid; }
$block_action = DiffusionBlockHeraldAction::ACTIONCONST;
if ($blocking_effect === null) { foreach ($effects as $effect) { if ($effect->getAction() == $block_action) { $blocking_effect = $effect; $blocked_update = $update; $blocking_xscript = $xscript; break; } } } }
if ($blocking_effect) { $rule = $blocking_effect->getRule();
$this->rejectCode = PhabricatorRepositoryPushLog::REJECT_HERALD; $this->rejectDetails = $rule->getPHID();
$message = $blocking_effect->getTarget(); if (!strlen($message)) { $message = pht('(None.)'); }
$blocked_ref_name = coalesce( $blocked_update->getRefName(), $blocked_update->getRefNewShort()); $blocked_name = $blocked_update->getRefType().'/'.$blocked_ref_name;
throw new DiffusionCommitHookRejectException( pht( "This push was rejected by Herald push rule %s.\n". " Change: %s\n". " Rule: %s\n". " Reason: %s\n". "Transcript: %s", $rule->getMonogram(), $blocked_name, $rule->getName(), $message, PhabricatorEnv::getProductionURI( '/herald/transcript/'.$blocking_xscript->getID().'/'))); } }
public function loadViewerProjectPHIDsForHerald() { // This just caches the viewer's projects so we don't need to load them // over and over again when applying Herald rules. if ($this->heraldViewerProjects === null) { $this->heraldViewerProjects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withMemberPHIDs(array($this->getViewer()->getPHID())) ->execute(); }
return mpull($this->heraldViewerProjects, 'getPHID'); }
/* -( Git )---------------------------------------------------------------- */
private function findGitRefUpdates() { $ref_updates = array();
// First, parse stdin, which lists all the ref changes. The input looks // like this: // // <old hash> <new hash> <ref>
$stdin = $this->getStdin(); $lines = phutil_split_lines($stdin, $retain_endings = false); foreach ($lines as $line) { $parts = explode(' ', $line, 3); if (count($parts) != 3) { throw new Exception(pht('Expected "old new ref", got "%s".', $line)); }
$ref_old = $parts[0]; $ref_new = $parts[1]; $ref_raw = $parts[2];
if (preg_match('(^refs/heads/)', $ref_raw)) { $ref_type = PhabricatorRepositoryPushLog::REFTYPE_BRANCH; $ref_raw = substr($ref_raw, strlen('refs/heads/')); } else if (preg_match('(^refs/tags/)', $ref_raw)) { $ref_type = PhabricatorRepositoryPushLog::REFTYPE_TAG; $ref_raw = substr($ref_raw, strlen('refs/tags/')); } else { $ref_type = PhabricatorRepositoryPushLog::REFTYPE_REF; }
$ref_update = $this->newPushLog() ->setRefType($ref_type) ->setRefName($ref_raw) ->setRefOld($ref_old) ->setRefNew($ref_new);
$ref_updates[] = $ref_update; }
$this->findGitMergeBases($ref_updates); $this->findGitChangeFlags($ref_updates);
return $ref_updates; }
/** * @param array<PhabricatorRepositoryPushLog> $ref_updates */ private function findGitMergeBases(array $ref_updates) { assert_instances_of($ref_updates, PhabricatorRepositoryPushLog::class);
$futures = array(); foreach ($ref_updates as $key => $ref_update) { // If the old hash is "00000...", the ref is being created (either a new // branch, or a new tag). If the new hash is "00000...", the ref is being // deleted. If both are nonempty, the ref is being updated. For updates, // we'll figure out the `merge-base` of the old and new objects here. This // lets us reject non-FF changes cheaply; later, we'll figure out exactly // which commits are new. $ref_old = $ref_update->getRefOld(); $ref_new = $ref_update->getRefNew();
if (($ref_old === self::EMPTY_HASH) || ($ref_new === self::EMPTY_HASH)) { continue; }
$futures[$key] = $this->getRepository()->getLocalCommandFuture( 'merge-base %s %s', $ref_old, $ref_new); }
$futures = id(new FutureIterator($futures)) ->limit(8); foreach ($futures as $key => $future) {
// If 'old' and 'new' have no common ancestors (for example, a force push // which completely rewrites a ref), `git merge-base` will exit with // an error and no output. It would be nice to find a positive test // for this instead, but I couldn't immediately come up with one. See // T4224. Assume this means there are no ancestors.
list($err, $stdout) = $future->resolve();
if ($err) { $merge_base = null; } else { $merge_base = rtrim($stdout, "\n"); }
$ref_update = $ref_updates[$key]; $ref_update->setMergeBase($merge_base); }
return $ref_updates; }
/** * @param array<PhabricatorRepositoryPushLog> $ref_updates */ private function findGitChangeFlags(array $ref_updates) { assert_instances_of($ref_updates, PhabricatorRepositoryPushLog::class);
foreach ($ref_updates as $key => $ref_update) { $ref_old = $ref_update->getRefOld(); $ref_new = $ref_update->getRefNew(); $ref_type = $ref_update->getRefType();
$ref_flags = 0; $dangerous = null;
if (($ref_old === self::EMPTY_HASH) && ($ref_new === self::EMPTY_HASH)) { // This happens if you try to delete a tag or branch which does not // exist by pushing directly to the ref. Git will warn about it but // allow it. Just call it a delete, without flagging it as dangerous. $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE; } else if ($ref_old === self::EMPTY_HASH) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD; } else if ($ref_new === self::EMPTY_HASH) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE; if ($ref_type == PhabricatorRepositoryPushLog::REFTYPE_BRANCH) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS; $dangerous = pht( "The change you're attempting to push deletes the branch '%s'.", $ref_update->getRefName()); } } else { $merge_base = $ref_update->getMergeBase(); if ($merge_base == $ref_old) { // This is a fast-forward update to an existing branch. // These are safe. $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND; } else { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE;
// For now, we don't consider deleting or moving tags to be a // "dangerous" update. It's way harder to get wrong and should be easy // to recover from once we have better logging. Only add the dangerous // flag if this ref is a branch.
if ($ref_type == PhabricatorRepositoryPushLog::REFTYPE_BRANCH) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS;
$dangerous = pht( "The change you're attempting to push updates the branch '%s' ". "from '%s' to '%s', but this is not a fast-forward. Pushes ". "which rewrite published branch history are dangerous.", $ref_update->getRefName(), $ref_update->getRefOldShort(), $ref_update->getRefNewShort()); } } }
$ref_update->setChangeFlags($ref_flags); if ($dangerous !== null) { $ref_update->attachDangerousChangeDescription($dangerous); } }
return $ref_updates; }
private function findGitContentUpdates(array $ref_updates) { $flag_delete = PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE;
$futures = array(); foreach ($ref_updates as $key => $ref_update) { if ($ref_update->hasChangeFlags($flag_delete)) { // Deleting a branch or tag can never create any new commits. continue; }
// NOTE: This piece of magic finds all new commits, by walking backward // from the new value to the value of *any* existing ref in the // repository. Particularly, this will cover the cases of a new branch, a // completely moved tag, etc. $futures[$key] = $this->getRepository()->getLocalCommandFuture( 'log %s %s --not --all --', '--format=%H', gitsprintf('%s', $ref_update->getRefNew())); }
$content_updates = array(); $futures = id(new FutureIterator($futures)) ->limit(8); foreach ($futures as $key => $future) { list($stdout) = $future->resolvex();
if (!strlen(trim($stdout))) { // This change doesn't have any new commits. One common case of this // is creating a new tag which points at an existing commit. continue; }
$commits = phutil_split_lines($stdout, $retain_newlines = false);
// If we're looking at a branch, mark all of the new commits as on that // branch. It's only possible for these commits to be on updated branches, // since any other branch heads are necessarily behind them. $branch_name = null; $ref_update = $ref_updates[$key]; $type_branch = PhabricatorRepositoryPushLog::REFTYPE_BRANCH; if ($ref_update->getRefType() == $type_branch) { $branch_name = $ref_update->getRefName(); }
foreach ($commits as $commit) { if ($branch_name) { $this->gitCommits[$commit][] = $branch_name; } $content_updates[$commit] = $this->newPushLog() ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT) ->setRefNew($commit) ->setChangeFlags(PhabricatorRepositoryPushLog::CHANGEFLAG_ADD); } }
return $content_updates; }
/* -( Custom )------------------------------------------------------------- */
private function applyCustomHooks(array $updates) { $args = $this->getOriginalArgv(); $stdin = $this->getStdin(); $console = PhutilConsole::getConsole();
$env = array( self::ENV_REPOSITORY => $this->getRepository()->getPHID(), self::ENV_USER => $this->getViewer()->getUsername(), self::ENV_REQUEST => $this->getRequestIdentifier(), self::ENV_REMOTE_PROTOCOL => $this->getRemoteProtocol(), self::ENV_REMOTE_ADDRESS => $this->getRemoteAddress(), );
$repository = $this->getRepository();
$env += $repository->getPassthroughEnvironmentalVariables();
$directories = $repository->getHookDirectories(); foreach ($directories as $directory) { $hooks = $this->getExecutablesInDirectory($directory); sort($hooks); foreach ($hooks as $hook) { // NOTE: We're explicitly running the hooks in sequential order to // make this more predictable. $future = id(new ExecFuture('%s %Ls', $hook, $args)) ->setEnv($env, $wipe_process_env = false) ->write($stdin);
list($err, $stdout, $stderr) = $future->resolve(); if (!$err) { // This hook ran OK, but echo its output in case there was something // informative. $console->writeOut('%s', $stdout); $console->writeErr('%s', $stderr); continue; }
$this->rejectCode = PhabricatorRepositoryPushLog::REJECT_EXTERNAL; $this->rejectDetails = basename($hook);
throw new DiffusionCommitHookRejectException( pht( "This push was rejected by custom hook script '%s':\n\n%s%s", basename($hook), $stdout, $stderr)); } } }
private function getExecutablesInDirectory($directory) { $executables = array();
if (!Filesystem::pathExists($directory)) { return $executables; }
foreach (Filesystem::listDirectory($directory) as $path) { $full_path = $directory.DIRECTORY_SEPARATOR.$path; if (!is_executable($full_path)) { // Don't include non-executable files. continue; }
if (basename($full_path) == 'README') { // Don't include README, even if it is marked as executable. It almost // certainly got caught in the crossfire of a sweeping `chmod`, since // users do this with some frequency. continue; }
$executables[] = $full_path; }
return $executables; }
/* -( Mercurial )---------------------------------------------------------- */
private function findMercurialRefUpdates() { $hook = $this->getMercurialHook(); switch ($hook) { case 'pretxnchangegroup': return $this->findMercurialChangegroupRefUpdates(); case 'prepushkey': return $this->findMercurialPushKeyRefUpdates(); default: throw new Exception(pht('Unrecognized hook "%s"!', $hook)); } }
private function findMercurialChangegroupRefUpdates() { $hg_node = getenv('HG_NODE'); if (!$hg_node) { throw new Exception( pht( 'Expected %s in environment!', 'HG_NODE')); }
// NOTE: We need to make sure this is passed to subprocesses, or they won't // be able to see new commits. Mercurial uses this as a marker to determine // whether the pending changes are visible or not. $_ENV['HG_PENDING'] = getenv('HG_PENDING'); $repository = $this->getRepository();
$futures = array();
foreach (array('old', 'new') as $key) { $futures[$key] = $repository->getLocalCommandFuture( 'heads --template %s', '{node}\1{branch}\2'); } // Wipe HG_PENDING out of the old environment so we see the pre-commit // state of the repository. $futures['old']->updateEnv('HG_PENDING', null);
$futures['commits'] = $repository->getLocalCommandFuture( 'log --rev %s --template %s', hgsprintf('%s:%s', $hg_node, 'tip'), '{node}\1{branch}\2');
// Resolve all of the futures now. We don't need the 'commits' future yet, // but it simplifies the logic to just get it out of the way. foreach (new FutureIterator($futures) as $future) { $future->resolve(); }
list($commit_raw) = $futures['commits']->resolvex(); $commit_map = $this->parseMercurialCommits($commit_raw); $this->mercurialCommits = $commit_map;
// NOTE: `hg heads` exits with an error code and no output if the repository // has no heads. Most commonly this happens on a new repository. We know // we can run `hg` successfully since the `hg log` above didn't error, so // just ignore the error code.
list($err, $old_raw) = $futures['old']->resolve(); $old_refs = $this->parseMercurialHeads($old_raw);
list($err, $new_raw) = $futures['new']->resolve(); $new_refs = $this->parseMercurialHeads($new_raw);
$all_refs = array_keys($old_refs + $new_refs);
$ref_updates = array(); foreach ($all_refs as $ref) { $old_heads = idx($old_refs, $ref, array()); $new_heads = idx($new_refs, $ref, array());
sort($old_heads); sort($new_heads);
if (!$old_heads && !$new_heads) { // This should never be possible, as it makes no sense. Explode. throw new Exception( pht( 'Mercurial repository has no new or old heads for branch "%s" '. 'after push. This makes no sense; rejecting change.', $ref)); }
if ($old_heads === $new_heads) { // No changes to this branch, so skip it. continue; }
$stray_heads = array(); $head_map = array();
if ($old_heads && !$new_heads) { // This is a branch deletion with "--close-branch". foreach ($old_heads as $old_head) { $head_map[$old_head] = array(self::EMPTY_HASH); } } else if (count($old_heads) > 1) { // HORRIBLE: In Mercurial, branches can have multiple heads. If the // old branch had multiple heads, we need to figure out which new // heads descend from which old heads, so we can tell whether you're // actively creating new heads (dangerous) or just working in a // repository that's already full of garbage (strongly discouraged but // not as inherently dangerous). These cases should be very uncommon.
// NOTE: We're only looking for heads on the same branch. The old // tip of the branch may be the branchpoint for other branches, but that // is OK.
$dfutures = array(); foreach ($old_heads as $old_head) { $dfutures[$old_head] = $repository->getLocalCommandFuture( 'log --branch %s --rev %s --template %s', $ref, hgsprintf('(descendants(%s) and head())', $old_head), '{node}\1'); }
foreach (new FutureIterator($dfutures) as $future_head => $dfuture) { list($stdout) = $dfuture->resolvex(); $descendant_heads = array_filter(explode("\1", $stdout)); if ($descendant_heads) { // This old head has at least one descendant in the push. $head_map[$future_head] = $descendant_heads; } else { // This old head has no descendants, so it is being deleted. $head_map[$future_head] = array(self::EMPTY_HASH); } }
// Now, find all the new stray heads this push creates, if any. These // are new heads which do not descend from the old heads. $seen = array_fuse(array_mergev($head_map)); foreach ($new_heads as $new_head) { if ($new_head === self::EMPTY_HASH) { // If a branch head is being deleted, don't insert it as an add. continue; } if (empty($seen[$new_head])) { $head_map[self::EMPTY_HASH][] = $new_head; } } } else if ($old_heads) { $head_map[head($old_heads)] = $new_heads; } else { $head_map[self::EMPTY_HASH] = $new_heads; }
foreach ($head_map as $old_head => $child_heads) { foreach ($child_heads as $new_head) { if ($new_head === $old_head) { continue; }
$ref_flags = 0; $dangerous = null; if ($old_head == self::EMPTY_HASH) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD; } else { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND; }
$deletes_existing_head = ($new_head == self::EMPTY_HASH); $splits_existing_head = (count($child_heads) > 1); $creates_duplicate_head = ($old_head == self::EMPTY_HASH) && (count($head_map) > 1);
if ($splits_existing_head || $creates_duplicate_head) { $readable_child_heads = array(); foreach ($child_heads as $child_head) { $readable_child_heads[] = substr($child_head, 0, 12); }
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS;
if ($splits_existing_head) { // We're splitting an existing head into two or more heads. // This is dangerous, and a super bad idea. Note that we're only // raising this if you're actively splitting a branch head. If a // head split in the past, we don't consider appends to it // to be dangerous. $dangerous = pht( "The change you're attempting to push splits the head of ". "branch '%s' into multiple heads: %s. This is inadvisable ". "and dangerous.", $ref, implode(', ', $readable_child_heads)); } else { // We're adding a second (or more) head to a branch. The new // head is not a descendant of any old head. $dangerous = pht( "The change you're attempting to push creates new, divergent ". "heads for the branch '%s': %s. This is inadvisable and ". "dangerous.", $ref, implode(', ', $readable_child_heads)); } }
if ($deletes_existing_head) { // TODO: Somewhere in here we should be setting CHANGEFLAG_REWRITE // if we are also creating at least one other head to replace // this one.
// NOTE: In Git, this is a dangerous change, but it is not dangerous // in Mercurial. Mercurial branches are version controlled, and // Mercurial does not prompt you for any special flags when pushing // a `--close-branch` commit by default.
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE; }
$ref_update = $this->newPushLog() ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_BRANCH) ->setRefName($ref) ->setRefOld($old_head) ->setRefNew($new_head) ->setChangeFlags($ref_flags);
if ($dangerous !== null) { $ref_update->attachDangerousChangeDescription($dangerous); }
$ref_updates[] = $ref_update; } } }
return $ref_updates; }
private function findMercurialPushKeyRefUpdates() { $key_namespace = getenv('HG_NAMESPACE');
if ($key_namespace === 'phases') { // Mercurial changes commit phases as part of normal push operations. We // just ignore these, as they don't seem to represent anything // interesting. return array(); }
$key_name = getenv('HG_KEY');
$key_old = getenv('HG_OLD'); if (!$key_old || !strlen($key_old)) { $key_old = null; }
$key_new = getenv('HG_NEW'); if (!$key_new || !strlen($key_new)) { $key_new = null; }
if ($key_namespace !== 'bookmarks') { throw new Exception( pht( "Unknown Mercurial key namespace '%s', with key '%s' (%s -> %s). ". "Rejecting push.", $key_namespace, $key_name, coalesce($key_old, pht('null')), coalesce($key_new, pht('null')))); }
if ($key_old === $key_new) { // We get a callback when the bookmark doesn't change. Just ignore this, // as it's a no-op. return array(); }
$ref_flags = 0; $merge_base = null; if ($key_old === null) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD; } else if ($key_new === null) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE; } else { list($merge_base_raw) = $this->getRepository()->execxLocalCommand( 'log --template %s --rev %s', '{node}', hgsprintf('ancestor(%s, %s)', $key_old, $key_new));
if (strlen(trim($merge_base_raw))) { $merge_base = trim($merge_base_raw); }
if ($merge_base && ($merge_base === $key_old)) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND; } else { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE; } }
$ref_update = $this->newPushLog() ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_BOOKMARK) ->setRefName($key_name) ->setRefOld(coalesce($key_old, self::EMPTY_HASH)) ->setRefNew(coalesce($key_new, self::EMPTY_HASH)) ->setChangeFlags($ref_flags);
return array($ref_update); }
private function findMercurialContentUpdates(array $ref_updates) { $content_updates = array();
foreach ($this->mercurialCommits as $commit => $branches) { $content_updates[$commit] = $this->newPushLog() ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT) ->setRefNew($commit) ->setChangeFlags(PhabricatorRepositoryPushLog::CHANGEFLAG_ADD); }
return $content_updates; }
private function parseMercurialCommits($raw) { $commits_lines = explode("\2", $raw); $commits_lines = array_filter($commits_lines); $commit_map = array(); foreach ($commits_lines as $commit_line) { list($node, $branch) = explode("\1", $commit_line); $commit_map[$node] = array($branch); }
return $commit_map; }
private function parseMercurialHeads($raw) { $heads_map = $this->parseMercurialCommits($raw);
$heads = array(); foreach ($heads_map as $commit => $branches) { foreach ($branches as $branch) { $heads[$branch][] = $commit; } }
return $heads; }
/* -( Subversion )--------------------------------------------------------- */
private function findSubversionRefUpdates() { // Subversion doesn't have any kind of mutable ref metadata. return array(); }
private function findSubversionContentUpdates(array $ref_updates) { list($youngest) = execx( 'svnlook youngest %s', $this->subversionRepository); $ref_new = (int)$youngest + 1;
$ref_flags = 0; $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD; $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND;
$ref_content = $this->newPushLog() ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT) ->setRefNew($ref_new) ->setChangeFlags($ref_flags);
return array($ref_content); }
/* -( Internals )---------------------------------------------------------- */
private function newPushLog() { // NOTE: We generate PHIDs up front so the Herald transcripts can pick them // up. $phid = id(new PhabricatorRepositoryPushLog())->generatePHID();
$device = AlmanacKeys::getLiveDevice(); if ($device) { $device_phid = $device->getPHID(); } else { $device_phid = null; }
return PhabricatorRepositoryPushLog::initializeNewLog($this->getViewer()) ->setPHID($phid) ->setDevicePHID($device_phid) ->setRepositoryPHID($this->getRepository()->getPHID()) ->attachRepository($this->getRepository()) ->setEpoch(PhabricatorTime::getNow()); }
private function newPushEvent() { $viewer = $this->getViewer();
$hook_start = $this->getStartTime();
$event = PhabricatorRepositoryPushEvent::initializeNewEvent($viewer) ->setRepositoryPHID($this->getRepository()->getPHID()) ->setRemoteAddress($this->getRemoteAddress()) ->setRemoteProtocol($this->getRemoteProtocol()) ->setEpoch(PhabricatorTime::getNow()) ->setHookWait(phutil_microseconds_since($hook_start));
$identifier = $this->getRequestIdentifier(); if ($identifier !== null && strlen($identifier)) { $event->setRequestIdentifier($identifier); }
return $event; }
private function rejectEnormousChanges(array $content_updates) { $repository = $this->getRepository(); if ($repository->shouldAllowEnormousChanges()) { return; }
// See T13142. Don't cache more than 64MB of changesets. For normal small // pushes, caching everything here can let us hit the cache from Herald if // we need to run content rules, which speeds things up a bit. For large // pushes, we may not be able to hold everything in memory. $cache_limit = 1024 * 1024 * 64;
foreach ($content_updates as $update) { $identifier = $update->getRefNew(); try { $info = $this->loadChangesetsForCommit($identifier); list($changesets, $size) = $info;
if ($this->changesetsSize + $size <= $cache_limit) { $this->changesets[$identifier] = $changesets; $this->changesetsSize += $size; } } catch (Exception $ex) { $this->changesets[$identifier] = $ex;
$message = pht( 'ENORMOUS CHANGE'. "\n". 'Enormous change protection is enabled for this repository, but '. 'you are pushing an enormous change ("%s"). Edit the repository '. 'configuration before making enormous changes.'. "\n\n". "Content Exception: %s", $identifier, $ex->getMessage());
throw new DiffusionCommitHookRejectException($message); } } }
private function loadChangesetsForCommit($identifier) { $byte_limit = HeraldCommitAdapter::getEnormousByteLimit(); $time_limit = HeraldCommitAdapter::getEnormousTimeLimit();
$vcs = $this->getRepository()->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: // For git and hg, we can use normal commands. $drequest = DiffusionRequest::newFromDictionary( array( 'repository' => $this->getRepository(), 'user' => $this->getViewer(), 'commit' => $identifier, ));
$raw_diff = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest) ->setTimeout($time_limit) ->setByteLimit($byte_limit) ->setLinesOfContext(0) ->executeInline(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // TODO: This diff has 3 lines of context, which produces slightly // incorrect "added file content" and "removed file content" results. // This may also choke on binaries, but "svnlook diff" does not support // the "--diff-cmd" flag.
// For subversion, we need to use `svnlook`. $future = new ExecFuture( 'svnlook diff -t %s %s', $this->subversionTransaction, $this->subversionRepository);
$future->setTimeout($time_limit); $future->setStdoutSizeLimit($byte_limit); $future->setStderrSizeLimit($byte_limit);
list($raw_diff) = $future->resolvex(); break; default: throw new Exception(pht("Unknown VCS '%s!'", $vcs)); }
if (strlen($raw_diff) >= $byte_limit) { throw new Exception( pht( 'The raw text of this change ("%s") is enormous (larger than %s '. 'bytes).', $identifier, new PhutilNumber($byte_limit))); }
if (!strlen($raw_diff)) { // If the commit is actually empty, just return no changesets. return array(array(), 0); }
$parser = new ArcanistDiffParser(); $changes = $parser->parseDiff($raw_diff); $diff = DifferentialDiff::newEphemeralFromRawChanges( $changes);
$changesets = $diff->getChangesets(); $size = strlen($raw_diff);
return array($changesets, $size); }
public function getChangesetsForCommit($identifier) { if (isset($this->changesets[$identifier])) { $cached = $this->changesets[$identifier];
if ($cached instanceof Exception) { throw $cached; }
return $cached; }
$info = $this->loadChangesetsForCommit($identifier); list($changesets, $size) = $info; return $changesets; }
private function rejectOversizedFiles(array $content_updates) { $repository = $this->getRepository();
$limit = $repository->getFilesizeLimit(); if (!$limit) { return; }
foreach ($content_updates as $update) { $identifier = $update->getRefNew();
$sizes = $this->getFileSizesForCommit($identifier);
foreach ($sizes as $path => $size) { if ($size <= $limit) { continue; }
$message = pht( 'OVERSIZED FILE'. "\n". 'This repository ("%s") is configured with a maximum individual '. 'file size limit, but you are pushing a change ("%s") which causes '. 'the size of a file ("%s") to exceed the limit. The commit makes '. 'the file %s bytes long, but the limit for this repository is '. '%s bytes.', $repository->getDisplayName(), $identifier, $path, new PhutilNumber($size), new PhutilNumber($limit));
throw new DiffusionCommitHookRejectException($message); } } }
private function rejectCommitsAffectingTooManyPaths(array $content_updates) { $repository = $this->getRepository();
$limit = $repository->getTouchLimit(); if (!$limit) { return; }
foreach ($content_updates as $update) { $identifier = $update->getRefNew();
$sizes = $this->getFileSizesForCommit($identifier); if (count($sizes) > $limit) { $message = pht( 'COMMIT AFFECTS TOO MANY PATHS'. "\n". 'This repository ("%s") is configured with a touched files limit '. 'that caps the maximum number of paths any single commit may '. 'affect. You are pushing a change ("%s") which exceeds this '. 'limit: it affects %s paths, but the largest number of paths any '. 'commit may affect is %s paths.', $repository->getDisplayName(), $identifier, phutil_count($sizes), new PhutilNumber($limit));
throw new DiffusionCommitHookRejectException($message); } } }
public function getFileSizesForCommit($identifier) { if (!isset($this->filesizeCache[$identifier])) { $file_sizes = $this->loadFileSizesForCommit($identifier); $this->filesizeCache[$identifier] = $file_sizes; }
return $this->filesizeCache[$identifier]; }
private function loadFileSizesForCommit($identifier) { $repository = $this->getRepository();
return id(new DiffusionLowLevelFilesizeQuery()) ->setRepository($repository) ->withIdentifier($identifier) ->execute(); }
public function loadCommitRefForCommit($identifier) { $repository = $this->getRepository(); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: return id(new DiffusionLowLevelCommitQuery()) ->setRepository($repository) ->withIdentifier($identifier) ->execute(); case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // For subversion, we need to use `svnlook`. list($message) = execx( 'svnlook log -t %s %s', $this->subversionTransaction, $this->subversionRepository);
return id(new DiffusionCommitRef()) ->setMessage($message); default: throw new Exception(pht("Unknown VCS '%s!'", $vcs)); } }
public function loadBranches($identifier) { $repository = $this->getRepository(); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: return idx($this->gitCommits, $identifier, array()); case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: // NOTE: This will be "the branch the commit was made to", not // "a list of all branch heads which descend from the commit". // This is consistent with Mercurial, but possibly confusing. return idx($this->mercurialCommits, $identifier, array()); case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // Subversion doesn't have branches. return array(); } }
private function loadCommitInfoForWorker(array $all_updates) { $type_commit = PhabricatorRepositoryPushLog::REFTYPE_COMMIT;
$map = array(); foreach ($all_updates as $update) { if ($update->getRefType() != $type_commit) { continue; } $map[$update->getRefNew()] = array(); }
foreach ($map as $identifier => $info) { $ref = $this->loadCommitRefForCommit($identifier); $map[$identifier] += array( 'summary' => $ref->getSummary(), 'branches' => $this->loadBranches($identifier), ); }
return $map; }
private function isInitialImport(array $all_updates) { $repository = $this->getRepository();
$vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // There is no meaningful way to import history into Subversion by // pushing. return false; default: break; }
// Now, apply a heuristic to guess whether this is a normal commit or // an initial import. We guess something is an initial import if: // // - the repository is currently empty; and // - it pushes more than 7 commits at once. // // The number "7" is chosen arbitrarily as seeming reasonable. We could // also look at author data (do the commits come from multiple different // authors?) and commit date data (is the oldest commit more than 48 hours // old), but we don't have immediate access to those and this simple // heuristic might be good enough.
$commit_count = 0; $type_commit = PhabricatorRepositoryPushLog::REFTYPE_COMMIT; foreach ($all_updates as $update) { if ($update->getRefType() != $type_commit) { continue; } $commit_count++; }
if ($commit_count <= PhabricatorRepository::IMPORT_THRESHOLD) { // If this pushes a very small number of commits, assume it's an // initial commit or stack of a few initial commits. return false; }
$any_commits = id(new DiffusionCommitQuery()) ->setViewer($this->getViewer()) ->withRepository($repository) ->setLimit(1) ->execute();
if ($any_commits) { // If the repository already has commits, this isn't an import. return false; }
return true; }
}