@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
at upstream/main 420 lines 13 kB view raw
1<?php 2 3final class DifferentialDiffExtractionEngine extends Phobject { 4 5 private $viewer; 6 private $authorPHID; 7 8 public function setViewer(PhabricatorUser $viewer) { 9 $this->viewer = $viewer; 10 return $this; 11 } 12 13 public function getViewer() { 14 return $this->viewer; 15 } 16 17 public function setAuthorPHID($author_phid) { 18 $this->authorPHID = $author_phid; 19 return $this; 20 } 21 22 public function getAuthorPHID() { 23 return $this->authorPHID; 24 } 25 26 public function newDiffFromCommit(PhabricatorRepositoryCommit $commit) { 27 $viewer = $this->getViewer(); 28 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); 40 } 41 42 $repository = $commit->getRepository(); 43 $identifier = $commit->getCommitIdentifier(); 44 $monogram = $commit->getMonogram(); 45 46 $drequest = DiffusionRequest::newFromDictionary( 47 array( 48 'user' => $viewer, 49 'repository' => $repository, 50 )); 51 52 $diff_info = DiffusionQuery::callConduitWithDiffusionRequest( 53 $viewer, 54 $drequest, 55 'diffusion.rawdiffquery', 56 array( 57 'commit' => $identifier, 58 )); 59 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')); 71 } 72 73 $raw_diff = $diff_file->loadFileData(); 74 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(); 85 } 86 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); 96 97 $author_phid = $this->getAuthorPHID(); 98 if ($author_phid !== null) { 99 $diff->setAuthorPHID($author_phid); 100 } 101 102 $parents = DiffusionQuery::callConduitWithDiffusionRequest( 103 $viewer, 104 $drequest, 105 'diffusion.commitparentsquery', 106 array( 107 'commit' => $identifier, 108 )); 109 110 if ($parents) { 111 $diff->setSourceControlBaseRevision(head($parents)); 112 } 113 114 // TODO: Attach binary files. 115 116 return $diff->save(); 117 } 118 119 public function isDiffChangedBeforeCommit( 120 PhabricatorRepositoryCommit $commit, 121 DifferentialDiff $old, 122 DifferentialDiff $new) { 123 124 $viewer = $this->getViewer(); 125 $repository = $commit->getRepository(); 126 $identifier = $commit->getCommitIdentifier(); 127 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; 133 } 134 135 $changesets = array(); 136 foreach ($new->getChangesets() as $changeset) { 137 $path = $changeset->getAbsoluteRepositoryPath($repository, $new); 138 $path = ltrim($path, '/'); 139 $changesets[$path] = $changeset; 140 } 141 142 if (array_fill_keys(array_keys($changesets), true) != 143 array_fill_keys(array_keys($vs_changesets), true)) { 144 return true; 145 } 146 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; 153 } 154 } 155 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'); 163 } 164 165 foreach ($changesets as $path => $changeset) { 166 $vs_changeset = $vs_changesets[$path]; 167 168 $file_phid = idx($vs_changeset->getMetadata(), 'new:binary-phid'); 169 if ($file_phid) { 170 if (!isset($files[$file_phid])) { 171 return true; 172 } 173 174 $drequest = DiffusionRequest::newFromDictionary( 175 array( 176 'user' => $viewer, 177 'repository' => $repository, 178 )); 179 180 try { 181 $response = DiffusionQuery::callConduitWithDiffusionRequest( 182 $viewer, 183 $drequest, 184 'diffusion.filecontentquery', 185 array( 186 'commit' => $identifier, 187 'path' => $path, 188 )); 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; 194 } 195 196 $new_file_phid = $response['filePHID']; 197 if (!$new_file_phid) { 198 return true; 199 } 200 201 $new_file = id(new PhabricatorFileQuery()) 202 ->setViewer($viewer) 203 ->withPHIDs(array($new_file_phid)) 204 ->executeOne(); 205 if (!$new_file) { 206 return true; 207 } 208 209 if ($files[$file_phid]->loadFileData() != $new_file->loadFileData()) { 210 return true; 211 } 212 } else { 213 $context = implode("\n", $changeset->makeChangesWithContext()); 214 $vs_context = implode("\n", $vs_changeset->makeChangesWithContext()); 215 216 // We couldn't just compare $context and $vs_context because following 217 // diffs will be considered different: 218 // 219 // -(empty line) 220 // -echo 'test'; 221 // (empty line) 222 // 223 // (empty line) 224 // -echo "test"; 225 // -(empty line) 226 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; 232 } 233 } 234 } 235 236 return false; 237 } 238 239 public function updateRevisionWithCommit( 240 DifferentialRevision $revision, 241 PhabricatorRepositoryCommit $commit, 242 array $more_xactions, 243 PhabricatorContentSource $content_source) { 244 245 $viewer = $this->getViewer(); 246 $new_diff = $this->newDiffFromCommit($commit); 247 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(); 265 266 $changed_uri = "/{$revision_monogram}?vs={$old_id}&id={$new_id}#toc"; 267 $changed_uri = PhabricatorEnv::getProductionURI($changed_uri); 268 } 269 } 270 } 271 272 $xactions = array(); 273 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; 278 279 $xactions[] = id(new DifferentialTransaction()) 280 ->setTransactionType($wrong_type) 281 ->setNewValue($revision->getModernRevisionStatus()); 282 } 283 284 $concerning_builds = self::loadConcerningBuilds( 285 $this->getViewer(), 286 $revision, 287 $strict = false); 288 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(), 295 ); 296 } 297 298 $wrong_builds = 299 DifferentialRevisionWrongBuildsTransaction::TRANSACTIONTYPE; 300 301 $xactions[] = id(new DifferentialTransaction()) 302 ->setTransactionType($wrong_builds) 303 ->setNewValue($build_list); 304 } 305 306 $type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE; 307 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())); 314 315 foreach ($more_xactions as $more_xaction) { 316 $xactions[] = $more_xaction; 317 } 318 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); 326 327 $author_phid = $this->getAuthorPHID(); 328 if ($author_phid !== null) { 329 $editor->setActingAsPHID($author_phid); 330 } 331 332 $editor->applyTransactions($revision, $xactions); 333 } 334 335 public static function loadConcerningBuilds( 336 PhabricatorUser $viewer, 337 DifferentialRevision $revision, 338 $strict) { 339 340 $diff = $revision->getActiveDiff(); 341 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(); 350 } 351 352 $land_key = HarbormasterBuildPlanBehavior::BEHAVIOR_LANDWARNING; 353 $behavior = HarbormasterBuildPlanBehavior::getBehavior($land_key); 354 355 $key_never = HarbormasterBuildPlanBehavior::LANDWARNING_NEVER; 356 $key_building = HarbormasterBuildPlanBehavior::LANDWARNING_IF_BUILDING; 357 $key_complete = HarbormasterBuildPlanBehavior::LANDWARNING_IF_COMPLETE; 358 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(); 366 367 $if_never = ($behavior_value === $key_never); 368 if ($if_never) { 369 continue; 370 } 371 372 $if_building = ($behavior_value === $key_building); 373 if ($if_building && $build->isComplete()) { 374 continue; 375 } 376 377 $if_complete = ($behavior_value === $key_complete); 378 if ($if_complete) { 379 if (!$build->isComplete()) { 380 continue; 381 } 382 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. 387 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. 391 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. 399 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. 403 404 if (!$strict) { 405 continue; 406 } 407 } 408 409 if ($build->isPassed()) { 410 continue; 411 } 412 413 $concerning_builds[] = $build; 414 } 415 } 416 417 return $concerning_builds; 418 } 419 420}