3 final class DifferentialDiffExtractionEngine
extends Phobject
{
8 public function setViewer(PhabricatorUser
$viewer) {
9 $this->viewer
= $viewer;
13 public function getViewer() {
17 public function setAuthorPHID($author_phid) {
18 $this->authorPHID
= $author_phid;
22 public function getAuthorPHID() {
23 return $this->authorPHID
;
26 public function newDiffFromCommit(PhabricatorRepositoryCommit
$commit) {
27 $viewer = $this->getViewer();
29 // If we already have an unattached diff for this commit, just reuse it.
30 // This stops us from repeatedly generating diffs if something goes wrong
31 // later in the process. See T10968 for context.
32 $existing_diffs = id(new DifferentialDiffQuery())
34 ->withCommitPHIDs(array($commit->getPHID()))
35 ->withHasRevision(false)
36 ->needChangesets(true)
38 if ($existing_diffs) {
39 return head($existing_diffs);
42 $repository = $commit->getRepository();
43 $identifier = $commit->getCommitIdentifier();
44 $monogram = $commit->getMonogram();
46 $drequest = DiffusionRequest
::newFromDictionary(
49 'repository' => $repository,
52 $diff_info = DiffusionQuery
::callConduitWithDiffusionRequest(
55 'diffusion.rawdiffquery',
57 'commit' => $identifier,
60 $file_phid = $diff_info['filePHID'];
61 $diff_file = id(new PhabricatorFileQuery())
63 ->withPHIDs(array($file_phid))
68 'Failed to load file ("%s") returned by "%s".',
70 'diffusion.rawdiffquery'));
73 $raw_diff = $diff_file->loadFileData();
75 // TODO: Support adds, deletes and moves under SVN.
76 if (strlen($raw_diff)) {
77 $changes = id(new ArcanistDiffParser())->parseDiff($raw_diff);
79 // This is an empty diff, maybe made with `git commit --allow-empty`.
80 // NOTE: These diffs have the same tree hash as their ancestors, so
81 // they may attach to revisions in an unexpected way. Just let this
82 // happen for now, although it might make sense to special case it
87 $diff = DifferentialDiff
::newFromRawChanges($viewer, $changes)
88 ->setRepositoryPHID($repository->getPHID())
89 ->setCommitPHID($commit->getPHID())
90 ->setCreationMethod('commit')
91 ->setSourceControlSystem($repository->getVersionControlSystem())
92 ->setLintStatus(DifferentialLintStatus
::LINT_AUTO_SKIP
)
93 ->setUnitStatus(DifferentialUnitStatus
::UNIT_AUTO_SKIP
)
94 ->setDateCreated($commit->getEpoch())
95 ->setDescription($monogram);
97 $author_phid = $this->getAuthorPHID();
98 if ($author_phid !== null) {
99 $diff->setAuthorPHID($author_phid);
102 $parents = DiffusionQuery
::callConduitWithDiffusionRequest(
105 'diffusion.commitparentsquery',
107 'commit' => $identifier,
111 $diff->setSourceControlBaseRevision(head($parents));
114 // TODO: Attach binary files.
116 return $diff->save();
119 public function isDiffChangedBeforeCommit(
120 PhabricatorRepositoryCommit
$commit,
121 DifferentialDiff
$old,
122 DifferentialDiff
$new) {
124 $viewer = $this->getViewer();
125 $repository = $commit->getRepository();
126 $identifier = $commit->getCommitIdentifier();
128 $vs_changesets = array();
129 foreach ($old->getChangesets() as $changeset) {
130 $path = $changeset->getAbsoluteRepositoryPath($repository, $old);
131 $path = ltrim($path, '/');
132 $vs_changesets[$path] = $changeset;
135 $changesets = array();
136 foreach ($new->getChangesets() as $changeset) {
137 $path = $changeset->getAbsoluteRepositoryPath($repository, $new);
138 $path = ltrim($path, '/');
139 $changesets[$path] = $changeset;
142 if (array_fill_keys(array_keys($changesets), true) !=
143 array_fill_keys(array_keys($vs_changesets), true)) {
147 $file_phids = array();
148 foreach ($vs_changesets as $changeset) {
149 $metadata = $changeset->getMetadata();
150 $file_phid = idx($metadata, 'new:binary-phid');
152 $file_phids[$file_phid] = $file_phid;
158 $files = id(new PhabricatorFileQuery())
159 ->setViewer(PhabricatorUser
::getOmnipotentUser())
160 ->withPHIDs($file_phids)
162 $files = mpull($files, null, 'getPHID');
165 foreach ($changesets as $path => $changeset) {
166 $vs_changeset = $vs_changesets[$path];
168 $file_phid = idx($vs_changeset->getMetadata(), 'new:binary-phid');
170 if (!isset($files[$file_phid])) {
174 $drequest = DiffusionRequest
::newFromDictionary(
177 'repository' => $repository,
181 $response = DiffusionQuery
::callConduitWithDiffusionRequest(
184 'diffusion.filecontentquery',
186 'commit' => $identifier,
189 } catch (Exception
$ex) {
190 // TODO: See PHI1044. This call may fail if the diff deleted the
191 // file. If the call fails, just detect a change for now. This should
192 // generally be made cleaner in the future.
196 $new_file_phid = $response['filePHID'];
197 if (!$new_file_phid) {
201 $new_file = id(new PhabricatorFileQuery())
203 ->withPHIDs(array($new_file_phid))
209 if ($files[$file_phid]->loadFileData() != $new_file->loadFileData()) {
213 $context = implode("\n", $changeset->makeChangesWithContext());
214 $vs_context = implode("\n", $vs_changeset->makeChangesWithContext());
216 // We couldn't just compare $context and $vs_context because following
217 // diffs will be considered different:
227 $hunk = id(new DifferentialHunk())->setChanges($context);
228 $vs_hunk = id(new DifferentialHunk())->setChanges($vs_context);
229 if ($hunk->makeOldFile() != $vs_hunk->makeOldFile() ||
230 $hunk->makeNewFile() != $vs_hunk->makeNewFile()) {
239 public function updateRevisionWithCommit(
240 DifferentialRevision
$revision,
241 PhabricatorRepositoryCommit
$commit,
242 array $more_xactions,
243 PhabricatorContentSource
$content_source) {
245 $viewer = $this->getViewer();
246 $new_diff = $this->newDiffFromCommit($commit);
248 $old_diff = $revision->getActiveDiff();
251 $old_diff = id(new DifferentialDiffQuery())
253 ->withIDs(array($old_diff->getID()))
254 ->needChangesets(true)
257 $has_changed = $this->isDiffChangedBeforeCommit(
262 $revision_monogram = $revision->getMonogram();
263 $old_id = $old_diff->getID();
264 $new_id = $new_diff->getID();
266 $changed_uri = "/{$revision_monogram}?vs={$old_id}&id={$new_id}#toc";
267 $changed_uri = PhabricatorEnv
::getProductionURI($changed_uri);
274 // If the revision isn't closed or "Accepted", write a warning into the
275 // transaction log. This makes it more clear when users bend the rules.
276 if (!$revision->isClosed() && !$revision->isAccepted()) {
277 $wrong_type = DifferentialRevisionWrongStateTransaction
::TRANSACTIONTYPE
;
279 $xactions[] = id(new DifferentialTransaction())
280 ->setTransactionType($wrong_type)
281 ->setNewValue($revision->getModernRevisionStatus());
284 $concerning_builds = self
::loadConcerningBuilds(
289 if ($concerning_builds) {
290 $build_list = array();
291 foreach ($concerning_builds as $build) {
292 $build_list[] = array(
293 'phid' => $build->getPHID(),
294 'status' => $build->getBuildStatus(),
299 DifferentialRevisionWrongBuildsTransaction
::TRANSACTIONTYPE
;
301 $xactions[] = id(new DifferentialTransaction())
302 ->setTransactionType($wrong_builds)
303 ->setNewValue($build_list);
306 $type_update = DifferentialRevisionUpdateTransaction
::TRANSACTIONTYPE
;
308 $xactions[] = id(new DifferentialTransaction())
309 ->setTransactionType($type_update)
310 ->setIgnoreOnNoEffect(true)
311 ->setNewValue($new_diff->getPHID())
312 ->setMetadataValue('isCommitUpdate', true)
313 ->setMetadataValue('commitPHIDs', array($commit->getPHID()));
315 foreach ($more_xactions as $more_xaction) {
316 $xactions[] = $more_xaction;
319 $editor = id(new DifferentialTransactionEditor())
321 ->setContinueOnMissingFields(true)
322 ->setContinueOnNoEffect(true)
323 ->setContentSource($content_source)
324 ->setChangedPriorToCommitURI($changed_uri)
325 ->setIsCloseByCommit(true);
327 $author_phid = $this->getAuthorPHID();
328 if ($author_phid !== null) {
329 $editor->setActingAsPHID($author_phid);
332 $editor->applyTransactions($revision, $xactions);
335 public static function loadConcerningBuilds(
336 PhabricatorUser
$viewer,
337 DifferentialRevision
$revision,
340 $diff = $revision->getActiveDiff();
342 $buildables = id(new HarbormasterBuildableQuery())
344 ->withBuildablePHIDs(array($diff->getPHID()))
346 ->withManualBuildables(false)
352 $land_key = HarbormasterBuildPlanBehavior
::BEHAVIOR_LANDWARNING
;
353 $behavior = HarbormasterBuildPlanBehavior
::getBehavior($land_key);
355 $key_never = HarbormasterBuildPlanBehavior
::LANDWARNING_NEVER
;
356 $key_building = HarbormasterBuildPlanBehavior
::LANDWARNING_IF_BUILDING
;
357 $key_complete = HarbormasterBuildPlanBehavior
::LANDWARNING_IF_COMPLETE
;
359 $concerning_builds = array();
360 foreach ($buildables as $buildable) {
361 $builds = $buildable->getBuilds();
362 foreach ($builds as $build) {
363 $plan = $build->getBuildPlan();
364 $option = $behavior->getPlanOption($plan);
365 $behavior_value = $option->getKey();
367 $if_never = ($behavior_value === $key_never);
372 $if_building = ($behavior_value === $key_building);
373 if ($if_building && $build->isComplete()) {
377 $if_complete = ($behavior_value === $key_complete);
379 if (!$build->isComplete()) {
383 // TODO: If you "arc land" and a build with "Warn: If Complete"
384 // is still running, you may not see a warning, and push the revision
385 // in good faith. The build may then complete before we get here, so
386 // we now see a completed, failed build.
388 // For now, just err on the side of caution and assume these builds
389 // were in a good state when we prompted the user, even if they're in
392 // We could refine this with a rule like "if the build finished
393 // within a couple of minutes before the push happened, assume it was
394 // in good faith", but we don't currently have an especially
395 // convenient way to check when the build finished or when the commit
396 // was pushed or discovered, and this would create some issues in
397 // cases where the repository is observed and the fetch pipeline
398 // stalls for a while.
400 // If we're in strict mode (from a pre-commit content hook), we do
401 // not ignore these, since we're doing an instantaneous check against
402 // the current state.
409 if ($build->isPassed()) {
413 $concerning_builds[] = $build;
417 return $concerning_builds;