Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F81708536
DifferentialTransactionEditor.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Sat, Sep 7, 13:20
Size
63 KB
Mime Type
text/x-php
Expires
Mon, Sep 9, 13:20 (2 d)
Engine
blob
Format
Raw Data
Handle
20609142
Attached To
rPH Phabricator
DifferentialTransactionEditor.php
View Options
<?php
final
class
DifferentialTransactionEditor
extends
PhabricatorApplicationTransactionEditor
{
private
$heraldEmailPHIDs
;
private
$changedPriorToCommitURI
;
private
$isCloseByCommit
;
private
$repositoryPHIDOverride
=
false
;
private
$didExpandInlineState
=
false
;
public
function
getEditorApplicationClass
()
{
return
'PhabricatorDifferentialApplication'
;
}
public
function
getEditorObjectsDescription
()
{
return
pht
(
'Differential Revisions'
);
}
public
function
getDiffUpdateTransaction
(
array
$xactions
)
{
$type_update
=
DifferentialTransaction
::
TYPE_UPDATE
;
foreach
(
$xactions
as
$xaction
)
{
if
(
$xaction
->
getTransactionType
()
==
$type_update
)
{
return
$xaction
;
}
}
return
null
;
}
public
function
setIsCloseByCommit
(
$is_close_by_commit
)
{
$this
->
isCloseByCommit
=
$is_close_by_commit
;
return
$this
;
}
public
function
getIsCloseByCommit
()
{
return
$this
->
isCloseByCommit
;
}
public
function
setChangedPriorToCommitURI
(
$uri
)
{
$this
->
changedPriorToCommitURI
=
$uri
;
return
$this
;
}
public
function
getChangedPriorToCommitURI
()
{
return
$this
->
changedPriorToCommitURI
;
}
public
function
setRepositoryPHIDOverride
(
$phid_or_null
)
{
$this
->
repositoryPHIDOverride
=
$phid_or_null
;
return
$this
;
}
public
function
getTransactionTypes
()
{
$types
=
parent
::
getTransactionTypes
();
$types
[]
=
PhabricatorTransactions
::
TYPE_COMMENT
;
$types
[]
=
PhabricatorTransactions
::
TYPE_VIEW_POLICY
;
$types
[]
=
PhabricatorTransactions
::
TYPE_EDIT_POLICY
;
$types
[]
=
DifferentialTransaction
::
TYPE_ACTION
;
$types
[]
=
DifferentialTransaction
::
TYPE_INLINE
;
$types
[]
=
DifferentialTransaction
::
TYPE_STATUS
;
$types
[]
=
DifferentialTransaction
::
TYPE_UPDATE
;
return
$types
;
}
protected
function
getCustomTransactionOldValue
(
PhabricatorLiskDAO
$object
,
PhabricatorApplicationTransaction
$xaction
)
{
switch
(
$xaction
->
getTransactionType
())
{
case
PhabricatorTransactions
::
TYPE_VIEW_POLICY
:
return
$object
->
getViewPolicy
();
case
PhabricatorTransactions
::
TYPE_EDIT_POLICY
:
return
$object
->
getEditPolicy
();
case
DifferentialTransaction
::
TYPE_ACTION
:
return
null
;
case
DifferentialTransaction
::
TYPE_INLINE
:
return
null
;
case
DifferentialTransaction
::
TYPE_UPDATE
:
if
(
$this
->
getIsNewObject
())
{
return
null
;
}
else
{
return
$object
->
getActiveDiff
()->
getPHID
();
}
}
return
parent
::
getCustomTransactionOldValue
(
$object
,
$xaction
);
}
protected
function
getCustomTransactionNewValue
(
PhabricatorLiskDAO
$object
,
PhabricatorApplicationTransaction
$xaction
)
{
switch
(
$xaction
->
getTransactionType
())
{
case
PhabricatorTransactions
::
TYPE_VIEW_POLICY
:
case
PhabricatorTransactions
::
TYPE_EDIT_POLICY
:
case
DifferentialTransaction
::
TYPE_ACTION
:
case
DifferentialTransaction
::
TYPE_UPDATE
:
return
$xaction
->
getNewValue
();
case
DifferentialTransaction
::
TYPE_INLINE
:
return
null
;
}
return
parent
::
getCustomTransactionNewValue
(
$object
,
$xaction
);
}
protected
function
transactionHasEffect
(
PhabricatorLiskDAO
$object
,
PhabricatorApplicationTransaction
$xaction
)
{
$actor_phid
=
$this
->
getActingAsPHID
();
switch
(
$xaction
->
getTransactionType
())
{
case
DifferentialTransaction
::
TYPE_INLINE
:
return
$xaction
->
hasComment
();
case
DifferentialTransaction
::
TYPE_ACTION
:
$status_closed
=
ArcanistDifferentialRevisionStatus
::
CLOSED
;
$status_abandoned
=
ArcanistDifferentialRevisionStatus
::
ABANDONED
;
$status_review
=
ArcanistDifferentialRevisionStatus
::
NEEDS_REVIEW
;
$status_revision
=
ArcanistDifferentialRevisionStatus
::
NEEDS_REVISION
;
$status_plan
=
ArcanistDifferentialRevisionStatus
::
CHANGES_PLANNED
;
$action_type
=
$xaction
->
getNewValue
();
switch
(
$action_type
)
{
case
DifferentialAction
::
ACTION_ACCEPT
:
case
DifferentialAction
::
ACTION_REJECT
:
if
(
$action_type
==
DifferentialAction
::
ACTION_ACCEPT
)
{
$new_status
=
DifferentialReviewerStatus
::
STATUS_ACCEPTED
;
}
else
{
$new_status
=
DifferentialReviewerStatus
::
STATUS_REJECTED
;
}
$actor
=
$this
->
getActor
();
// These transactions can cause effects in two ways: by altering the
// status of an existing reviewer; or by adding the actor as a new
// reviewer.
$will_add_reviewer
=
true
;
foreach
(
$object
->
getReviewerStatus
()
as
$reviewer
)
{
if
(
$reviewer
->
hasAuthority
(
$actor
))
{
if
(
$reviewer
->
getStatus
()
!=
$new_status
)
{
return
true
;
}
}
if
(
$reviewer
->
getReviewerPHID
()
==
$actor_phid
)
{
$will_add_reviwer
=
false
;
}
}
return
$will_add_reviewer
;
case
DifferentialAction
::
ACTION_CLOSE
:
return
(
$object
->
getStatus
()
!=
$status_closed
);
case
DifferentialAction
::
ACTION_ABANDON
:
return
(
$object
->
getStatus
()
!=
$status_abandoned
);
case
DifferentialAction
::
ACTION_RECLAIM
:
return
(
$object
->
getStatus
()
==
$status_abandoned
);
case
DifferentialAction
::
ACTION_REOPEN
:
return
(
$object
->
getStatus
()
==
$status_closed
);
case
DifferentialAction
::
ACTION_RETHINK
:
return
(
$object
->
getStatus
()
!=
$status_plan
);
case
DifferentialAction
::
ACTION_REQUEST
:
return
(
$object
->
getStatus
()
!=
$status_review
);
case
DifferentialAction
::
ACTION_RESIGN
:
foreach
(
$object
->
getReviewerStatus
()
as
$reviewer
)
{
if
(
$reviewer
->
getReviewerPHID
()
==
$actor_phid
)
{
return
true
;
}
}
return
false
;
case
DifferentialAction
::
ACTION_CLAIM
:
return
(
$actor_phid
!=
$object
->
getAuthorPHID
());
}
}
return
parent
::
transactionHasEffect
(
$object
,
$xaction
);
}
protected
function
applyCustomInternalTransaction
(
PhabricatorLiskDAO
$object
,
PhabricatorApplicationTransaction
$xaction
)
{
$status_review
=
ArcanistDifferentialRevisionStatus
::
NEEDS_REVIEW
;
$status_revision
=
ArcanistDifferentialRevisionStatus
::
NEEDS_REVISION
;
$status_plan
=
ArcanistDifferentialRevisionStatus
::
CHANGES_PLANNED
;
$status_abandoned
=
ArcanistDifferentialRevisionStatus
::
ABANDONED
;
switch
(
$xaction
->
getTransactionType
())
{
case
PhabricatorTransactions
::
TYPE_VIEW_POLICY
:
$object
->
setViewPolicy
(
$xaction
->
getNewValue
());
return
;
case
PhabricatorTransactions
::
TYPE_EDIT_POLICY
:
$object
->
setEditPolicy
(
$xaction
->
getNewValue
());
return
;
case
PhabricatorTransactions
::
TYPE_SUBSCRIBERS
:
case
PhabricatorTransactions
::
TYPE_COMMENT
:
case
DifferentialTransaction
::
TYPE_INLINE
:
return
;
case
PhabricatorTransactions
::
TYPE_EDGE
:
return
;
case
DifferentialTransaction
::
TYPE_UPDATE
:
if
(!
$this
->
getIsCloseByCommit
())
{
switch
(
$object
->
getStatus
())
{
case
$status_revision
:
case
$status_plan
:
case
$status_abandoned
:
$object
->
setStatus
(
$status_review
);
break
;
}
}
$diff
=
$this
->
requireDiff
(
$xaction
->
getNewValue
());
$object
->
setLineCount
(
$diff
->
getLineCount
());
if
(
$this
->
repositoryPHIDOverride
!==
false
)
{
$object
->
setRepositoryPHID
(
$this
->
repositoryPHIDOverride
);
}
else
{
$object
->
setRepositoryPHID
(
$diff
->
getRepositoryPHID
());
}
$object
->
setArcanistProjectPHID
(
$diff
->
getArcanistProjectPHID
());
$object
->
attachActiveDiff
(
$diff
);
// TODO: Update the `diffPHID` once we add that.
return
;
case
DifferentialTransaction
::
TYPE_ACTION
:
switch
(
$xaction
->
getNewValue
())
{
case
DifferentialAction
::
ACTION_RESIGN
:
case
DifferentialAction
::
ACTION_ACCEPT
:
case
DifferentialAction
::
ACTION_REJECT
:
// These have no direct effects, and affect review status only
// indirectly by altering reviewers with TYPE_EDGE transactions.
return
;
case
DifferentialAction
::
ACTION_ABANDON
:
$object
->
setStatus
(
ArcanistDifferentialRevisionStatus
::
ABANDONED
);
return
;
case
DifferentialAction
::
ACTION_RETHINK
:
$object
->
setStatus
(
$status_plan
);
return
;
case
DifferentialAction
::
ACTION_RECLAIM
:
$object
->
setStatus
(
$status_review
);
return
;
case
DifferentialAction
::
ACTION_REOPEN
:
$object
->
setStatus
(
$status_review
);
return
;
case
DifferentialAction
::
ACTION_REQUEST
:
$object
->
setStatus
(
$status_review
);
return
;
case
DifferentialAction
::
ACTION_CLOSE
:
$object
->
setStatus
(
ArcanistDifferentialRevisionStatus
::
CLOSED
);
return
;
case
DifferentialAction
::
ACTION_CLAIM
:
$object
->
setAuthorPHID
(
$this
->
getActingAsPHID
());
return
;
default
:
throw
new
Exception
(
pht
(
'Differential action "%s" is not a valid action!'
,
$xaction
->
getNewValue
()));
}
break
;
}
return
parent
::
applyCustomInternalTransaction
(
$object
,
$xaction
);
}
protected
function
expandTransaction
(
PhabricatorLiskDAO
$object
,
PhabricatorApplicationTransaction
$xaction
)
{
$results
=
parent
::
expandTransaction
(
$object
,
$xaction
);
$actor
=
$this
->
getActor
();
$actor_phid
=
$this
->
getActingAsPHID
();
$type_edge
=
PhabricatorTransactions
::
TYPE_EDGE
;
$status_plan
=
ArcanistDifferentialRevisionStatus
::
CHANGES_PLANNED
;
$edge_reviewer
=
DifferentialRevisionHasReviewerEdgeType
::
EDGECONST
;
$edge_ref_task
=
DifferentialRevisionHasTaskEdgeType
::
EDGECONST
;
$is_sticky_accept
=
PhabricatorEnv
::
getEnvConfig
(
'differential.sticky-accept'
);
$downgrade_rejects
=
false
;
$downgrade_accepts
=
false
;
if
(
$this
->
getIsCloseByCommit
())
{
// Never downgrade reviewers when we're closing a revision after a
// commit.
}
else
{
switch
(
$xaction
->
getTransactionType
())
{
case
DifferentialTransaction
::
TYPE_UPDATE
:
$downgrade_rejects
=
true
;
if
(!
$is_sticky_accept
)
{
// If "sticky accept" is disabled, also downgrade the accepts.
$downgrade_accepts
=
true
;
}
break
;
case
DifferentialTransaction
::
TYPE_ACTION
:
switch
(
$xaction
->
getNewValue
())
{
case
DifferentialAction
::
ACTION_REQUEST
:
$downgrade_rejects
=
true
;
if
((!
$is_sticky_accept
)
||
(
$object
->
getStatus
()
!=
$status_plan
))
{
// If the old state isn't "changes planned", downgrade the
// accepts. This exception allows an accepted revision to
// go through Plan Changes -> Request Review to return to
// "accepted" if the author didn't update the revision.
$downgrade_accepts
=
true
;
}
break
;
}
break
;
}
}
$new_accept
=
DifferentialReviewerStatus
::
STATUS_ACCEPTED
;
$new_reject
=
DifferentialReviewerStatus
::
STATUS_REJECTED
;
$old_accept
=
DifferentialReviewerStatus
::
STATUS_ACCEPTED_OLDER
;
$old_reject
=
DifferentialReviewerStatus
::
STATUS_REJECTED_OLDER
;
if
(
$downgrade_rejects
||
$downgrade_accepts
)
{
// When a revision is updated, change all "reject" to "rejected older
// revision". This means we won't immediately push the update back into
// "needs review", but outstanding rejects will still block it from
// moving to "accepted".
// We also do this for "Request Review", even though the diff is not
// updated directly. Essentially, this acts like an update which doesn't
// actually change the diff text.
$edits
=
array
();
foreach
(
$object
->
getReviewerStatus
()
as
$reviewer
)
{
if
(
$downgrade_rejects
)
{
if
(
$reviewer
->
getStatus
()
==
$new_reject
)
{
$edits
[
$reviewer
->
getReviewerPHID
()]
=
array
(
'data'
=>
array
(
'status'
=>
$old_reject
,
),
);
}
}
if
(
$downgrade_accepts
)
{
if
(
$reviewer
->
getStatus
()
==
$new_accept
)
{
$edits
[
$reviewer
->
getReviewerPHID
()]
=
array
(
'data'
=>
array
(
'status'
=>
$old_accept
,
),
);
}
}
}
if
(
$edits
)
{
$results
[]
=
id
(
new
DifferentialTransaction
())
->
setTransactionType
(
$type_edge
)
->
setMetadataValue
(
'edge:type'
,
$edge_reviewer
)
->
setIgnoreOnNoEffect
(
true
)
->
setNewValue
(
array
(
'+'
=>
$edits
));
}
}
switch
(
$xaction
->
getTransactionType
())
{
case
DifferentialTransaction
::
TYPE_UPDATE
:
if
(
$this
->
getIsCloseByCommit
())
{
// Don't bother with any of this if this update is a side effect of
// commit detection.
break
;
}
// When a revision is updated and the diff comes from a branch named
// "T123" or similar, automatically associate the commit with the
// task that the branch names.
$maniphest
=
'PhabricatorManiphestApplication'
;
if
(
PhabricatorApplication
::
isClassInstalled
(
$maniphest
))
{
$diff
=
$this
->
requireDiff
(
$xaction
->
getNewValue
());
$branch
=
$diff
->
getBranch
();
// No "$", to allow for branches like T123_demo.
$match
=
null
;
if
(
preg_match
(
'/^T(
\d
+)/i'
,
$branch
,
$match
))
{
$task_id
=
$match
[
1
];
$tasks
=
id
(
new
ManiphestTaskQuery
())
->
setViewer
(
$this
->
getActor
())
->
withIDs
(
array
(
$task_id
))
->
execute
();
if
(
$tasks
)
{
$task
=
head
(
$tasks
);
$task_phid
=
$task
->
getPHID
();
$results
[]
=
id
(
new
DifferentialTransaction
())
->
setTransactionType
(
$type_edge
)
->
setMetadataValue
(
'edge:type'
,
$edge_ref_task
)
->
setIgnoreOnNoEffect
(
true
)
->
setNewValue
(
array
(
'+'
=>
array
(
$task_phid
=>
$task_phid
)));
}
}
}
break
;
case
PhabricatorTransactions
::
TYPE_COMMENT
:
// When a user leaves a comment, upgrade their reviewer status from
// "added" to "commented" if they're also a reviewer. We may further
// upgrade this based on other actions in the transaction group.
$status_added
=
DifferentialReviewerStatus
::
STATUS_ADDED
;
$status_commented
=
DifferentialReviewerStatus
::
STATUS_COMMENTED
;
$data
=
array
(
'status'
=>
$status_commented
,
);
$edits
=
array
();
foreach
(
$object
->
getReviewerStatus
()
as
$reviewer
)
{
if
(
$reviewer
->
getReviewerPHID
()
==
$actor_phid
)
{
if
(
$reviewer
->
getStatus
()
==
$status_added
)
{
$edits
[
$actor_phid
]
=
array
(
'data'
=>
$data
,
);
}
}
}
if
(
$edits
)
{
$results
[]
=
id
(
new
DifferentialTransaction
())
->
setTransactionType
(
$type_edge
)
->
setMetadataValue
(
'edge:type'
,
$edge_reviewer
)
->
setIgnoreOnNoEffect
(
true
)
->
setNewValue
(
array
(
'+'
=>
$edits
));
}
break
;
case
DifferentialTransaction
::
TYPE_ACTION
:
$action_type
=
$xaction
->
getNewValue
();
switch
(
$action_type
)
{
case
DifferentialAction
::
ACTION_ACCEPT
:
case
DifferentialAction
::
ACTION_REJECT
:
if
(
$action_type
==
DifferentialAction
::
ACTION_ACCEPT
)
{
$data
=
array
(
'status'
=>
DifferentialReviewerStatus
::
STATUS_ACCEPTED
,
);
}
else
{
$data
=
array
(
'status'
=>
DifferentialReviewerStatus
::
STATUS_REJECTED
,
);
}
$edits
=
array
();
foreach
(
$object
->
getReviewerStatus
()
as
$reviewer
)
{
if
(
$reviewer
->
hasAuthority
(
$actor
))
{
$edits
[
$reviewer
->
getReviewerPHID
()]
=
array
(
'data'
=>
$data
,
);
}
}
// Also either update or add the actor themselves as a reviewer.
$edits
[
$actor_phid
]
=
array
(
'data'
=>
$data
,
);
$results
[]
=
id
(
new
DifferentialTransaction
())
->
setTransactionType
(
$type_edge
)
->
setMetadataValue
(
'edge:type'
,
$edge_reviewer
)
->
setIgnoreOnNoEffect
(
true
)
->
setNewValue
(
array
(
'+'
=>
$edits
));
break
;
case
DifferentialAction
::
ACTION_CLAIM
:
// If the user is commandeering, add the previous owner as a
// reviewer and remove the actor.
$edits
=
array
(
'-'
=>
array
(
$actor_phid
=>
$actor_phid
,
),
);
$owner_phid
=
$object
->
getAuthorPHID
();
if
(
$owner_phid
)
{
$reviewer
=
new
DifferentialReviewer
(
$owner_phid
,
array
(
'status'
=>
DifferentialReviewerStatus
::
STATUS_ADDED
,
));
$edits
[
'+'
]
=
array
(
$owner_phid
=>
array
(
'data'
=>
$reviewer
->
getEdgeData
(),
),
);
}
// NOTE: We're setting setIsCommandeerSideEffect() on this because
// normally you can't add a revision's author as a reviewer, but
// this action swaps them after validation executes.
$results
[]
=
id
(
new
DifferentialTransaction
())
->
setTransactionType
(
$type_edge
)
->
setMetadataValue
(
'edge:type'
,
$edge_reviewer
)
->
setIgnoreOnNoEffect
(
true
)
->
setIsCommandeerSideEffect
(
true
)
->
setNewValue
(
$edits
);
break
;
case
DifferentialAction
::
ACTION_RESIGN
:
// If the user is resigning, add a separate reviewer edit
// transaction which removes them as a reviewer.
$results
[]
=
id
(
new
DifferentialTransaction
())
->
setTransactionType
(
$type_edge
)
->
setMetadataValue
(
'edge:type'
,
$edge_reviewer
)
->
setIgnoreOnNoEffect
(
true
)
->
setNewValue
(
array
(
'-'
=>
array
(
$actor_phid
=>
$actor_phid
,
),
));
break
;
}
break
;
}
if
(!
$this
->
didExpandInlineState
)
{
switch
(
$xaction
->
getTransactionType
())
{
case
PhabricatorTransactions
::
TYPE_COMMENT
:
case
DifferentialTransaction
::
TYPE_ACTION
:
case
DifferentialTransaction
::
TYPE_UPDATE
:
case
DifferentialTransaction
::
TYPE_INLINE
:
$this
->
didExpandInlineState
=
true
;
$actor_phid
=
$this
->
getActingAsPHID
();
$actor_is_author
=
(
$object
->
getAuthorPHID
()
==
$actor_phid
);
if
(!
$actor_is_author
)
{
break
;
}
$state_map
=
PhabricatorTransactions
::
getInlineStateMap
();
$inlines
=
id
(
new
DifferentialDiffInlineCommentQuery
())
->
setViewer
(
$this
->
getActor
())
->
withRevisionPHIDs
(
array
(
$object
->
getPHID
()))
->
withFixedStates
(
array_keys
(
$state_map
))
->
execute
();
if
(!
$inlines
)
{
break
;
}
$old_value
=
mpull
(
$inlines
,
'getFixedState'
,
'getPHID'
);
$new_value
=
array
();
foreach
(
$old_value
as
$key
=>
$state
)
{
$new_value
[
$key
]
=
$state_map
[
$state
];
}
$results
[]
=
id
(
new
DifferentialTransaction
())
->
setTransactionType
(
PhabricatorTransactions
::
TYPE_INLINESTATE
)
->
setIgnoreOnNoEffect
(
true
)
->
setOldValue
(
$old_value
)
->
setNewValue
(
$new_value
);
break
;
}
}
return
$results
;
}
protected
function
applyCustomExternalTransaction
(
PhabricatorLiskDAO
$object
,
PhabricatorApplicationTransaction
$xaction
)
{
switch
(
$xaction
->
getTransactionType
())
{
case
PhabricatorTransactions
::
TYPE_VIEW_POLICY
:
case
PhabricatorTransactions
::
TYPE_EDIT_POLICY
:
return
;
case
PhabricatorTransactions
::
TYPE_SUBSCRIBERS
:
case
PhabricatorTransactions
::
TYPE_EDGE
:
case
PhabricatorTransactions
::
TYPE_COMMENT
:
case
DifferentialTransaction
::
TYPE_ACTION
:
return
;
case
DifferentialTransaction
::
TYPE_INLINE
:
$reply
=
$xaction
->
getComment
()->
getReplyToComment
();
if
(
$reply
&&
!
$reply
->
getHasReplies
())
{
$reply
->
setHasReplies
(
1
)->
save
();
}
return
;
case
DifferentialTransaction
::
TYPE_UPDATE
:
// Now that we're inside the transaction, do a final check.
$diff
=
$this
->
requireDiff
(
$xaction
->
getNewValue
());
// TODO: It would be slightly cleaner to just revalidate this
// transaction somehow using the same validation code, but that's
// not easy to do at the moment.
$revision_id
=
$diff
->
getRevisionID
();
if
(
$revision_id
&&
(
$revision_id
!=
$object
->
getID
()))
{
throw
new
Exception
(
pht
(
'Diff is already attached to another revision. You lost '
.
'a race?'
));
}
$diff
->
setRevisionID
(
$object
->
getID
());
$diff
->
save
();
return
;
}
return
parent
::
applyCustomExternalTransaction
(
$object
,
$xaction
);
}
protected
function
applyBuiltinExternalTransaction
(
PhabricatorLiskDAO
$object
,
PhabricatorApplicationTransaction
$xaction
)
{
switch
(
$xaction
->
getTransactionType
())
{
case
PhabricatorTransactions
::
TYPE_INLINESTATE
:
$table
=
new
DifferentialTransactionComment
();
$conn_w
=
$table
->
establishConnection
(
'w'
);
foreach
(
$xaction
->
getNewValue
()
as
$phid
=>
$state
)
{
queryfx
(
$conn_w
,
'UPDATE %T SET fixedState = %s WHERE phid = %s'
,
$table
->
getTableName
(),
$state
,
$phid
);
}
return
;
}
return
parent
::
applyBuiltinExternalTransaction
(
$object
,
$xaction
);
}
protected
function
mergeEdgeData
(
$type
,
array
$u
,
array
$v
)
{
$result
=
parent
::
mergeEdgeData
(
$type
,
$u
,
$v
);
switch
(
$type
)
{
case
DifferentialRevisionHasReviewerEdgeType
::
EDGECONST
:
// When the same reviewer has their status updated by multiple
// transactions, we want the strongest status to win. An example of
// this is when a user adds a comment and also accepts a revision which
// they are a reviewer on. The comment creates a "commented" status,
// while the accept creates an "accepted" status. Since accept is
// stronger, it should win and persist.
$u_status
=
idx
(
$u
,
'status'
);
$v_status
=
idx
(
$v
,
'status'
);
$u_str
=
DifferentialReviewerStatus
::
getStatusStrength
(
$u_status
);
$v_str
=
DifferentialReviewerStatus
::
getStatusStrength
(
$v_status
);
if
(
$u_str
>
$v_str
)
{
$result
[
'status'
]
=
$u_status
;
}
else
{
$result
[
'status'
]
=
$v_status
;
}
break
;
}
return
$result
;
}
protected
function
applyFinalEffects
(
PhabricatorLiskDAO
$object
,
array
$xactions
)
{
// Load the most up-to-date version of the revision and its reviewers,
// so we don't need to try to deduce the state of reviewers by examining
// all the changes made by the transactions. Then, update the reviewers
// on the object to make sure we're acting on the current reviewer set
// (and, for example, sending mail to the right people).
$new_revision
=
id
(
new
DifferentialRevisionQuery
())
->
setViewer
(
$this
->
getActor
())
->
needReviewerStatus
(
true
)
->
needActiveDiffs
(
true
)
->
withIDs
(
array
(
$object
->
getID
()))
->
executeOne
();
if
(!
$new_revision
)
{
throw
new
Exception
(
pht
(
'Failed to load revision from transaction finalization.'
));
}
$object
->
attachReviewerStatus
(
$new_revision
->
getReviewerStatus
());
$object
->
attachActiveDiff
(
$new_revision
->
getActiveDiff
());
$object
->
attachRepository
(
$new_revision
->
getRepository
());
foreach
(
$xactions
as
$xaction
)
{
switch
(
$xaction
->
getTransactionType
())
{
case
DifferentialTransaction
::
TYPE_UPDATE
:
$diff
=
$this
->
requireDiff
(
$xaction
->
getNewValue
(),
true
);
// Update these denormalized index tables when we attach a new
// diff to a revision.
$this
->
updateRevisionHashTable
(
$object
,
$diff
);
$this
->
updateAffectedPathTable
(
$object
,
$diff
);
break
;
}
}
$status_accepted
=
ArcanistDifferentialRevisionStatus
::
ACCEPTED
;
$status_revision
=
ArcanistDifferentialRevisionStatus
::
NEEDS_REVISION
;
$status_review
=
ArcanistDifferentialRevisionStatus
::
NEEDS_REVIEW
;
$old_status
=
$object
->
getStatus
();
switch
(
$old_status
)
{
case
$status_accepted
:
case
$status_revision
:
case
$status_review
:
// Try to move a revision to "accepted". We look for:
//
// - at least one accepting reviewer who is a user; and
// - no rejects; and
// - no rejects of older diffs; and
// - no blocking reviewers.
$has_accepting_user
=
false
;
$has_rejecting_reviewer
=
false
;
$has_rejecting_older_reviewer
=
false
;
$has_blocking_reviewer
=
false
;
foreach
(
$object
->
getReviewerStatus
()
as
$reviewer
)
{
$reviewer_status
=
$reviewer
->
getStatus
();
switch
(
$reviewer_status
)
{
case
DifferentialReviewerStatus
::
STATUS_REJECTED
:
$has_rejecting_reviewer
=
true
;
break
;
case
DifferentialReviewerStatus
::
STATUS_REJECTED_OLDER
:
$has_rejecting_older_reviewer
=
true
;
break
;
case
DifferentialReviewerStatus
::
STATUS_BLOCKING
:
$has_blocking_reviewer
=
true
;
break
;
case
DifferentialReviewerStatus
::
STATUS_ACCEPTED
:
if
(
$reviewer
->
isUser
())
{
$has_accepting_user
=
true
;
}
break
;
}
}
$new_status
=
null
;
if
(
$has_accepting_user
&&
!
$has_rejecting_reviewer
&&
!
$has_rejecting_older_reviewer
&&
!
$has_blocking_reviewer
)
{
$new_status
=
$status_accepted
;
}
else
if
(
$has_rejecting_reviewer
)
{
// This isn't accepted, and there's at least one rejecting reviewer,
// so the revision needs changes. This usually happens after a
// "reject".
$new_status
=
$status_revision
;
}
else
if
(
$old_status
==
$status_accepted
)
{
// This revision was accepted, but it no longer satisfies the
// conditions for acceptance. This usually happens after an accepting
// reviewer resigns or is removed.
$new_status
=
$status_review
;
}
if
(
$new_status
!==
null
&&
(
$new_status
!=
$old_status
))
{
$xaction
=
id
(
new
DifferentialTransaction
())
->
setTransactionType
(
DifferentialTransaction
::
TYPE_STATUS
)
->
setOldValue
(
$old_status
)
->
setNewValue
(
$new_status
);
$xaction
=
$this
->
populateTransaction
(
$object
,
$xaction
)->
save
();
$xactions
[]
=
$xaction
;
$object
->
setStatus
(
$new_status
)->
save
();
}
break
;
default
:
// Revisions can't transition out of other statuses (like closed or
// abandoned) as a side effect of reviewer status changes.
break
;
}
return
$xactions
;
}
protected
function
validateTransaction
(
PhabricatorLiskDAO
$object
,
$type
,
array
$xactions
)
{
$errors
=
parent
::
validateTransaction
(
$object
,
$type
,
$xactions
);
$config_self_accept_key
=
'differential.allow-self-accept'
;
$allow_self_accept
=
PhabricatorEnv
::
getEnvConfig
(
$config_self_accept_key
);
foreach
(
$xactions
as
$xaction
)
{
switch
(
$type
)
{
case
PhabricatorTransactions
::
TYPE_EDGE
:
switch
(
$xaction
->
getMetadataValue
(
'edge:type'
))
{
case
DifferentialRevisionHasReviewerEdgeType
::
EDGECONST
:
// Prevent the author from becoming a reviewer.
// NOTE: This is pretty gross, but this restriction is unusual.
// If we end up with too much more of this, we should try to clean
// this up -- maybe by moving validation to after transactions
// are adjusted (so we can just examine the final value) or adding
// a second phase there?
$author_phid
=
$object
->
getAuthorPHID
();
$new
=
$xaction
->
getNewValue
();
$add
=
idx
(
$new
,
'+'
,
array
());
$eq
=
idx
(
$new
,
'='
,
array
());
$phids
=
array_keys
(
$add
+
$eq
);
foreach
(
$phids
as
$phid
)
{
if
((
$phid
==
$author_phid
)
&&
!
$allow_self_accept
&&
!
$xaction
->
getIsCommandeerSideEffect
())
{
$errors
[]
=
new
PhabricatorApplicationTransactionValidationError
(
$type
,
pht
(
'Invalid'
),
pht
(
'The author of a revision can not be a reviewer.'
),
$xaction
);
}
}
break
;
}
break
;
case
DifferentialTransaction
::
TYPE_UPDATE
:
$diff
=
$this
->
loadDiff
(
$xaction
->
getNewValue
());
if
(!
$diff
)
{
$errors
[]
=
new
PhabricatorApplicationTransactionValidationError
(
$type
,
pht
(
'Invalid'
),
pht
(
'The specified diff does not exist.'
),
$xaction
);
}
else
if
((
$diff
->
getRevisionID
())
&&
(
$diff
->
getRevisionID
()
!=
$object
->
getID
()))
{
$errors
[]
=
new
PhabricatorApplicationTransactionValidationError
(
$type
,
pht
(
'Invalid'
),
pht
(
'You can not update this revision to the specified diff, '
.
'because the diff is already attached to another revision.'
),
$xaction
);
}
break
;
case
DifferentialTransaction
::
TYPE_ACTION
:
$error
=
$this
->
validateDifferentialAction
(
$object
,
$type
,
$xaction
,
$xaction
->
getNewValue
());
if
(
$error
)
{
$errors
[]
=
new
PhabricatorApplicationTransactionValidationError
(
$type
,
pht
(
'Invalid'
),
$error
,
$xaction
);
}
break
;
}
}
return
$errors
;
}
private
function
validateDifferentialAction
(
DifferentialRevision
$revision
,
$type
,
DifferentialTransaction
$xaction
,
$action
)
{
$author_phid
=
$revision
->
getAuthorPHID
();
$actor_phid
=
$this
->
getActingAsPHID
();
$actor_is_author
=
(
$author_phid
==
$actor_phid
);
$config_abandon_key
=
'differential.always-allow-abandon'
;
$always_allow_abandon
=
PhabricatorEnv
::
getEnvConfig
(
$config_abandon_key
);
$config_close_key
=
'differential.always-allow-close'
;
$always_allow_close
=
PhabricatorEnv
::
getEnvConfig
(
$config_close_key
);
$config_reopen_key
=
'differential.allow-reopen'
;
$allow_reopen
=
PhabricatorEnv
::
getEnvConfig
(
$config_reopen_key
);
$config_self_accept_key
=
'differential.allow-self-accept'
;
$allow_self_accept
=
PhabricatorEnv
::
getEnvConfig
(
$config_self_accept_key
);
$revision_status
=
$revision
->
getStatus
();
$status_accepted
=
ArcanistDifferentialRevisionStatus
::
ACCEPTED
;
$status_abandoned
=
ArcanistDifferentialRevisionStatus
::
ABANDONED
;
$status_closed
=
ArcanistDifferentialRevisionStatus
::
CLOSED
;
switch
(
$action
)
{
case
DifferentialAction
::
ACTION_ACCEPT
:
if
(
$actor_is_author
&&
!
$allow_self_accept
)
{
return
pht
(
'You can not accept this revision because you are the owner.'
);
}
if
(
$revision_status
==
$status_abandoned
)
{
return
pht
(
'You can not accept this revision because it has been '
.
'abandoned.'
);
}
if
(
$revision_status
==
$status_closed
)
{
return
pht
(
'You can not accept this revision because it has already been '
.
'closed.'
);
}
// TODO: It would be nice to make this generic at some point.
$signatures
=
DifferentialRequiredSignaturesField
::
loadForRevision
(
$revision
);
foreach
(
$signatures
as
$phid
=>
$signed
)
{
if
(!
$signed
)
{
return
pht
(
'You can not accept this revision because the author has '
.
'not signed all of the required legal documents.'
);
}
}
break
;
case
DifferentialAction
::
ACTION_REJECT
:
if
(
$actor_is_author
)
{
return
pht
(
'You can not request changes to your own revision.'
);
}
if
(
$revision_status
==
$status_abandoned
)
{
return
pht
(
'You can not request changes to this revision because it has been '
.
'abandoned.'
);
}
if
(
$revision_status
==
$status_closed
)
{
return
pht
(
'You can not request changes to this revision because it has '
.
'already been closed.'
);
}
break
;
case
DifferentialAction
::
ACTION_RESIGN
:
// You can always resign from a revision if you're a reviewer. If you
// aren't, this is a no-op rather than invalid.
break
;
case
DifferentialAction
::
ACTION_CLAIM
:
// You can claim a revision if you're not the owner. If you are, this
// is a no-op rather than invalid.
if
(
$revision_status
==
$status_closed
)
{
return
pht
(
'You can not commandeer this revision because it has already been '
.
'closed.'
);
}
break
;
case
DifferentialAction
::
ACTION_ABANDON
:
if
(!
$actor_is_author
&&
!
$always_allow_abandon
)
{
return
pht
(
'You can not abandon this revision because you do not own it. '
.
'You can only abandon revisions you own.'
);
}
if
(
$revision_status
==
$status_closed
)
{
return
pht
(
'You can not abandon this revision because it has already been '
.
'closed.'
);
}
// NOTE: Abandons of already-abandoned revisions are treated as no-op
// instead of invalid. Other abandons are OK.
break
;
case
DifferentialAction
::
ACTION_RECLAIM
:
if
(!
$actor_is_author
)
{
return
pht
(
'You can not reclaim this revision because you do not own '
.
'it. You can only reclaim revisions you own.'
);
}
if
(
$revision_status
==
$status_closed
)
{
return
pht
(
'You can not reclaim this revision because it has already been '
.
'closed.'
);
}
// NOTE: Reclaims of other non-abandoned revisions are treated as no-op
// instead of invalid.
break
;
case
DifferentialAction
::
ACTION_REOPEN
:
if
(!
$allow_reopen
)
{
return
pht
(
'The reopen action is not enabled on this Phabricator install. '
.
'Adjust your configuration to enable it.'
);
}
// NOTE: If the revision is not closed, this is caught as a no-op
// instead of an invalid transaction.
break
;
case
DifferentialAction
::
ACTION_RETHINK
:
if
(!
$actor_is_author
)
{
return
pht
(
'You can not plan changes to this revision because you do not '
.
'own it. To plan changes to a revision, you must be its owner.'
);
}
switch
(
$revision_status
)
{
case
ArcanistDifferentialRevisionStatus
::
ACCEPTED
:
case
ArcanistDifferentialRevisionStatus
::
NEEDS_REVISION
:
case
ArcanistDifferentialRevisionStatus
::
NEEDS_REVIEW
:
// These are OK.
break
;
case
ArcanistDifferentialRevisionStatus
::
CHANGES_PLANNED
:
// Let this through, it's a no-op.
break
;
case
ArcanistDifferentialRevisionStatus
::
ABANDONED
:
return
pht
(
'You can not plan changes to this revision because it has '
.
'been abandoned.'
);
case
ArcanistDifferentialRevisionStatus
::
CLOSED
:
return
pht
(
'You can not plan changes to this revision because it has '
.
'already been closed.'
);
default
:
throw
new
Exception
(
pht
(
'Encountered unexpected revision status ("%s") when '
.
'validating "%s" action.'
,
$revision_status
,
$action
));
}
break
;
case
DifferentialAction
::
ACTION_REQUEST
:
if
(!
$actor_is_author
)
{
return
pht
(
'You can not request review of this revision because you do '
.
'not own it. To request review of a revision, you must be its '
.
'owner.'
);
}
switch
(
$revision_status
)
{
case
ArcanistDifferentialRevisionStatus
::
ACCEPTED
:
case
ArcanistDifferentialRevisionStatus
::
NEEDS_REVISION
:
case
ArcanistDifferentialRevisionStatus
::
CHANGES_PLANNED
:
// These are OK.
break
;
case
ArcanistDifferentialRevisionStatus
::
NEEDS_REVIEW
:
// This will be caught as "no effect" later on.
break
;
case
ArcanistDifferentialRevisionStatus
::
ABANDONED
:
return
pht
(
'You can not request review of this revision because it has '
.
'been abandoned. Instead, reclaim it.'
);
case
ArcanistDifferentialRevisionStatus
::
CLOSED
:
return
pht
(
'You can not request review of this revision because it has '
.
'already been closed.'
);
default
:
throw
new
Exception
(
pht
(
'Encountered unexpected revision status ("%s") when '
.
'validating "%s" action.'
,
$revision_status
,
$action
));
}
break
;
case
DifferentialAction
::
ACTION_CLOSE
:
// We force revisions closed when we discover a corresponding commit.
// In this case, revisions are allowed to transition to closed from
// any state. This is an automated action taken by the daemons.
if
(!
$this
->
getIsCloseByCommit
())
{
if
(!
$actor_is_author
&&
!
$always_allow_close
)
{
return
pht
(
'You can not close this revision because you do not own it. To '
.
'close a revision, you must be its owner.'
);
}
if
(
$revision_status
!=
$status_accepted
)
{
return
pht
(
'You can not close this revision because it has not been '
.
'accepted. You can only close accepted revisions.'
);
}
}
break
;
}
return
null
;
}
protected
function
sortTransactions
(
array
$xactions
)
{
$xactions
=
parent
::
sortTransactions
(
$xactions
);
$head
=
array
();
$tail
=
array
();
foreach
(
$xactions
as
$xaction
)
{
$type
=
$xaction
->
getTransactionType
();
if
(
$type
==
DifferentialTransaction
::
TYPE_INLINE
)
{
$tail
[]
=
$xaction
;
}
else
{
$head
[]
=
$xaction
;
}
}
return
array_values
(
array_merge
(
$head
,
$tail
));
}
protected
function
requireCapabilities
(
PhabricatorLiskDAO
$object
,
PhabricatorApplicationTransaction
$xaction
)
{
switch
(
$xaction
->
getTransactionType
())
{}
return
parent
::
requireCapabilities
(
$object
,
$xaction
);
}
protected
function
shouldPublishFeedStory
(
PhabricatorLiskDAO
$object
,
array
$xactions
)
{
return
true
;
}
protected
function
shouldSendMail
(
PhabricatorLiskDAO
$object
,
array
$xactions
)
{
return
true
;
}
protected
function
getMailTo
(
PhabricatorLiskDAO
$object
)
{
$phids
=
array
();
$phids
[]
=
$object
->
getAuthorPHID
();
foreach
(
$object
->
getReviewerStatus
()
as
$reviewer
)
{
$phids
[]
=
$reviewer
->
getReviewerPHID
();
}
return
$phids
;
}
protected
function
getMailCC
(
PhabricatorLiskDAO
$object
)
{
$phids
=
parent
::
getMailCC
(
$object
);
if
(
$this
->
heraldEmailPHIDs
)
{
foreach
(
$this
->
heraldEmailPHIDs
as
$phid
)
{
$phids
[]
=
$phid
;
}
}
return
$phids
;
}
protected
function
getMailAction
(
PhabricatorLiskDAO
$object
,
array
$xactions
)
{
$action
=
parent
::
getMailAction
(
$object
,
$xactions
);
$strongest
=
$this
->
getStrongestAction
(
$object
,
$xactions
);
switch
(
$strongest
->
getTransactionType
())
{
case
DifferentialTransaction
::
TYPE_UPDATE
:
$count
=
new
PhutilNumber
(
$object
->
getLineCount
());
$action
=
pht
(
'%s, %s line(s)'
,
$action
,
$count
);
break
;
}
return
$action
;
}
protected
function
getMailSubjectPrefix
()
{
return
PhabricatorEnv
::
getEnvConfig
(
'metamta.differential.subject-prefix'
);
}
protected
function
getMailThreadID
(
PhabricatorLiskDAO
$object
)
{
// This is nonstandard, but retains threading with older messages.
$phid
=
$object
->
getPHID
();
return
"differential-rev-{$phid}-req"
;
}
protected
function
buildReplyHandler
(
PhabricatorLiskDAO
$object
)
{
return
id
(
new
DifferentialReplyHandler
())
->
setMailReceiver
(
$object
);
}
protected
function
buildMailTemplate
(
PhabricatorLiskDAO
$object
)
{
$id
=
$object
->
getID
();
$title
=
$object
->
getTitle
();
$original_title
=
$object
->
getOriginalTitle
();
$subject
=
"D{$id}: {$title}"
;
$thread_topic
=
"D{$id}: {$original_title}"
;
return
id
(
new
PhabricatorMetaMTAMail
())
->
setSubject
(
$subject
)
->
addHeader
(
'Thread-Topic'
,
$thread_topic
);
}
protected
function
buildMailBody
(
PhabricatorLiskDAO
$object
,
array
$xactions
)
{
$body
=
new
PhabricatorMetaMTAMailBody
();
$body
->
setViewer
(
$this
->
requireActor
());
$this
->
addHeadersAndCommentsToMailBody
(
$body
,
$xactions
);
$type_inline
=
DifferentialTransaction
::
TYPE_INLINE
;
$inlines
=
array
();
foreach
(
$xactions
as
$xaction
)
{
if
(
$xaction
->
getTransactionType
()
==
$type_inline
)
{
$inlines
[]
=
$xaction
;
}
}
if
(
$inlines
)
{
$body
->
addTextSection
(
pht
(
'INLINE COMMENTS'
),
$this
->
renderInlineCommentsForMail
(
$object
,
$inlines
));
}
$changed_uri
=
$this
->
getChangedPriorToCommitURI
();
if
(
$changed_uri
)
{
$body
->
addLinkSection
(
pht
(
'CHANGED PRIOR TO COMMIT'
),
$changed_uri
);
}
$this
->
addCustomFieldsToMailBody
(
$body
,
$object
,
$xactions
);
$body
->
addLinkSection
(
pht
(
'REVISION DETAIL'
),
PhabricatorEnv
::
getProductionURI
(
'/D'
.
$object
->
getID
()));
$update_xaction
=
null
;
foreach
(
$xactions
as
$xaction
)
{
switch
(
$xaction
->
getTransactionType
())
{
case
DifferentialTransaction
::
TYPE_UPDATE
:
$update_xaction
=
$xaction
;
break
;
}
}
if
(
$update_xaction
)
{
$diff
=
$this
->
requireDiff
(
$update_xaction
->
getNewValue
(),
true
);
$body
->
addTextSection
(
pht
(
'AFFECTED FILES'
),
$this
->
renderAffectedFilesForMail
(
$diff
));
$config_key_inline
=
'metamta.differential.inline-patches'
;
$config_inline
=
PhabricatorEnv
::
getEnvConfig
(
$config_key_inline
);
$config_key_attach
=
'metamta.differential.attach-patches'
;
$config_attach
=
PhabricatorEnv
::
getEnvConfig
(
$config_key_attach
);
if
(
$config_inline
||
$config_attach
)
{
$patch_section
=
$this
->
renderPatchForMail
(
$diff
);
$lines
=
count
(
phutil_split_lines
(
$patch_section
->
getPlaintext
()));
if
(
$config_inline
&&
(
$lines
<=
$config_inline
))
{
$body
->
addTextSection
(
pht
(
'CHANGE DETAILS'
),
$patch_section
);
}
if
(
$config_attach
)
{
$name
=
pht
(
'D%s.%s.patch'
,
$object
->
getID
(),
$diff
->
getID
());
$mime_type
=
'text/x-patch; charset=utf-8'
;
$body
->
addAttachment
(
new
PhabricatorMetaMTAAttachment
(
$patch_section
->
getPlaintext
(),
$name
,
$mime_type
));
}
}
}
return
$body
;
}
public
function
getMailTagsMap
()
{
return
array
(
MetaMTANotificationType
::
TYPE_DIFFERENTIAL_REVIEW_REQUEST
=>
pht
(
'A revision is created.'
),
MetaMTANotificationType
::
TYPE_DIFFERENTIAL_UPDATED
=>
pht
(
'A revision is updated.'
),
MetaMTANotificationType
::
TYPE_DIFFERENTIAL_COMMENT
=>
pht
(
'Someone comments on a revision.'
),
MetaMTANotificationType
::
TYPE_DIFFERENTIAL_CLOSED
=>
pht
(
'A revision is closed.'
),
MetaMTANotificationType
::
TYPE_DIFFERENTIAL_REVIEWERS
=>
pht
(
"A revision's reviewers change."
),
MetaMTANotificationType
::
TYPE_DIFFERENTIAL_CC
=>
pht
(
"A revision's CCs change."
),
MetaMTANotificationType
::
TYPE_DIFFERENTIAL_OTHER
=>
pht
(
'Other revision activity not listed above occurs.'
),
);
}
protected
function
supportsSearch
()
{
return
true
;
}
protected
function
extractFilePHIDsFromCustomTransaction
(
PhabricatorLiskDAO
$object
,
PhabricatorApplicationTransaction
$xaction
)
{
switch
(
$xaction
->
getTransactionType
())
{}
return
parent
::
extractFilePHIDsFromCustomTransaction
(
$object
,
$xaction
);
}
protected
function
expandCustomRemarkupBlockTransactions
(
PhabricatorLiskDAO
$object
,
array
$xactions
,
$blocks
,
PhutilMarkupEngine
$engine
)
{
$flat_blocks
=
array_mergev
(
$blocks
);
$huge_block
=
implode
(
"
\n\n
"
,
$flat_blocks
);
$task_map
=
array
();
$task_refs
=
id
(
new
ManiphestCustomFieldStatusParser
())
->
parseCorpus
(
$huge_block
);
foreach
(
$task_refs
as
$match
)
{
foreach
(
$match
[
'monograms'
]
as
$monogram
)
{
$task_id
=
(
int
)
trim
(
$monogram
,
'tT'
);
$task_map
[
$task_id
]
=
true
;
}
}
$rev_map
=
array
();
$rev_refs
=
id
(
new
DifferentialCustomFieldDependsOnParser
())
->
parseCorpus
(
$huge_block
);
foreach
(
$rev_refs
as
$match
)
{
foreach
(
$match
[
'monograms'
]
as
$monogram
)
{
$rev_id
=
(
int
)
trim
(
$monogram
,
'dD'
);
$rev_map
[
$rev_id
]
=
true
;
}
}
$edges
=
array
();
$task_phids
=
array
();
$rev_phids
=
array
();
if
(
$task_map
)
{
$tasks
=
id
(
new
ManiphestTaskQuery
())
->
setViewer
(
$this
->
getActor
())
->
withIDs
(
array_keys
(
$task_map
))
->
execute
();
if
(
$tasks
)
{
$task_phids
=
mpull
(
$tasks
,
'getPHID'
,
'getPHID'
);
$edge_related
=
DifferentialRevisionHasTaskEdgeType
::
EDGECONST
;
$edges
[
$edge_related
]
=
$task_phids
;
}
}
if
(
$rev_map
)
{
$revs
=
id
(
new
DifferentialRevisionQuery
())
->
setViewer
(
$this
->
getActor
())
->
withIDs
(
array_keys
(
$rev_map
))
->
execute
();
$rev_phids
=
mpull
(
$revs
,
'getPHID'
,
'getPHID'
);
// NOTE: Skip any write attempts if a user cleverly implies a revision
// depends upon itself.
unset
(
$rev_phids
[
$object
->
getPHID
()]);
if
(
$revs
)
{
$depends
=
DifferentialRevisionDependsOnRevisionEdgeType
::
EDGECONST
;
$edges
[
$depends
]
=
$rev_phids
;
}
}
$this
->
setUnmentionablePHIDMap
(
array_merge
(
$task_phids
,
$rev_phids
));
$result
=
array
();
foreach
(
$edges
as
$type
=>
$specs
)
{
$result
[]
=
id
(
new
DifferentialTransaction
())
->
setTransactionType
(
PhabricatorTransactions
::
TYPE_EDGE
)
->
setMetadataValue
(
'edge:type'
,
$type
)
->
setNewValue
(
array
(
'+'
=>
$specs
));
}
return
$result
;
}
protected
function
indentForMail
(
array
$lines
)
{
$indented
=
array
();
foreach
(
$lines
as
$line
)
{
$indented
[]
=
'> '
.
$line
;
}
return
$indented
;
}
protected
function
nestCommentHistory
(
DifferentialTransactionComment
$comment
,
array
$comments_by_line_number
,
array
$users_by_phid
)
{
$nested
=
array
();
$previous_comments
=
$comments_by_line_number
[
$comment
->
getChangesetID
()]
[
$comment
->
getLineNumber
()];
foreach
(
$previous_comments
as
$previous_comment
)
{
if
(
$previous_comment
->
getID
()
>=
$comment
->
getID
())
{
break
;
}
$nested
=
$this
->
indentForMail
(
array_merge
(
$nested
,
explode
(
"
\n
"
,
$previous_comment
->
getContent
())));
$user
=
idx
(
$users_by_phid
,
$previous_comment
->
getAuthorPHID
(),
null
);
if
(
$user
)
{
array_unshift
(
$nested
,
pht
(
'%s wrote:'
,
$user
->
getUserName
()));
}
}
$nested
=
array_merge
(
$nested
,
explode
(
"
\n
"
,
$comment
->
getContent
()));
return
implode
(
"
\n
"
,
$nested
);
}
private
function
renderInlineCommentsForMail
(
PhabricatorLiskDAO
$object
,
array
$inlines
)
{
$context_key
=
'metamta.differential.unified-comment-context'
;
$show_context
=
PhabricatorEnv
::
getEnvConfig
(
$context_key
);
$changeset_ids
=
array
();
$line_numbers_by_changeset
=
array
();
foreach
(
$inlines
as
$inline
)
{
$id
=
$inline
->
getComment
()->
getChangesetID
();
$changeset_ids
[
$id
]
=
$id
;
$line_numbers_by_changeset
[
$id
][]
=
$inline
->
getComment
()->
getLineNumber
();
}
$changesets
=
id
(
new
DifferentialChangesetQuery
())
->
setViewer
(
$this
->
getActor
())
->
withIDs
(
$changeset_ids
)
->
needHunks
(
true
)
->
execute
();
$inline_groups
=
DifferentialTransactionComment
::
sortAndGroupInlines
(
$inlines
,
$changesets
);
if
(
$show_context
)
{
$hunk_parser
=
new
DifferentialHunkParser
();
$table
=
new
DifferentialTransactionComment
();
$conn_r
=
$table
->
establishConnection
(
'r'
);
$queries
=
array
();
foreach
(
$line_numbers_by_changeset
as
$id
=>
$line_numbers
)
{
$queries
[]
=
qsprintf
(
$conn_r
,
'(changesetID = %d AND lineNumber IN (%Ld))'
,
$id
,
$line_numbers
);
}
$all_comments
=
id
(
new
DifferentialTransactionComment
())->
loadAllWhere
(
'transactionPHID IS NOT NULL AND (%Q)'
,
implode
(
' OR '
,
$queries
));
$comments_by_line_number
=
array
();
foreach
(
$all_comments
as
$comment
)
{
$comments_by_line_number
[
$comment
->
getChangesetID
()]
[
$comment
->
getLineNumber
()]
[
$comment
->
getID
()]
=
$comment
;
}
$author_phids
=
mpull
(
$all_comments
,
'getAuthorPHID'
);
$authors
=
id
(
new
PhabricatorPeopleQuery
())
->
setViewer
(
$this
->
getActor
())
->
withPHIDs
(
$author_phids
)
->
execute
();
$authors_by_phid
=
mpull
(
$authors
,
null
,
'getPHID'
);
}
$section
=
new
PhabricatorMetaMTAMailSection
();
foreach
(
$inline_groups
as
$changeset_id
=>
$group
)
{
$changeset
=
idx
(
$changesets
,
$changeset_id
);
if
(!
$changeset
)
{
continue
;
}
foreach
(
$group
as
$inline
)
{
$comment
=
$inline
->
getComment
();
$file
=
$changeset
->
getFilename
();
$start
=
$comment
->
getLineNumber
();
$len
=
$comment
->
getLineLength
();
if
(
$len
)
{
$range
=
$start
.
'-'
.(
$start
+
$len
);
}
else
{
$range
=
$start
;
}
$inline_content
=
$comment
->
getContent
();
if
(!
$show_context
)
{
$section
->
addFragment
(
"{$file}:{$range} {$inline_content}"
);
}
else
{
$patch
=
$hunk_parser
->
makeContextDiff
(
$changeset
->
getHunks
(),
$comment
->
getIsNewFile
(),
$comment
->
getLineNumber
(),
$comment
->
getLineLength
(),
1
);
$nested_comments
=
$this
->
nestCommentHistory
(
$inline
->
getComment
(),
$comments_by_line_number
,
$authors_by_phid
);
$section
->
addFragment
(
'================'
)
->
addFragment
(
'Comment at: '
.
$file
.
':'
.
$range
)
->
addPlaintextFragment
(
$patch
)
->
addHTMLFragment
(
$this
->
renderPatchHTMLForMail
(
$patch
))
->
addFragment
(
'----------------'
)
->
addFragment
(
$nested_comments
)
->
addFragment
(
null
);
}
}
}
return
$section
;
}
private
function
loadDiff
(
$phid
,
$need_changesets
=
false
)
{
$query
=
id
(
new
DifferentialDiffQuery
())
->
withPHIDs
(
array
(
$phid
))
->
setViewer
(
$this
->
getActor
());
if
(
$need_changesets
)
{
$query
->
needChangesets
(
true
);
}
return
$query
->
executeOne
();
}
private
function
requireDiff
(
$phid
,
$need_changesets
=
false
)
{
$diff
=
$this
->
loadDiff
(
$phid
,
$need_changesets
);
if
(!
$diff
)
{
throw
new
Exception
(
pht
(
'Diff "%s" does not exist!'
,
$phid
));
}
return
$diff
;
}
/* -( Herald Integration )------------------------------------------------- */
protected
function
shouldApplyHeraldRules
(
PhabricatorLiskDAO
$object
,
array
$xactions
)
{
if
(
$this
->
getIsNewObject
())
{
return
true
;
}
foreach
(
$xactions
as
$xaction
)
{
switch
(
$xaction
->
getTransactionType
())
{
case
DifferentialTransaction
::
TYPE_UPDATE
:
if
(!
$this
->
getIsCloseByCommit
())
{
return
true
;
}
break
;
case
DifferentialTransaction
::
TYPE_ACTION
:
switch
(
$xaction
->
getNewValue
())
{
case
DifferentialAction
::
ACTION_CLAIM
:
// When users commandeer revisions, we may need to trigger
// signatures or author-based rules.
return
true
;
}
break
;
}
}
return
parent
::
shouldApplyHeraldRules
(
$object
,
$xactions
);
}
protected
function
buildHeraldAdapter
(
PhabricatorLiskDAO
$object
,
array
$xactions
)
{
$unsubscribed_phids
=
PhabricatorEdgeQuery
::
loadDestinationPHIDs
(
$object
->
getPHID
(),
PhabricatorObjectHasUnsubscriberEdgeType
::
EDGECONST
);
$subscribed_phids
=
PhabricatorSubscribersQuery
::
loadSubscribersForPHID
(
$object
->
getPHID
());
$revision
=
id
(
new
DifferentialRevisionQuery
())
->
setViewer
(
$this
->
getActor
())
->
withPHIDs
(
array
(
$object
->
getPHID
()))
->
needActiveDiffs
(
true
)
->
needReviewerStatus
(
true
)
->
executeOne
();
if
(!
$revision
)
{
throw
new
Exception
(
pht
(
'Failed to load revision for Herald adapter construction!'
));
}
$adapter
=
HeraldDifferentialRevisionAdapter
::
newLegacyAdapter
(
$revision
,
$revision
->
getActiveDiff
());
$reviewers
=
$revision
->
getReviewerStatus
();
$reviewer_phids
=
mpull
(
$reviewers
,
'getReviewerPHID'
);
$adapter
->
setExplicitCCs
(
$subscribed_phids
);
$adapter
->
setExplicitReviewers
(
$reviewer_phids
);
$adapter
->
setForbiddenCCs
(
$unsubscribed_phids
);
return
$adapter
;
}
protected
function
didApplyHeraldRules
(
PhabricatorLiskDAO
$object
,
HeraldAdapter
$adapter
,
HeraldTranscript
$transcript
)
{
$xactions
=
array
();
// Build a transaction to adjust CCs.
$ccs
=
array
(
'+'
=>
array_keys
(
$adapter
->
getCCsAddedByHerald
()),
'-'
=>
array_keys
(
$adapter
->
getCCsRemovedByHerald
()),
);
$value
=
array
();
foreach
(
$ccs
as
$type
=>
$phids
)
{
foreach
(
$phids
as
$phid
)
{
$value
[
$type
][
$phid
]
=
$phid
;
}
}
if
(
$value
)
{
$xactions
[]
=
id
(
new
DifferentialTransaction
())
->
setTransactionType
(
PhabricatorTransactions
::
TYPE_SUBSCRIBERS
)
->
setNewValue
(
$value
);
}
// Build a transaction to adjust reviewers.
$reviewers
=
array
(
DifferentialReviewerStatus
::
STATUS_ADDED
=>
array_keys
(
$adapter
->
getReviewersAddedByHerald
()),
DifferentialReviewerStatus
::
STATUS_BLOCKING
=>
array_keys
(
$adapter
->
getBlockingReviewersAddedByHerald
()),
);
$old_reviewers
=
$object
->
getReviewerStatus
();
$old_reviewers
=
mpull
(
$old_reviewers
,
null
,
'getReviewerPHID'
);
$value
=
array
();
foreach
(
$reviewers
as
$status
=>
$phids
)
{
foreach
(
$phids
as
$phid
)
{
if
(
$phid
==
$object
->
getAuthorPHID
())
{
// Don't try to add the revision's author as a reviewer, since this
// isn't valid and doesn't make sense.
continue
;
}
// If the target is already a reviewer, don't try to change anything
// if their current status is at least as strong as the new status.
// For example, don't downgrade an "Accepted" to a "Blocking Reviewer".
$old_reviewer
=
idx
(
$old_reviewers
,
$phid
);
if
(
$old_reviewer
)
{
$old_status
=
$old_reviewer
->
getStatus
();
$old_strength
=
DifferentialReviewerStatus
::
getStatusStrength
(
$old_status
);
$new_strength
=
DifferentialReviewerStatus
::
getStatusStrength
(
$status
);
if
(
$new_strength
<=
$old_strength
)
{
continue
;
}
}
$value
[
'+'
][
$phid
]
=
array
(
'data'
=>
array
(
'status'
=>
$status
,
),
);
}
}
if
(
$value
)
{
$edge_reviewer
=
DifferentialRevisionHasReviewerEdgeType
::
EDGECONST
;
$xactions
[]
=
id
(
new
DifferentialTransaction
())
->
setTransactionType
(
PhabricatorTransactions
::
TYPE_EDGE
)
->
setMetadataValue
(
'edge:type'
,
$edge_reviewer
)
->
setNewValue
(
$value
);
}
// Require legalpad document signatures.
$legal_phids
=
$adapter
->
getRequiredSignatureDocumentPHIDs
();
if
(
$legal_phids
)
{
// We only require signatures of documents which have not already
// been signed. In general, this reduces the amount of churn that
// signature rules cause.
$signatures
=
id
(
new
LegalpadDocumentSignatureQuery
())
->
setViewer
(
PhabricatorUser
::
getOmnipotentUser
())
->
withDocumentPHIDs
(
$legal_phids
)
->
withSignerPHIDs
(
array
(
$object
->
getAuthorPHID
()))
->
execute
();
$signed_phids
=
mpull
(
$signatures
,
'getDocumentPHID'
);
$legal_phids
=
array_diff
(
$legal_phids
,
$signed_phids
);
// If we still have something to trigger, add the edges.
if
(
$legal_phids
)
{
$edge_legal
=
LegalpadObjectNeedsSignatureEdgeType
::
EDGECONST
;
$xactions
[]
=
id
(
new
DifferentialTransaction
())
->
setTransactionType
(
PhabricatorTransactions
::
TYPE_EDGE
)
->
setMetadataValue
(
'edge:type'
,
$edge_legal
)
->
setNewValue
(
array
(
'+'
=>
array_fuse
(
$legal_phids
),
));
}
}
// Save extra email PHIDs for later.
$this
->
heraldEmailPHIDs
=
$adapter
->
getEmailPHIDs
();
// Apply build plans.
HarbormasterBuildable
::
applyBuildPlans
(
$adapter
->
getDiff
()->
getPHID
(),
$adapter
->
getPHID
(),
$adapter
->
getBuildPlans
());
return
$xactions
;
}
/**
* Update the table which links Differential revisions to paths they affect,
* so Diffusion can efficiently find pending revisions for a given file.
*/
private
function
updateAffectedPathTable
(
DifferentialRevision
$revision
,
DifferentialDiff
$diff
)
{
$repository
=
$revision
->
getRepository
();
if
(!
$repository
)
{
// The repository where the code lives is untracked.
return
;
}
$path_prefix
=
null
;
$local_root
=
$diff
->
getSourceControlPath
();
if
(
$local_root
)
{
// We're in a working copy which supports subdirectory checkouts (e.g.,
// SVN) so we need to figure out what prefix we should add to each path
// (e.g., trunk/projects/example/) to get the absolute path from the
// root of the repository. DVCS systems like Git and Mercurial are not
// affected.
// Normalize both paths and check if the repository root is a prefix of
// the local root. If so, throw it away. Note that this correctly handles
// the case where the remote path is "/".
$local_root
=
id
(
new
PhutilURI
(
$local_root
))->
getPath
();
$local_root
=
rtrim
(
$local_root
,
'/'
);
$repo_root
=
id
(
new
PhutilURI
(
$repository
->
getRemoteURI
()))->
getPath
();
$repo_root
=
rtrim
(
$repo_root
,
'/'
);
if
(!
strncmp
(
$repo_root
,
$local_root
,
strlen
(
$repo_root
)))
{
$path_prefix
=
substr
(
$local_root
,
strlen
(
$repo_root
));
}
}
$changesets
=
$diff
->
getChangesets
();
$paths
=
array
();
foreach
(
$changesets
as
$changeset
)
{
$paths
[]
=
$path_prefix
.
'/'
.
$changeset
->
getFilename
();
}
// Mark this as also touching all parent paths, so you can see all pending
// changes to any file within a directory.
$all_paths
=
array
();
foreach
(
$paths
as
$local
)
{
foreach
(
DiffusionPathIDQuery
::
expandPathToRoot
(
$local
)
as
$path
)
{
$all_paths
[
$path
]
=
true
;
}
}
$all_paths
=
array_keys
(
$all_paths
);
$path_ids
=
PhabricatorRepositoryCommitChangeParserWorker
::
lookupOrCreatePaths
(
$all_paths
);
$table
=
new
DifferentialAffectedPath
();
$conn_w
=
$table
->
establishConnection
(
'w'
);
$sql
=
array
();
foreach
(
$path_ids
as
$path_id
)
{
$sql
[]
=
qsprintf
(
$conn_w
,
'(%d, %d, %d, %d)'
,
$repository
->
getID
(),
$path_id
,
time
(),
$revision
->
getID
());
}
queryfx
(
$conn_w
,
'DELETE FROM %T WHERE revisionID = %d'
,
$table
->
getTableName
(),
$revision
->
getID
());
foreach
(
array_chunk
(
$sql
,
256
)
as
$chunk
)
{
queryfx
(
$conn_w
,
'INSERT INTO %T (repositoryID, pathID, epoch, revisionID) VALUES %Q'
,
$table
->
getTableName
(),
implode
(
', '
,
$chunk
));
}
}
/**
* Update the table connecting revisions to DVCS local hashes, so we can
* identify revisions by commit/tree hashes.
*/
private
function
updateRevisionHashTable
(
DifferentialRevision
$revision
,
DifferentialDiff
$diff
)
{
$vcs
=
$diff
->
getSourceControlSystem
();
if
(
$vcs
==
DifferentialRevisionControlSystem
::
SVN
)
{
// Subversion has no local commit or tree hash information, so we don't
// have to do anything.
return
;
}
$property
=
id
(
new
DifferentialDiffProperty
())->
loadOneWhere
(
'diffID = %d AND name = %s'
,
$diff
->
getID
(),
'local:commits'
);
if
(!
$property
)
{
return
;
}
$hashes
=
array
();
$data
=
$property
->
getData
();
switch
(
$vcs
)
{
case
DifferentialRevisionControlSystem
::
GIT
:
foreach
(
$data
as
$commit
)
{
$hashes
[]
=
array
(
ArcanistDifferentialRevisionHash
::
HASH_GIT_COMMIT
,
$commit
[
'commit'
],
);
$hashes
[]
=
array
(
ArcanistDifferentialRevisionHash
::
HASH_GIT_TREE
,
$commit
[
'tree'
],
);
}
break
;
case
DifferentialRevisionControlSystem
::
MERCURIAL
:
foreach
(
$data
as
$commit
)
{
$hashes
[]
=
array
(
ArcanistDifferentialRevisionHash
::
HASH_MERCURIAL_COMMIT
,
$commit
[
'rev'
],
);
}
break
;
}
$conn_w
=
$revision
->
establishConnection
(
'w'
);
$sql
=
array
();
foreach
(
$hashes
as
$info
)
{
list
(
$type
,
$hash
)
=
$info
;
$sql
[]
=
qsprintf
(
$conn_w
,
'(%d, %s, %s)'
,
$revision
->
getID
(),
$type
,
$hash
);
}
queryfx
(
$conn_w
,
'DELETE FROM %T WHERE revisionID = %d'
,
ArcanistDifferentialRevisionHash
::
TABLE_NAME
,
$revision
->
getID
());
if
(
$sql
)
{
queryfx
(
$conn_w
,
'INSERT INTO %T (revisionID, type, hash) VALUES %Q'
,
ArcanistDifferentialRevisionHash
::
TABLE_NAME
,
implode
(
', '
,
$sql
));
}
}
private
function
renderAffectedFilesForMail
(
DifferentialDiff
$diff
)
{
$changesets
=
$diff
->
getChangesets
();
$filenames
=
mpull
(
$changesets
,
'getDisplayFilename'
);
sort
(
$filenames
);
$count
=
count
(
$filenames
);
$max
=
250
;
if
(
$count
>
$max
)
{
$filenames
=
array_slice
(
$filenames
,
0
,
$max
);
$filenames
[]
=
pht
(
'(%d more files...)'
,
(
$count
-
$max
));
}
return
implode
(
"
\n
"
,
$filenames
);
}
private
function
renderPatchHTMLForMail
(
$patch
)
{
return
phutil_tag
(
'pre'
,
array
(
'style'
=>
'font-family: monospace;'
),
$patch
);
}
private
function
renderPatchForMail
(
DifferentialDiff
$diff
)
{
$format
=
PhabricatorEnv
::
getEnvConfig
(
'metamta.differential.patch-format'
);
$patch
=
id
(
new
DifferentialRawDiffRenderer
())
->
setViewer
(
$this
->
getActor
())
->
setFormat
(
$format
)
->
setChangesets
(
$diff
->
getChangesets
())
->
buildPatch
();
$section
=
new
PhabricatorMetaMTAMailSection
();
$section
->
addHTMLFragment
(
$this
->
renderPatchHTMLForMail
(
$patch
));
$section
->
addPlaintextFragment
(
$patch
);
return
$section
;
}
}
Event Timeline
Log In to Comment