Generate file attachment transactions for explicit Remarkup attachments on common...
[phabricator.git] / src / applications / differential / engine / DifferentialDiffExtractionEngine.php
blob6ffe33333363d197dddf03f13fbb7acbf9a00ee2
1 <?php
3 final class DifferentialDiffExtractionEngine extends Phobject {
5 private $viewer;
6 private $authorPHID;
8 public function setViewer(PhabricatorUser $viewer) {
9 $this->viewer = $viewer;
10 return $this;
13 public function getViewer() {
14 return $this->viewer;
17 public function setAuthorPHID($author_phid) {
18 $this->authorPHID = $author_phid;
19 return $this;
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())
33 ->setViewer($viewer)
34 ->withCommitPHIDs(array($commit->getPHID()))
35 ->withHasRevision(false)
36 ->needChangesets(true)
37 ->execute();
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(
47 array(
48 'user' => $viewer,
49 'repository' => $repository,
50 ));
52 $diff_info = DiffusionQuery::callConduitWithDiffusionRequest(
53 $viewer,
54 $drequest,
55 'diffusion.rawdiffquery',
56 array(
57 'commit' => $identifier,
58 ));
60 $file_phid = $diff_info['filePHID'];
61 $diff_file = id(new PhabricatorFileQuery())
62 ->setViewer($viewer)
63 ->withPHIDs(array($file_phid))
64 ->executeOne();
65 if (!$diff_file) {
66 throw new Exception(
67 pht(
68 'Failed to load file ("%s") returned by "%s".',
69 $file_phid,
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);
78 } else {
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
83 // eventually.
84 $changes = array();
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(
103 $viewer,
104 $drequest,
105 'diffusion.commitparentsquery',
106 array(
107 'commit' => $identifier,
110 if ($parents) {
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)) {
144 return true;
147 $file_phids = array();
148 foreach ($vs_changesets as $changeset) {
149 $metadata = $changeset->getMetadata();
150 $file_phid = idx($metadata, 'new:binary-phid');
151 if ($file_phid) {
152 $file_phids[$file_phid] = $file_phid;
156 $files = array();
157 if ($file_phids) {
158 $files = id(new PhabricatorFileQuery())
159 ->setViewer(PhabricatorUser::getOmnipotentUser())
160 ->withPHIDs($file_phids)
161 ->execute();
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');
169 if ($file_phid) {
170 if (!isset($files[$file_phid])) {
171 return true;
174 $drequest = DiffusionRequest::newFromDictionary(
175 array(
176 'user' => $viewer,
177 'repository' => $repository,
180 try {
181 $response = DiffusionQuery::callConduitWithDiffusionRequest(
182 $viewer,
183 $drequest,
184 'diffusion.filecontentquery',
185 array(
186 'commit' => $identifier,
187 'path' => $path,
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.
193 return true;
196 $new_file_phid = $response['filePHID'];
197 if (!$new_file_phid) {
198 return true;
201 $new_file = id(new PhabricatorFileQuery())
202 ->setViewer($viewer)
203 ->withPHIDs(array($new_file_phid))
204 ->executeOne();
205 if (!$new_file) {
206 return true;
209 if ($files[$file_phid]->loadFileData() != $new_file->loadFileData()) {
210 return true;
212 } else {
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:
219 // -(empty line)
220 // -echo 'test';
221 // (empty line)
223 // (empty line)
224 // -echo "test";
225 // -(empty line)
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()) {
231 return true;
236 return false;
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();
249 $changed_uri = null;
250 if ($old_diff) {
251 $old_diff = id(new DifferentialDiffQuery())
252 ->setViewer($viewer)
253 ->withIDs(array($old_diff->getID()))
254 ->needChangesets(true)
255 ->executeOne();
256 if ($old_diff) {
257 $has_changed = $this->isDiffChangedBeforeCommit(
258 $commit,
259 $old_diff,
260 $new_diff);
261 if ($has_changed) {
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);
272 $xactions = array();
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(
285 $this->getViewer(),
286 $revision,
287 $strict = false);
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(),
298 $wrong_builds =
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())
320 ->setActor($viewer)
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,
338 $strict) {
340 $diff = $revision->getActiveDiff();
342 $buildables = id(new HarbormasterBuildableQuery())
343 ->setViewer($viewer)
344 ->withBuildablePHIDs(array($diff->getPHID()))
345 ->needBuilds(true)
346 ->withManualBuildables(false)
347 ->execute();
348 if (!$buildables) {
349 return array();
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);
368 if ($if_never) {
369 continue;
372 $if_building = ($behavior_value === $key_building);
373 if ($if_building && $build->isComplete()) {
374 continue;
377 $if_complete = ($behavior_value === $key_complete);
378 if ($if_complete) {
379 if (!$build->isComplete()) {
380 continue;
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
390 // a bad state now.
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.
404 if (!$strict) {
405 continue;
409 if ($build->isPassed()) {
410 continue;
413 $concerning_builds[] = $build;
417 return $concerning_builds;