Path: blob/master/src/applications/differential/engine/DifferentialDiffExtractionEngine.php
12256 views
<?php12final class DifferentialDiffExtractionEngine extends Phobject {34private $viewer;5private $authorPHID;67public function setViewer(PhabricatorUser $viewer) {8$this->viewer = $viewer;9return $this;10}1112public function getViewer() {13return $this->viewer;14}1516public function setAuthorPHID($author_phid) {17$this->authorPHID = $author_phid;18return $this;19}2021public function getAuthorPHID() {22return $this->authorPHID;23}2425public function newDiffFromCommit(PhabricatorRepositoryCommit $commit) {26$viewer = $this->getViewer();2728// If we already have an unattached diff for this commit, just reuse it.29// This stops us from repeatedly generating diffs if something goes wrong30// later in the process. See T10968 for context.31$existing_diffs = id(new DifferentialDiffQuery())32->setViewer($viewer)33->withCommitPHIDs(array($commit->getPHID()))34->withHasRevision(false)35->needChangesets(true)36->execute();37if ($existing_diffs) {38return head($existing_diffs);39}4041$repository = $commit->getRepository();42$identifier = $commit->getCommitIdentifier();43$monogram = $commit->getMonogram();4445$drequest = DiffusionRequest::newFromDictionary(46array(47'user' => $viewer,48'repository' => $repository,49));5051$diff_info = DiffusionQuery::callConduitWithDiffusionRequest(52$viewer,53$drequest,54'diffusion.rawdiffquery',55array(56'commit' => $identifier,57));5859$file_phid = $diff_info['filePHID'];60$diff_file = id(new PhabricatorFileQuery())61->setViewer($viewer)62->withPHIDs(array($file_phid))63->executeOne();64if (!$diff_file) {65throw new Exception(66pht(67'Failed to load file ("%s") returned by "%s".',68$file_phid,69'diffusion.rawdiffquery'));70}7172$raw_diff = $diff_file->loadFileData();7374// TODO: Support adds, deletes and moves under SVN.75if (strlen($raw_diff)) {76$changes = id(new ArcanistDiffParser())->parseDiff($raw_diff);77} else {78// This is an empty diff, maybe made with `git commit --allow-empty`.79// NOTE: These diffs have the same tree hash as their ancestors, so80// they may attach to revisions in an unexpected way. Just let this81// happen for now, although it might make sense to special case it82// eventually.83$changes = array();84}8586$diff = DifferentialDiff::newFromRawChanges($viewer, $changes)87->setRepositoryPHID($repository->getPHID())88->setCommitPHID($commit->getPHID())89->setCreationMethod('commit')90->setSourceControlSystem($repository->getVersionControlSystem())91->setLintStatus(DifferentialLintStatus::LINT_AUTO_SKIP)92->setUnitStatus(DifferentialUnitStatus::UNIT_AUTO_SKIP)93->setDateCreated($commit->getEpoch())94->setDescription($monogram);9596$author_phid = $this->getAuthorPHID();97if ($author_phid !== null) {98$diff->setAuthorPHID($author_phid);99}100101$parents = DiffusionQuery::callConduitWithDiffusionRequest(102$viewer,103$drequest,104'diffusion.commitparentsquery',105array(106'commit' => $identifier,107));108109if ($parents) {110$diff->setSourceControlBaseRevision(head($parents));111}112113// TODO: Attach binary files.114115return $diff->save();116}117118public function isDiffChangedBeforeCommit(119PhabricatorRepositoryCommit $commit,120DifferentialDiff $old,121DifferentialDiff $new) {122123$viewer = $this->getViewer();124$repository = $commit->getRepository();125$identifier = $commit->getCommitIdentifier();126127$vs_changesets = array();128foreach ($old->getChangesets() as $changeset) {129$path = $changeset->getAbsoluteRepositoryPath($repository, $old);130$path = ltrim($path, '/');131$vs_changesets[$path] = $changeset;132}133134$changesets = array();135foreach ($new->getChangesets() as $changeset) {136$path = $changeset->getAbsoluteRepositoryPath($repository, $new);137$path = ltrim($path, '/');138$changesets[$path] = $changeset;139}140141if (array_fill_keys(array_keys($changesets), true) !=142array_fill_keys(array_keys($vs_changesets), true)) {143return true;144}145146$file_phids = array();147foreach ($vs_changesets as $changeset) {148$metadata = $changeset->getMetadata();149$file_phid = idx($metadata, 'new:binary-phid');150if ($file_phid) {151$file_phids[$file_phid] = $file_phid;152}153}154155$files = array();156if ($file_phids) {157$files = id(new PhabricatorFileQuery())158->setViewer(PhabricatorUser::getOmnipotentUser())159->withPHIDs($file_phids)160->execute();161$files = mpull($files, null, 'getPHID');162}163164foreach ($changesets as $path => $changeset) {165$vs_changeset = $vs_changesets[$path];166167$file_phid = idx($vs_changeset->getMetadata(), 'new:binary-phid');168if ($file_phid) {169if (!isset($files[$file_phid])) {170return true;171}172173$drequest = DiffusionRequest::newFromDictionary(174array(175'user' => $viewer,176'repository' => $repository,177));178179try {180$response = DiffusionQuery::callConduitWithDiffusionRequest(181$viewer,182$drequest,183'diffusion.filecontentquery',184array(185'commit' => $identifier,186'path' => $path,187));188} catch (Exception $ex) {189// TODO: See PHI1044. This call may fail if the diff deleted the190// file. If the call fails, just detect a change for now. This should191// generally be made cleaner in the future.192return true;193}194195$new_file_phid = $response['filePHID'];196if (!$new_file_phid) {197return true;198}199200$new_file = id(new PhabricatorFileQuery())201->setViewer($viewer)202->withPHIDs(array($new_file_phid))203->executeOne();204if (!$new_file) {205return true;206}207208if ($files[$file_phid]->loadFileData() != $new_file->loadFileData()) {209return true;210}211} else {212$context = implode("\n", $changeset->makeChangesWithContext());213$vs_context = implode("\n", $vs_changeset->makeChangesWithContext());214215// We couldn't just compare $context and $vs_context because following216// diffs will be considered different:217//218// -(empty line)219// -echo 'test';220// (empty line)221//222// (empty line)223// -echo "test";224// -(empty line)225226$hunk = id(new DifferentialHunk())->setChanges($context);227$vs_hunk = id(new DifferentialHunk())->setChanges($vs_context);228if ($hunk->makeOldFile() != $vs_hunk->makeOldFile() ||229$hunk->makeNewFile() != $vs_hunk->makeNewFile()) {230return true;231}232}233}234235return false;236}237238public function updateRevisionWithCommit(239DifferentialRevision $revision,240PhabricatorRepositoryCommit $commit,241array $more_xactions,242PhabricatorContentSource $content_source) {243244$viewer = $this->getViewer();245$new_diff = $this->newDiffFromCommit($commit);246247$old_diff = $revision->getActiveDiff();248$changed_uri = null;249if ($old_diff) {250$old_diff = id(new DifferentialDiffQuery())251->setViewer($viewer)252->withIDs(array($old_diff->getID()))253->needChangesets(true)254->executeOne();255if ($old_diff) {256$has_changed = $this->isDiffChangedBeforeCommit(257$commit,258$old_diff,259$new_diff);260if ($has_changed) {261$revision_monogram = $revision->getMonogram();262$old_id = $old_diff->getID();263$new_id = $new_diff->getID();264265$changed_uri = "/{$revision_monogram}?vs={$old_id}&id={$new_id}#toc";266$changed_uri = PhabricatorEnv::getProductionURI($changed_uri);267}268}269}270271$xactions = array();272273// If the revision isn't closed or "Accepted", write a warning into the274// transaction log. This makes it more clear when users bend the rules.275if (!$revision->isClosed() && !$revision->isAccepted()) {276$wrong_type = DifferentialRevisionWrongStateTransaction::TRANSACTIONTYPE;277278$xactions[] = id(new DifferentialTransaction())279->setTransactionType($wrong_type)280->setNewValue($revision->getModernRevisionStatus());281}282283$concerning_builds = self::loadConcerningBuilds(284$this->getViewer(),285$revision,286$strict = false);287288if ($concerning_builds) {289$build_list = array();290foreach ($concerning_builds as $build) {291$build_list[] = array(292'phid' => $build->getPHID(),293'status' => $build->getBuildStatus(),294);295}296297$wrong_builds =298DifferentialRevisionWrongBuildsTransaction::TRANSACTIONTYPE;299300$xactions[] = id(new DifferentialTransaction())301->setTransactionType($wrong_builds)302->setNewValue($build_list);303}304305$type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE;306307$xactions[] = id(new DifferentialTransaction())308->setTransactionType($type_update)309->setIgnoreOnNoEffect(true)310->setNewValue($new_diff->getPHID())311->setMetadataValue('isCommitUpdate', true)312->setMetadataValue('commitPHIDs', array($commit->getPHID()));313314foreach ($more_xactions as $more_xaction) {315$xactions[] = $more_xaction;316}317318$editor = id(new DifferentialTransactionEditor())319->setActor($viewer)320->setContinueOnMissingFields(true)321->setContinueOnNoEffect(true)322->setContentSource($content_source)323->setChangedPriorToCommitURI($changed_uri)324->setIsCloseByCommit(true);325326$author_phid = $this->getAuthorPHID();327if ($author_phid !== null) {328$editor->setActingAsPHID($author_phid);329}330331$editor->applyTransactions($revision, $xactions);332}333334public static function loadConcerningBuilds(335PhabricatorUser $viewer,336DifferentialRevision $revision,337$strict) {338339$diff = $revision->getActiveDiff();340341$buildables = id(new HarbormasterBuildableQuery())342->setViewer($viewer)343->withBuildablePHIDs(array($diff->getPHID()))344->needBuilds(true)345->withManualBuildables(false)346->execute();347if (!$buildables) {348return array();349}350351$land_key = HarbormasterBuildPlanBehavior::BEHAVIOR_LANDWARNING;352$behavior = HarbormasterBuildPlanBehavior::getBehavior($land_key);353354$key_never = HarbormasterBuildPlanBehavior::LANDWARNING_NEVER;355$key_building = HarbormasterBuildPlanBehavior::LANDWARNING_IF_BUILDING;356$key_complete = HarbormasterBuildPlanBehavior::LANDWARNING_IF_COMPLETE;357358$concerning_builds = array();359foreach ($buildables as $buildable) {360$builds = $buildable->getBuilds();361foreach ($builds as $build) {362$plan = $build->getBuildPlan();363$option = $behavior->getPlanOption($plan);364$behavior_value = $option->getKey();365366$if_never = ($behavior_value === $key_never);367if ($if_never) {368continue;369}370371$if_building = ($behavior_value === $key_building);372if ($if_building && $build->isComplete()) {373continue;374}375376$if_complete = ($behavior_value === $key_complete);377if ($if_complete) {378if (!$build->isComplete()) {379continue;380}381382// TODO: If you "arc land" and a build with "Warn: If Complete"383// is still running, you may not see a warning, and push the revision384// in good faith. The build may then complete before we get here, so385// we now see a completed, failed build.386387// For now, just err on the side of caution and assume these builds388// were in a good state when we prompted the user, even if they're in389// a bad state now.390391// We could refine this with a rule like "if the build finished392// within a couple of minutes before the push happened, assume it was393// in good faith", but we don't currently have an especially394// convenient way to check when the build finished or when the commit395// was pushed or discovered, and this would create some issues in396// cases where the repository is observed and the fetch pipeline397// stalls for a while.398399// If we're in strict mode (from a pre-commit content hook), we do400// not ignore these, since we're doing an instantaneous check against401// the current state.402403if (!$strict) {404continue;405}406}407408if ($build->isPassed()) {409continue;410}411412$concerning_builds[] = $build;413}414}415416return $concerning_builds;417}418419}420421422