@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
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Allow users to "Force accept" package reviews if they own a more general package

Summary:
Ref T12272. If you own a package which owns "/", this allows you to force-accept package reviews for packages which own sub-paths, like "/src/adventure/".

The default UI looks something like this:

```
[X] Accept as epriestley
[X] Accept as Root Package
[ ] Force accept as Adventure Package
```

By default, force-accepts are not selected.

(I may do some UI cleanup and/or annotate "because you own X" in the future and/or mark these accepts specially in some way, particularly if this proves confusing along whatever dimension.)

Test Plan: {F4314747}

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T12272

Differential Revision: https://secure.phabricator.com/D17569

+332 -19
+2 -4
src/applications/differential/controller/DifferentialController.php
··· 54 54 $toc_view->setAuthorityPackages($packages); 55 55 } 56 56 57 - // TODO: For Subversion, we should adjust these paths to be relative to 58 - // the repository root where possible. 59 - $paths = mpull($changesets, 'getFilename'); 57 + $paths = mpull($changesets, 'getOwnersFilename'); 60 58 61 59 $control_query = id(new PhabricatorOwnersPackageQuery()) 62 60 ->setViewer($viewer) ··· 83 81 if ($have_owners) { 84 82 $packages = $control_query->getControllingPackagesForPath( 85 83 $repository_phid, 86 - $changeset->getFilename()); 84 + $changeset->getOwnersFilename()); 87 85 $item->setPackages($packages); 88 86 } 89 87
-2
src/applications/differential/editor/DifferentialTransactionEditor.php
··· 1857 1857 $acting_phid); 1858 1858 } 1859 1859 1860 - 1861 - 1862 1860 }
+17
src/applications/differential/storage/DifferentialChangeset.php
··· 75 75 return $name; 76 76 } 77 77 78 + public function getOwnersFilename() { 79 + // TODO: For Subversion, we should adjust these paths to be relative to 80 + // the repository root where possible. 81 + 82 + $path = $this->getFilename(); 83 + 84 + if (!isset($path[0])) { 85 + return '/'; 86 + } 87 + 88 + if ($path[0] != '/') { 89 + $path = '/'.$path; 90 + } 91 + 92 + return $path; 93 + } 94 + 78 95 public function addUnsavedHunk(DifferentialHunk $hunk) { 79 96 if ($this->hunks === self::ATTACHABLE) { 80 97 $this->hunks = array();
+5
src/applications/differential/storage/DifferentialReviewer.php
··· 39 39 return (phid_get_type($this->getReviewerPHID()) == $user_type); 40 40 } 41 41 42 + public function isPackage() { 43 + $package_type = PhabricatorOwnersPackagePHIDType::TYPECONST; 44 + return (phid_get_type($this->getReviewerPHID()) == $package_type); 45 + } 46 + 42 47 public function attachAuthority(PhabricatorUser $user, $has_authority) { 43 48 $this->authority[$user->getCacheFragment()] = $has_authority; 44 49 return $this;
+238
src/applications/differential/storage/DifferentialRevision.php
··· 48 48 private $customFields = self::ATTACHABLE; 49 49 private $drafts = array(); 50 50 private $flags = array(); 51 + private $forceMap = array(); 51 52 52 53 const TABLE_COMMIT = 'differential_commit'; 53 54 ··· 243 244 public function attachHashes(array $hashes) { 244 245 $this->hashes = $hashes; 245 246 return $this; 247 + } 248 + 249 + public function canReviewerForceAccept( 250 + PhabricatorUser $viewer, 251 + DifferentialReviewer $reviewer) { 252 + 253 + if (!$reviewer->isPackage()) { 254 + return false; 255 + } 256 + 257 + $map = $this->getReviewerForceAcceptMap($viewer); 258 + if (!$map) { 259 + return false; 260 + } 261 + 262 + if (isset($map[$reviewer->getReviewerPHID()])) { 263 + return true; 264 + } 265 + 266 + return false; 267 + } 268 + 269 + private function getReviewerForceAcceptMap(PhabricatorUser $viewer) { 270 + $fragment = $viewer->getCacheFragment(); 271 + 272 + if (!array_key_exists($fragment, $this->forceMap)) { 273 + $map = $this->newReviewerForceAcceptMap($viewer); 274 + $this->forceMap[$fragment] = $map; 275 + } 276 + 277 + return $this->forceMap[$fragment]; 278 + } 279 + 280 + private function newReviewerForceAcceptMap(PhabricatorUser $viewer) { 281 + $diff = $this->getActiveDiff(); 282 + if (!$diff) { 283 + return null; 284 + } 285 + 286 + $repository_phid = $diff->getRepositoryPHID(); 287 + if (!$repository_phid) { 288 + return null; 289 + } 290 + 291 + $paths = array(); 292 + 293 + try { 294 + $changesets = $diff->getChangesets(); 295 + } catch (Exception $ex) { 296 + $changesets = id(new DifferentialChangesetQuery()) 297 + ->setViewer($viewer) 298 + ->withDiffs(array($diff)) 299 + ->execute(); 300 + } 301 + 302 + foreach ($changesets as $changeset) { 303 + $paths[] = $changeset->getOwnersFilename(); 304 + } 305 + 306 + if (!$paths) { 307 + return null; 308 + } 309 + 310 + $reviewer_phids = array(); 311 + foreach ($this->getReviewers() as $reviewer) { 312 + if (!$reviewer->isPackage()) { 313 + continue; 314 + } 315 + 316 + $reviewer_phids[] = $reviewer->getReviewerPHID(); 317 + } 318 + 319 + if (!$reviewer_phids) { 320 + return null; 321 + } 322 + 323 + // Load all the reviewing packages which have control over some of the 324 + // paths in the change. These are packages which the actor may be able 325 + // to force-accept on behalf of. 326 + $control_query = id(new PhabricatorOwnersPackageQuery()) 327 + ->setViewer($viewer) 328 + ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE)) 329 + ->withPHIDs($reviewer_phids) 330 + ->withControl($repository_phid, $paths); 331 + $control_packages = $control_query->execute(); 332 + if (!$control_packages) { 333 + return null; 334 + } 335 + 336 + // Load all the packages which have potential control over some of the 337 + // paths in the change and are owned by the actor. These are packages 338 + // which the actor may be able to use their authority over to gain the 339 + // ability to force-accept for other packages. This query doesn't apply 340 + // dominion rules yet, and we'll bypass those rules later on. 341 + $authority_query = id(new PhabricatorOwnersPackageQuery()) 342 + ->setViewer($viewer) 343 + ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE)) 344 + ->withAuthorityPHIDs(array($viewer->getPHID())) 345 + ->withControl($repository_phid, $paths); 346 + $authority_packages = $authority_query->execute(); 347 + if (!$authority_packages) { 348 + return null; 349 + } 350 + $authority_packages = mpull($authority_packages, null, 'getPHID'); 351 + 352 + // Build a map from each path in the revision to the reviewer packages 353 + // which control it. 354 + $control_map = array(); 355 + foreach ($paths as $path) { 356 + $control_packages = $control_query->getControllingPackagesForPath( 357 + $repository_phid, 358 + $path); 359 + 360 + // Remove packages which the viewer has authority over. We don't need 361 + // to check these for force-accept because they can just accept them 362 + // normally. 363 + $control_packages = mpull($control_packages, null, 'getPHID'); 364 + foreach ($control_packages as $phid => $control_package) { 365 + if (isset($authority_packages[$phid])) { 366 + unset($control_packages[$phid]); 367 + } 368 + } 369 + 370 + if (!$control_packages) { 371 + continue; 372 + } 373 + 374 + $control_map[$path] = $control_packages; 375 + } 376 + 377 + if (!$control_map) { 378 + return null; 379 + } 380 + 381 + // From here on out, we only care about paths which we have at least one 382 + // controlling package for. 383 + $paths = array_keys($control_map); 384 + 385 + // Now, build a map from each path to the packages which would control it 386 + // if there were no dominion rules. 387 + $authority_map = array(); 388 + foreach ($paths as $path) { 389 + $authority_packages = $authority_query->getControllingPackagesForPath( 390 + $repository_phid, 391 + $path, 392 + $ignore_dominion = true); 393 + 394 + $authority_map[$path] = mpull($authority_packages, null, 'getPHID'); 395 + } 396 + 397 + // For each path, find the most general package that the viewer has 398 + // authority over. For example, we'll prefer a package that owns "/" to a 399 + // package that owns "/src/". 400 + $force_map = array(); 401 + foreach ($authority_map as $path => $package_map) { 402 + $path_fragments = PhabricatorOwnersPackage::splitPath($path); 403 + $fragment_count = count($path_fragments); 404 + 405 + // Find the package that we have authority over which has the most 406 + // general match for this path. 407 + $best_match = null; 408 + $best_package = null; 409 + foreach ($package_map as $package_phid => $package) { 410 + $package_paths = $package->getPathsForRepository($repository_phid); 411 + foreach ($package_paths as $package_path) { 412 + 413 + // NOTE: A strength of 0 means "no match". A strength of 1 means 414 + // that we matched "/", so we can not possibly find another stronger 415 + // match. 416 + 417 + $strength = $package_path->getPathMatchStrength( 418 + $path_fragments, 419 + $fragment_count); 420 + if (!$strength) { 421 + continue; 422 + } 423 + 424 + if ($strength < $best_match || !$best_package) { 425 + $best_match = $strength; 426 + $best_package = $package; 427 + if ($strength == 1) { 428 + break 2; 429 + } 430 + } 431 + } 432 + } 433 + 434 + if ($best_package) { 435 + $force_map[$path] = array( 436 + 'strength' => $best_match, 437 + 'package' => $best_package, 438 + ); 439 + } 440 + } 441 + 442 + // For each path which the viewer owns a package for, find other packages 443 + // which that authority can be used to force-accept. Once we find a way to 444 + // force-accept a package, we don't need to keep loooking. 445 + $has_control = array(); 446 + foreach ($force_map as $path => $spec) { 447 + $path_fragments = PhabricatorOwnersPackage::splitPath($path); 448 + $fragment_count = count($path_fragments); 449 + 450 + $authority_strength = $spec['strength']; 451 + 452 + $control_packages = $control_map[$path]; 453 + foreach ($control_packages as $control_phid => $control_package) { 454 + if (isset($has_control[$control_phid])) { 455 + continue; 456 + } 457 + 458 + $control_paths = $control_package->getPathsForRepository( 459 + $repository_phid); 460 + foreach ($control_paths as $control_path) { 461 + $strength = $control_path->getPathMatchStrength( 462 + $path_fragments, 463 + $fragment_count); 464 + 465 + if (!$strength) { 466 + continue; 467 + } 468 + 469 + if ($strength > $authority_strength) { 470 + $authority = $spec['package']; 471 + $has_control[$control_phid] = array( 472 + 'authority' => $authority, 473 + 'phid' => $authority->getPHID(), 474 + ); 475 + break; 476 + } 477 + } 478 + } 479 + } 480 + 481 + // Return a map from packages which may be force accepted to the packages 482 + // which permit that forced acceptance. 483 + return ipull($has_control, 'phid'); 246 484 } 247 485 248 486
+31 -7
src/applications/differential/xaction/DifferentialRevisionAcceptTransaction.php
··· 84 84 } 85 85 } 86 86 87 + $default_unchecked = array(); 87 88 foreach ($reviewers as $reviewer) { 89 + $reviewer_phid = $reviewer->getReviewerPHID(); 90 + 88 91 if (!$reviewer->hasAuthority($viewer)) { 89 92 // If the viewer doesn't have authority to act on behalf of a reviewer, 90 - // don't include that reviewer as an option. 91 - continue; 93 + // we check if they can accept by force. 94 + if ($revision->canReviewerForceAccept($viewer, $reviewer)) { 95 + $default_unchecked[$reviewer_phid] = true; 96 + } else { 97 + continue; 98 + } 92 99 } 93 100 94 101 if ($reviewer->isAccepted($diff_phid)) { ··· 97 104 continue; 98 105 } 99 106 100 - $reviewer_phid = $reviewer->getReviewerPHID(); 101 107 $reviewer_phids[$reviewer_phid] = $reviewer_phid; 102 108 } 103 109 104 110 $handles = $viewer->loadHandles($reviewer_phids); 105 111 112 + $head = array(); 113 + $tail = array(); 106 114 foreach ($reviewer_phids as $reviewer_phid) { 107 - $options[$reviewer_phid] = pht( 108 - 'Accept as %s', 109 - $viewer->renderHandle($reviewer_phid)); 115 + $is_force = isset($default_unchecked[$reviewer_phid]); 110 116 111 - $value[] = $reviewer_phid; 117 + if ($is_force) { 118 + $tail[] = $reviewer_phid; 119 + 120 + $options[$reviewer_phid] = pht( 121 + 'Force accept as %s', 122 + $viewer->renderHandle($reviewer_phid)); 123 + } else { 124 + $head[] = $reviewer_phid; 125 + $value[] = $reviewer_phid; 126 + 127 + $options[$reviewer_phid] = pht( 128 + 'Accept as %s', 129 + $viewer->renderHandle($reviewer_phid)); 130 + } 112 131 } 132 + 133 + // Reorder reviewers so "force accept" reviewers come at the end. 134 + $options = 135 + array_select_keys($options, $head) + 136 + array_select_keys($options, $tail); 113 137 114 138 return array($options, $value); 115 139 }
+6 -1
src/applications/differential/xaction/DifferentialRevisionActionTransaction.php
··· 122 122 $field->setActionConflictKey('revision.action'); 123 123 124 124 list($options, $value) = $this->getActionOptions($viewer, $revision); 125 - if (count($options) > 1) { 125 + 126 + // Show the options if the user can select on behalf of two or more 127 + // reviewers, or can force-accept on behalf of one or more reviewers. 128 + $can_multi = (count($options) > 1); 129 + $can_force = (count($value) < count($options)); 130 + if ($can_multi || $can_force) { 126 131 $field->setOptions($options); 127 132 $field->setValue($value); 128 133 }
+23 -3
src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php
··· 109 109 // the desired set of states. 110 110 foreach ($revision->getReviewers() as $reviewer) { 111 111 if (!$reviewer->hasAuthority($viewer)) { 112 - continue; 112 + $can_force = false; 113 + 114 + if ($is_accepted) { 115 + if ($revision->canReviewerForceAccept($viewer, $reviewer)) { 116 + $can_force = true; 117 + } 118 + } 119 + 120 + if (!$can_force) { 121 + continue; 122 + } 113 123 } 114 124 115 125 $status = $reviewer->getReviewerStatus(); ··· 152 162 // reviewers you have authority for. When you resign, you only affect 153 163 // yourself. 154 164 $with_authority = ($status != DifferentialReviewerStatus::STATUS_RESIGNED); 165 + $with_force = ($status == DifferentialReviewerStatus::STATUS_ACCEPTED); 166 + 155 167 if ($with_authority) { 156 168 foreach ($revision->getReviewers() as $reviewer) { 157 - if ($reviewer->hasAuthority($viewer)) { 158 - $map[$reviewer->getReviewerPHID()] = $status; 169 + if (!$reviewer->hasAuthority($viewer)) { 170 + if (!$with_force) { 171 + continue; 172 + } 173 + 174 + if (!$revision->canReviewerForceAccept($viewer, $reviewer)) { 175 + continue; 176 + } 159 177 } 178 + 179 + $map[$reviewer->getReviewerPHID()] = $status; 160 180 } 161 181 } 162 182
+10 -2
src/applications/owners/query/PhabricatorOwnersPackageQuery.php
··· 348 348 * 349 349 * @return list<PhabricatorOwnersPackage> List of controlling packages. 350 350 */ 351 - public function getControllingPackagesForPath($repository_phid, $path) { 351 + public function getControllingPackagesForPath( 352 + $repository_phid, 353 + $path, 354 + $ignore_dominion = false) { 352 355 $path = (string)$path; 353 356 354 357 if (!isset($this->controlMap[$repository_phid][$path])) { ··· 382 385 } 383 386 384 387 if ($best_match && $include) { 388 + if ($ignore_dominion) { 389 + $is_weak = false; 390 + } else { 391 + $is_weak = ($package->getDominion() == $weak_dominion); 392 + } 385 393 $matches[$package_id] = array( 386 394 'strength' => $best_match, 387 - 'weak' => ($package->getDominion() == $weak_dominion), 395 + 'weak' => $is_weak, 388 396 'package' => $package, 389 397 ); 390 398 }