1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-27 01:02:42 +01:00

Add a "Needs Verification" state to Audit

Summary:
Fixes T2393. This allows authors to explicitly say "I think I fixed everything, please accept my commit now thank you".

Also improves behavior of "re-accept" and "re-reject" after new auditors you have authority over get added.

Test Plan:
  - Kicked a commit back and forth between an author and auditor by alternately using "Request Verification" and "Raise Concern".
  - Verified it showed up properly in bucketing for both users.
  - Accepted, added a project, accepted again (works now; didn't before).
  - Audited on behalf of projects / packages.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T2393

Differential Revision: https://secure.phabricator.com/D17252
This commit is contained in:
epriestley 2017-01-25 11:05:49 -08:00
parent ca182c7f48
commit 97cac83e9b
10 changed files with 191 additions and 32 deletions

View file

@ -674,6 +674,7 @@ phutil_register_library_map(array(
'DiffusionCommitStateTransaction' => 'applications/diffusion/xaction/DiffusionCommitStateTransaction.php', 'DiffusionCommitStateTransaction' => 'applications/diffusion/xaction/DiffusionCommitStateTransaction.php',
'DiffusionCommitTagsController' => 'applications/diffusion/controller/DiffusionCommitTagsController.php', 'DiffusionCommitTagsController' => 'applications/diffusion/controller/DiffusionCommitTagsController.php',
'DiffusionCommitTransactionType' => 'applications/diffusion/xaction/DiffusionCommitTransactionType.php', 'DiffusionCommitTransactionType' => 'applications/diffusion/xaction/DiffusionCommitTransactionType.php',
'DiffusionCommitVerifyTransaction' => 'applications/diffusion/xaction/DiffusionCommitVerifyTransaction.php',
'DiffusionCompareController' => 'applications/diffusion/controller/DiffusionCompareController.php', 'DiffusionCompareController' => 'applications/diffusion/controller/DiffusionCompareController.php',
'DiffusionConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionConduitAPIMethod.php', 'DiffusionConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionConduitAPIMethod.php',
'DiffusionController' => 'applications/diffusion/controller/DiffusionController.php', 'DiffusionController' => 'applications/diffusion/controller/DiffusionController.php',
@ -5382,6 +5383,7 @@ phutil_register_library_map(array(
'DiffusionCommitStateTransaction' => 'DiffusionCommitTransactionType', 'DiffusionCommitStateTransaction' => 'DiffusionCommitTransactionType',
'DiffusionCommitTagsController' => 'DiffusionController', 'DiffusionCommitTagsController' => 'DiffusionController',
'DiffusionCommitTransactionType' => 'PhabricatorModularTransactionType', 'DiffusionCommitTransactionType' => 'PhabricatorModularTransactionType',
'DiffusionCommitVerifyTransaction' => 'DiffusionCommitAuditTransaction',
'DiffusionCompareController' => 'DiffusionController', 'DiffusionCompareController' => 'DiffusionController',
'DiffusionConduitAPIMethod' => 'ConduitAPIMethod', 'DiffusionConduitAPIMethod' => 'ConduitAPIMethod',
'DiffusionController' => 'PhabricatorController', 'DiffusionController' => 'PhabricatorController',

View file

@ -7,12 +7,14 @@ final class PhabricatorAuditCommitStatusConstants extends Phobject {
const CONCERN_RAISED = 2; const CONCERN_RAISED = 2;
const PARTIALLY_AUDITED = 3; const PARTIALLY_AUDITED = 3;
const FULLY_AUDITED = 4; const FULLY_AUDITED = 4;
const NEEDS_VERIFICATION = 5;
public static function getStatusNameMap() { public static function getStatusNameMap() {
$map = array( $map = array(
self::NONE => pht('No Audits'), self::NONE => pht('No Audits'),
self::NEEDS_AUDIT => pht('Audit Required'), self::NEEDS_AUDIT => pht('Audit Required'),
self::CONCERN_RAISED => pht('Concern Raised'), self::CONCERN_RAISED => pht('Concern Raised'),
self::NEEDS_VERIFICATION => pht('Needs Verification'),
self::PARTIALLY_AUDITED => pht('Partially Audited'), self::PARTIALLY_AUDITED => pht('Partially Audited'),
self::FULLY_AUDITED => pht('Audited'), self::FULLY_AUDITED => pht('Audited'),
); );
@ -28,6 +30,7 @@ final class PhabricatorAuditCommitStatusConstants extends Phobject {
return array( return array(
self::CONCERN_RAISED, self::CONCERN_RAISED,
self::NEEDS_AUDIT, self::NEEDS_AUDIT,
self::NEEDS_VERIFICATION,
self::PARTIALLY_AUDITED, self::PARTIALLY_AUDITED,
); );
} }
@ -49,6 +52,9 @@ final class PhabricatorAuditCommitStatusConstants extends Phobject {
case self::NONE: case self::NONE:
$color = 'bluegrey'; $color = 'bluegrey';
break; break;
case self::NEEDS_VERIFICATION:
$color = 'indigo';
break;
default: default:
$color = null; $color = null;
break; break;
@ -73,6 +79,9 @@ final class PhabricatorAuditCommitStatusConstants extends Phobject {
case self::NONE: case self::NONE:
$icon = 'fa-check'; $icon = 'fa-check';
break; break;
case self::NEEDS_VERIFICATION:
$icon = 'fa-refresh';
break;
default: default:
$icon = null; $icon = null;
break; break;

View file

@ -11,6 +11,7 @@ final class PhabricatorAuditEditor
private $auditorPHIDs = array(); private $auditorPHIDs = array();
private $didExpandInlineState = false; private $didExpandInlineState = false;
private $oldAuditStatus = null;
public function addAuditReason($phid, $reason) { public function addAuditReason($phid, $reason) {
if (!isset($this->auditReasonMap[$phid])) { if (!isset($this->auditReasonMap[$phid])) {
@ -78,6 +79,8 @@ final class PhabricatorAuditEditor
} }
} }
$this->oldAuditStatus = $object->getAuditStatus();
$object->loadAndAttachAuditAuthority( $object->loadAndAttachAuditAuthority(
$this->getActor(), $this->getActor(),
$this->getActingAsPHID()); $this->getActingAsPHID());
@ -269,7 +272,7 @@ final class PhabricatorAuditEditor
} }
} }
$old_status = $object->getAuditStatus(); $old_status = $this->oldAuditStatus;
$requests = $object->getAudits(); $requests = $object->getAudits();
$object->updateAuditStatus($requests); $object->updateAuditStatus($requests);

View file

@ -33,6 +33,11 @@ final class DiffusionCommitRequiredActionResultBucket
->setNoDataString(pht('None of your commits have active concerns.')) ->setNoDataString(pht('None of your commits have active concerns.'))
->setObjects($this->filterConcernRaised($phids)); ->setObjects($this->filterConcernRaised($phids));
$groups[] = $this->newGroup()
->setName(pht('Needs Verification'))
->setNoDataString(pht('No commits are awaiting your verification.'))
->setObjects($this->filterNeedsVerification($phids));
$groups[] = $this->newGroup() $groups[] = $this->newGroup()
->setName(pht('Ready to Audit')) ->setName(pht('Ready to Audit'))
->setNoDataString(pht('No commits are waiting for you to audit them.')) ->setNoDataString(pht('No commits are waiting for you to audit them.'))
@ -82,6 +87,36 @@ final class DiffusionCommitRequiredActionResultBucket
return $results; return $results;
} }
private function filterNeedsVerification(array $phids) {
$results = array();
$objects = $this->objects;
$status_verify = PhabricatorAuditCommitStatusConstants::NEEDS_VERIFICATION;
$has_concern = array(
PhabricatorAuditStatusConstants::CONCERNED,
);
$has_concern = array_fuse($has_concern);
foreach ($objects as $key => $object) {
if (isset($phids[$object->getAuthorPHID()])) {
continue;
}
if ($object->getAuditStatus() != $status_verify) {
continue;
}
if (!$this->hasAuditorsWithStatus($object, $phids, $has_concern)) {
continue;
}
$results[$key] = $object;
unset($this->objects[$key]);
}
return $results;
}
private function filterShouldAudit(array $phids) { private function filterShouldAudit(array $phids) {
$results = array(); $results = array();
$objects = $this->objects; $objects = $this->objects;
@ -132,6 +167,7 @@ final class DiffusionCommitRequiredActionResultBucket
$status_waiting = array( $status_waiting = array(
PhabricatorAuditCommitStatusConstants::NEEDS_AUDIT, PhabricatorAuditCommitStatusConstants::NEEDS_AUDIT,
PhabricatorAuditCommitStatusConstants::NEEDS_VERIFICATION,
PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED, PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED,
); );
$status_waiting = array_fuse($status_waiting); $status_waiting = array_fuse($status_waiting);

View file

@ -30,11 +30,6 @@ final class DiffusionCommitAcceptTransaction
return pht('Accepted'); return pht('Accepted');
} }
public function generateOldValue($object) {
$actor = $this->getActor();
return $this->isViewerAcceptingAuditor($object, $actor);
}
public function applyExternalEffects($object, $value) { public function applyExternalEffects($object, $value) {
$status = PhabricatorAuditStatusConstants::ACCEPTED; $status = PhabricatorAuditStatusConstants::ACCEPTED;
$actor = $this->getActor(); $actor = $this->getActor();
@ -54,7 +49,7 @@ final class DiffusionCommitAcceptTransaction
} }
} }
if ($this->isViewerAcceptingAuditor($object, $viewer)) { if ($this->isViewerFullyAccepted($object, $viewer)) {
throw new Exception( throw new Exception(
pht( pht(
'You can not accept this commit because you have already '. 'You can not accept this commit because you have already '.

View file

@ -7,6 +7,10 @@ abstract class DiffusionCommitAuditTransaction
return DiffusionCommitEditEngine::ACTIONGROUP_AUDIT; return DiffusionCommitEditEngine::ACTIONGROUP_AUDIT;
} }
public function generateOldValue($object) {
return false;
}
protected function isViewerAnyAuditor( protected function isViewerAnyAuditor(
PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommit $commit,
PhabricatorUser $viewer) { PhabricatorUser $viewer) {
@ -18,22 +22,23 @@ abstract class DiffusionCommitAuditTransaction
PhabricatorUser $viewer) { PhabricatorUser $viewer) {
// This omits various inactive states like "Resigned" and "Not Required". // This omits various inactive states like "Resigned" and "Not Required".
$active = array(
return $this->isViewerAuditStatusAmong(
$commit,
$viewer,
array(
PhabricatorAuditStatusConstants::AUDIT_REQUIRED, PhabricatorAuditStatusConstants::AUDIT_REQUIRED,
PhabricatorAuditStatusConstants::CONCERNED, PhabricatorAuditStatusConstants::CONCERNED,
PhabricatorAuditStatusConstants::ACCEPTED, PhabricatorAuditStatusConstants::ACCEPTED,
PhabricatorAuditStatusConstants::AUDIT_REQUESTED, PhabricatorAuditStatusConstants::AUDIT_REQUESTED,
)); );
$active = array_fuse($active);
$viewer_status = $this->getViewerAuditStatus($commit, $viewer);
return isset($active[$viewer_status]);
} }
protected function isViewerAcceptingAuditor( protected function isViewerFullyAccepted(
PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommit $commit,
PhabricatorUser $viewer) { PhabricatorUser $viewer) {
return $this->isViewerAuditStatusAmong( return $this->isViewerAuditStatusFullyAmong(
$commit, $commit,
$viewer, $viewer,
array( array(
@ -41,10 +46,10 @@ abstract class DiffusionCommitAuditTransaction
)); ));
} }
protected function isViewerRejectingAuditor( protected function isViewerFullyRejected(
PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommit $commit,
PhabricatorUser $viewer) { PhabricatorUser $viewer) {
return $this->isViewerAuditStatusAmong( return $this->isViewerAuditStatusFullyAmong(
$commit, $commit,
$viewer, $viewer,
array( array(
@ -71,7 +76,7 @@ abstract class DiffusionCommitAuditTransaction
return null; return null;
} }
protected function isViewerAuditStatusAmong( protected function isViewerAuditStatusFullyAmong(
PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommit $commit,
PhabricatorUser $viewer, PhabricatorUser $viewer,
array $status_list) { array $status_list) {
@ -82,7 +87,20 @@ abstract class DiffusionCommitAuditTransaction
} }
$status_map = array_fuse($status_list); $status_map = array_fuse($status_list);
return isset($status_map[$status]); foreach ($commit->getAudits() as $audit) {
if (!$commit->hasAuditAuthority($viewer, $audit)) {
continue;
}
$status = $audit->getAuditStatus();
if (isset($status_map[$status])) {
continue;
}
return false;
}
return true;
} }
protected function applyAuditorEffect( protected function applyAuditorEffect(

View file

@ -30,9 +30,11 @@ final class DiffusionCommitConcernTransaction
return pht('Raised Concern'); return pht('Raised Concern');
} }
public function generateOldValue($object) { public function applyInternalEffects($object, $value) {
$actor = $this->getActor(); // NOTE: We force the commit directly into "Concern Raised" so that we
return $this->isViewerRejectingAuditor($object, $actor); // override a possible "Needs Verification" state.
$object->setAuditStatus(
PhabricatorAuditCommitStatusConstants::CONCERN_RAISED);
} }
public function applyExternalEffects($object, $value) { public function applyExternalEffects($object, $value) {
@ -50,13 +52,19 @@ final class DiffusionCommitConcernTransaction
'you did not author.')); 'you did not author.'));
} }
if ($this->isViewerRejectingAuditor($object, $viewer)) { // Even if you've already raised a concern, you can raise again as long
// as the author requsted you verify.
$state_verify = PhabricatorAuditCommitStatusConstants::NEEDS_VERIFICATION;
if ($this->isViewerFullyRejected($object, $viewer)) {
if ($object->getAuditStatus() != $state_verify) {
throw new Exception( throw new Exception(
pht( pht(
'You can not raise a concern with this commit because you have '. 'You can not raise a concern with this commit because you have '.
'already raised a concern with it.')); 'already raised a concern with it.'));
} }
} }
}
public function getTitle() { public function getTitle() {
return pht( return pht(

View file

@ -31,6 +31,8 @@ final class DiffusionCommitStateTransaction
return pht('This commit now requires audit.'); return pht('This commit now requires audit.');
case PhabricatorAuditCommitStatusConstants::CONCERN_RAISED: case PhabricatorAuditCommitStatusConstants::CONCERN_RAISED:
return pht('This commit now has outstanding concerns.'); return pht('This commit now has outstanding concerns.');
case PhabricatorAuditCommitStatusConstants::NEEDS_VERIFICATION:
return pht('This commit now requires verification by auditors.');
case PhabricatorAuditCommitStatusConstants::FULLY_AUDITED: case PhabricatorAuditCommitStatusConstants::FULLY_AUDITED:
return pht('All concerns with this commit have now been addressed.'); return pht('All concerns with this commit have now been addressed.');
} }
@ -54,6 +56,10 @@ final class DiffusionCommitStateTransaction
return pht( return pht(
'%s now has outstanding concerns.', '%s now has outstanding concerns.',
$this->renderObject()); $this->renderObject());
case PhabricatorAuditCommitStatusConstants::NEEDS_VERIFICATION:
return pht(
'%s now requires verification by auditors.',
$this->renderObject());
case PhabricatorAuditCommitStatusConstants::FULLY_AUDITED: case PhabricatorAuditCommitStatusConstants::FULLY_AUDITED:
return pht( return pht(
'All concerns with %s have now been addressed.', 'All concerns with %s have now been addressed.',

View file

@ -0,0 +1,73 @@
<?php
final class DiffusionCommitVerifyTransaction
extends DiffusionCommitAuditTransaction {
const TRANSACTIONTYPE = 'diffusion.commit.verify';
const ACTIONKEY = 'verify';
protected function getCommitActionLabel() {
return pht('Request Verification');
}
protected function getCommitActionDescription() {
return pht(
'Auditors will be asked to verify that concerns have been addressed.');
}
protected function getCommitActionGroupKey() {
return DiffusionCommitEditEngine::ACTIONGROUP_COMMIT;
}
public function getIcon() {
return 'fa-refresh';
}
public function getColor() {
return 'indigo';
}
protected function getCommitActionOrder() {
return 600;
}
public function getActionName() {
return pht('Requested Verification');
}
public function applyInternalEffects($object, $value) {
$object->setAuditStatus(
PhabricatorAuditCommitStatusConstants::NEEDS_VERIFICATION);
}
protected function validateAction($object, PhabricatorUser $viewer) {
if (!$this->isViewerCommitAuthor($object, $viewer)) {
throw new Exception(
pht(
'You can not request verification of this commit because you '.
'are not the author.'));
}
$status = $object->getAuditStatus();
if ($status != PhabricatorAuditCommitStatusConstants::CONCERN_RAISED) {
throw new Exception(
pht(
'You can not request verification of this commit because no '.
'auditors have raised conerns with it.'));
}
}
public function getTitle() {
return pht(
'%s requested verification of this commit.',
$this->renderAuthor());
}
public function getTitleForFeed() {
return pht(
'%s requested verification of %s.',
$this->renderAuthor(),
$this->renderObject());
}
}

View file

@ -327,8 +327,17 @@ final class PhabricatorRepositoryCommit
} }
} }
$current_status = $this->getAuditStatus();
$status_verify = PhabricatorAuditCommitStatusConstants::NEEDS_VERIFICATION;
if ($any_concern) { if ($any_concern) {
if ($current_status == $status_verify) {
// If the change is in "Needs Verification", we keep it there as
// long as any auditors still have concerns.
$status = $status_verify;
} else {
$status = PhabricatorAuditCommitStatusConstants::CONCERN_RAISED; $status = PhabricatorAuditCommitStatusConstants::CONCERN_RAISED;
}
} else if ($any_accept) { } else if ($any_accept) {
if ($any_need) { if ($any_need) {
$status = PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED; $status = PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED;