Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2894754
DiffusionCommitHookEngine.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
47 KB
Referenced Files
None
Subscribers
None
DiffusionCommitHookEngine.php
View Options
<?php
/**
* @task config Configuring the Hook Engine
* @task hook Hook Execution
* @task git Git Hooks
* @task hg Mercurial Hooks
* @task svn Subversion Hooks
* @task internal Internals
*/
final
class
DiffusionCommitHookEngine
extends
Phobject
{
const
ENV_REPOSITORY
=
'PHABRICATOR_REPOSITORY'
;
const
ENV_USER
=
'PHABRICATOR_USER'
;
const
ENV_REQUEST
=
'PHABRICATOR_REQUEST'
;
const
ENV_REMOTE_ADDRESS
=
'PHABRICATOR_REMOTE_ADDRESS'
;
const
ENV_REMOTE_PROTOCOL
=
'PHABRICATOR_REMOTE_PROTOCOL'
;
const
EMPTY_HASH
=
'0000000000000000000000000000000000000000'
;
private
$viewer
;
private
$repository
;
private
$stdin
;
private
$originalArgv
;
private
$subversionTransaction
;
private
$subversionRepository
;
private
$remoteAddress
;
private
$remoteProtocol
;
private
$requestIdentifier
;
private
$transactionKey
;
private
$mercurialHook
;
private
$mercurialCommits
=
array
(
)
;
private
$gitCommits
=
array
(
)
;
private
$startTime
;
private
$heraldViewerProjects
;
private
$rejectCode
=
PhabricatorRepositoryPushLog
::
REJECT_BROKEN
;
private
$rejectDetails
;
private
$emailPHIDs
=
array
(
)
;
private
$changesets
=
array
(
)
;
private
$changesetsSize
=
0
;
private
$filesizeCache
=
array
(
)
;
/* -( Config )------------------------------------------------------------- */
public
function
setRemoteProtocol
(
$remote_protocol
)
{
$this
->
remoteProtocol
=
$remote_protocol
;
return
$this
;
}
public
function
getRemoteProtocol
(
)
{
return
$this
->
remoteProtocol
;
}
public
function
setRemoteAddress
(
$remote_address
)
{
$this
->
remoteAddress
=
$remote_address
;
return
$this
;
}
public
function
getRemoteAddress
(
)
{
return
$this
->
remoteAddress
;
}
public
function
setRequestIdentifier
(
$request_identifier
)
{
$this
->
requestIdentifier
=
$request_identifier
;
return
$this
;
}
public
function
getRequestIdentifier
(
)
{
return
$this
->
requestIdentifier
;
}
public
function
setStartTime
(
$start_time
)
{
$this
->
startTime
=
$start_time
;
return
$this
;
}
public
function
getStartTime
(
)
{
return
$this
->
startTime
;
}
public
function
setSubversionTransactionInfo
(
$transaction
,
$repository
)
{
$this
->
subversionTransaction
=
$transaction
;
$this
->
subversionRepository
=
$repository
;
return
$this
;
}
public
function
setStdin
(
$stdin
)
{
$this
->
stdin
=
$stdin
;
return
$this
;
}
public
function
getStdin
(
)
{
return
$this
->
stdin
;
}
public
function
setOriginalArgv
(
array
$original_argv
)
{
$this
->
originalArgv
=
$original_argv
;
return
$this
;
}
public
function
getOriginalArgv
(
)
{
return
$this
->
originalArgv
;
}
public
function
setRepository
(
PhabricatorRepository
$repository
)
{
$this
->
repository
=
$repository
;
return
$this
;
}
public
function
getRepository
(
)
{
return
$this
->
repository
;
}
public
function
setViewer
(
PhabricatorUser
$viewer
)
{
$this
->
viewer
=
$viewer
;
return
$this
;
}
public
function
getViewer
(
)
{
return
$this
->
viewer
;
}
public
function
setMercurialHook
(
$mercurial_hook
)
{
$this
->
mercurialHook
=
$mercurial_hook
;
return
$this
;
}
public
function
getMercurialHook
(
)
{
return
$this
->
mercurialHook
;
}
/* -( Hook Execution )----------------------------------------------------- */
public
function
execute
(
)
{
$ref_updates
=
$this
->
findRefUpdates
(
)
;
$all_updates
=
$ref_updates
;
$caught
=
null
;
try
{
try
{
$this
->
rejectDangerousChanges
(
$ref_updates
)
;
}
catch
(
DiffusionCommitHookRejectException
$ex
)
{
// If we're rejecting dangerous changes, flag everything that we've
// seen as rejected so it's clear that none of it was accepted.
$this
->
rejectCode
=
PhabricatorRepositoryPushLog
::
REJECT_DANGEROUS
;
throw
$ex
;
}
$content_updates
=
$this
->
findContentUpdates
(
$ref_updates
)
;
$all_updates
=
array_merge
(
$ref_updates
,
$content_updates
)
;
// If this is an "initial import" (a sizable push to a previously empty
// repository) we'll allow enormous changes and disable Herald rules.
// These rulesets can consume a large amount of time and memory and are
// generally not relevant when importing repository history.
$is_initial_import
=
$this
->
isInitialImport
(
$all_updates
)
;
if
(
!
$is_initial_import
)
{
$this
->
applyHeraldRefRules
(
$ref_updates
)
;
}
try
{
if
(
!
$is_initial_import
)
{
$this
->
rejectOversizedFiles
(
$content_updates
)
;
}
}
catch
(
DiffusionCommitHookRejectException
$ex
)
{
// If we're rejecting oversized files, flag everything.
$this
->
rejectCode
=
PhabricatorRepositoryPushLog
::
REJECT_OVERSIZED
;
throw
$ex
;
}
try
{
if
(
!
$is_initial_import
)
{
$this
->
rejectCommitsAffectingTooManyPaths
(
$content_updates
)
;
}
}
catch
(
DiffusionCommitHookRejectException
$ex
)
{
$this
->
rejectCode
=
PhabricatorRepositoryPushLog
::
REJECT_TOUCHES
;
throw
$ex
;
}
try
{
if
(
!
$is_initial_import
)
{
$this
->
rejectEnormousChanges
(
$content_updates
)
;
}
}
catch
(
DiffusionCommitHookRejectException
$ex
)
{
// If we're rejecting enormous changes, flag everything.
$this
->
rejectCode
=
PhabricatorRepositoryPushLog
::
REJECT_ENORMOUS
;
throw
$ex
;
}
if
(
!
$is_initial_import
)
{
$this
->
applyHeraldContentRules
(
$content_updates
)
;
}
// Run custom scripts in `hook.d/` directories.
$this
->
applyCustomHooks
(
$all_updates
)
;
// If we make it this far, we're accepting these changes. Mark all the
// logs as accepted.
$this
->
rejectCode
=
PhabricatorRepositoryPushLog
::
REJECT_ACCEPT
;
}
catch
(
Exception
$ex
)
{
// We'll throw this again in a minute, but we want to save all the logs
// first.
$caught
=
$ex
;
}
// Save all the logs no matter what the outcome was.
$event
=
$this
->
newPushEvent
(
)
;
$event
->
setRejectCode
(
$this
->
rejectCode
)
;
$event
->
setRejectDetails
(
$this
->
rejectDetails
)
;
$event
->
saveWithLogs
(
$all_updates
)
;
if
(
$caught
)
{
throw
$caught
;
}
// If this went through cleanly and was an import, set the importing flag
// on the repository. It will be cleared once we fully process everything.
if
(
$is_initial_import
)
{
$repository
=
$this
->
getRepository
(
)
;
$repository
->
markImporting
(
)
;
}
if
(
$this
->
emailPHIDs
)
{
// If Herald rules triggered email to users, queue a worker to send the
// mail. We do this out-of-process so that we block pushes as briefly
// as possible.
// (We do need to pull some commit info here because the commit objects
// may not exist yet when this worker runs, which could be immediately.)
PhabricatorWorker
::
scheduleTask
(
'PhabricatorRepositoryPushMailWorker'
,
array
(
'eventPHID'
=>
$event
->
getPHID
(
)
,
'emailPHIDs'
=>
array_values
(
$this
->
emailPHIDs
)
,
'info'
=>
$this
->
loadCommitInfoForWorker
(
$all_updates
)
,
)
,
array
(
'priority'
=>
PhabricatorWorker
::
PRIORITY_ALERTS
,
)
)
;
}
return
0
;
}
private
function
findRefUpdates
(
)
{
$type
=
$this
->
getRepository
(
)
->
getVersionControlSystem
(
)
;
switch
(
$type
)
{
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
:
return
$this
->
findGitRefUpdates
(
)
;
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_MERCURIAL
:
return
$this
->
findMercurialRefUpdates
(
)
;
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_SVN
:
return
$this
->
findSubversionRefUpdates
(
)
;
default
:
throw
new
Exception
(
pht
(
'Unsupported repository type "%s"!'
,
$type
)
)
;
}
}
private
function
rejectDangerousChanges
(
array
$ref_updates
)
{
assert_instances_of
(
$ref_updates
,
'PhabricatorRepositoryPushLog'
)
;
$repository
=
$this
->
getRepository
(
)
;
if
(
$repository
->
shouldAllowDangerousChanges
(
)
)
{
return
;
}
$flag_dangerous
=
PhabricatorRepositoryPushLog
::
CHANGEFLAG_DANGEROUS
;
foreach
(
$ref_updates
as
$ref_update
)
{
if
(
!
$ref_update
->
hasChangeFlags
(
$flag_dangerous
)
)
{
// This is not a dangerous change.
continue
;
}
// We either have a branch deletion or a non fast-forward branch update.
// Format a message and reject the push.
$message
=
pht
(
"DANGEROUS CHANGE: %s\n"
.
"Dangerous change protection is enabled for this repository.\n"
.
"Edit the repository configuration before making dangerous changes."
,
$ref_update
->
getDangerousChangeDescription
(
)
)
;
throw
new
DiffusionCommitHookRejectException
(
$message
)
;
}
}
private
function
findContentUpdates
(
array
$ref_updates
)
{
assert_instances_of
(
$ref_updates
,
'PhabricatorRepositoryPushLog'
)
;
$type
=
$this
->
getRepository
(
)
->
getVersionControlSystem
(
)
;
switch
(
$type
)
{
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
:
return
$this
->
findGitContentUpdates
(
$ref_updates
)
;
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_MERCURIAL
:
return
$this
->
findMercurialContentUpdates
(
$ref_updates
)
;
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_SVN
:
return
$this
->
findSubversionContentUpdates
(
$ref_updates
)
;
default
:
throw
new
Exception
(
pht
(
'Unsupported repository type "%s"!'
,
$type
)
)
;
}
}
/* -( Herald )------------------------------------------------------------- */
private
function
applyHeraldRefRules
(
array
$ref_updates
)
{
$this
->
applyHeraldRules
(
$ref_updates
,
new
HeraldPreCommitRefAdapter
(
)
)
;
}
private
function
applyHeraldContentRules
(
array
$content_updates
)
{
$this
->
applyHeraldRules
(
$content_updates
,
new
HeraldPreCommitContentAdapter
(
)
)
;
}
private
function
applyHeraldRules
(
array
$updates
,
HeraldAdapter
$adapter_template
)
{
if
(
!
$updates
)
{
return
;
}
$viewer
=
$this
->
getViewer
(
)
;
$adapter_template
->
setHookEngine
(
$this
)
->
setActingAsPHID
(
$viewer
->
getPHID
(
)
)
;
$engine
=
new
HeraldEngine
(
)
;
$rules
=
null
;
$blocking_effect
=
null
;
$blocked_update
=
null
;
$blocking_xscript
=
null
;
foreach
(
$updates
as
$update
)
{
$adapter
=
id
(
clone
$adapter_template
)
->
setPushLog
(
$update
)
;
if
(
$rules
===
null
)
{
$rules
=
$engine
->
loadRulesForAdapter
(
$adapter
)
;
}
$effects
=
$engine
->
applyRules
(
$rules
,
$adapter
)
;
$engine
->
applyEffects
(
$effects
,
$adapter
,
$rules
)
;
$xscript
=
$engine
->
getTranscript
(
)
;
// Store any PHIDs we want to send email to for later.
foreach
(
$adapter
->
getEmailPHIDs
(
)
as
$email_phid
)
{
$this
->
emailPHIDs
[
$email_phid
]
=
$email_phid
;
}
$block_action
=
DiffusionBlockHeraldAction
::
ACTIONCONST
;
if
(
$blocking_effect
===
null
)
{
foreach
(
$effects
as
$effect
)
{
if
(
$effect
->
getAction
(
)
==
$block_action
)
{
$blocking_effect
=
$effect
;
$blocked_update
=
$update
;
$blocking_xscript
=
$xscript
;
break
;
}
}
}
}
if
(
$blocking_effect
)
{
$rule
=
$blocking_effect
->
getRule
(
)
;
$this
->
rejectCode
=
PhabricatorRepositoryPushLog
::
REJECT_HERALD
;
$this
->
rejectDetails
=
$rule
->
getPHID
(
)
;
$message
=
$blocking_effect
->
getTarget
(
)
;
if
(
!
strlen
(
$message
)
)
{
$message
=
pht
(
'(None.)'
)
;
}
$blocked_ref_name
=
coalesce
(
$blocked_update
->
getRefName
(
)
,
$blocked_update
->
getRefNewShort
(
)
)
;
$blocked_name
=
$blocked_update
->
getRefType
(
)
.
'/'
.
$blocked_ref_name
;
throw
new
DiffusionCommitHookRejectException
(
pht
(
"This push was rejected by Herald push rule %s.\n"
.
" Change: %s\n"
.
" Rule: %s\n"
.
" Reason: %s\n"
.
"Transcript: %s"
,
$rule
->
getMonogram
(
)
,
$blocked_name
,
$rule
->
getName
(
)
,
$message
,
PhabricatorEnv
::
getProductionURI
(
'/herald/transcript/'
.
$blocking_xscript
->
getID
(
)
.
'/'
)
)
)
;
}
}
public
function
loadViewerProjectPHIDsForHerald
(
)
{
// This just caches the viewer's projects so we don't need to load them
// over and over again when applying Herald rules.
if
(
$this
->
heraldViewerProjects
===
null
)
{
$this
->
heraldViewerProjects
=
id
(
new
PhabricatorProjectQuery
(
)
)
->
setViewer
(
$this
->
getViewer
(
)
)
->
withMemberPHIDs
(
array
(
$this
->
getViewer
(
)
->
getPHID
(
)
)
)
->
execute
(
)
;
}
return
mpull
(
$this
->
heraldViewerProjects
,
'getPHID'
)
;
}
/* -( Git )---------------------------------------------------------------- */
private
function
findGitRefUpdates
(
)
{
$ref_updates
=
array
(
)
;
// First, parse stdin, which lists all the ref changes. The input looks
// like this:
//
// <old hash> <new hash> <ref>
$stdin
=
$this
->
getStdin
(
)
;
$lines
=
phutil_split_lines
(
$stdin
,
$retain_endings
=
false
)
;
foreach
(
$lines
as
$line
)
{
$parts
=
explode
(
' '
,
$line
,
3
)
;
if
(
count
(
$parts
)
!=
3
)
{
throw
new
Exception
(
pht
(
'Expected "old new ref", got "%s".'
,
$line
)
)
;
}
$ref_old
=
$parts
[
0
]
;
$ref_new
=
$parts
[
1
]
;
$ref_raw
=
$parts
[
2
]
;
if
(
preg_match
(
'(^refs/heads/)'
,
$ref_raw
)
)
{
$ref_type
=
PhabricatorRepositoryPushLog
::
REFTYPE_BRANCH
;
$ref_raw
=
substr
(
$ref_raw
,
strlen
(
'refs/heads/'
)
)
;
}
else
if
(
preg_match
(
'(^refs/tags/)'
,
$ref_raw
)
)
{
$ref_type
=
PhabricatorRepositoryPushLog
::
REFTYPE_TAG
;
$ref_raw
=
substr
(
$ref_raw
,
strlen
(
'refs/tags/'
)
)
;
}
else
{
$ref_type
=
PhabricatorRepositoryPushLog
::
REFTYPE_REF
;
}
$ref_update
=
$this
->
newPushLog
(
)
->
setRefType
(
$ref_type
)
->
setRefName
(
$ref_raw
)
->
setRefOld
(
$ref_old
)
->
setRefNew
(
$ref_new
)
;
$ref_updates
[
]
=
$ref_update
;
}
$this
->
findGitMergeBases
(
$ref_updates
)
;
$this
->
findGitChangeFlags
(
$ref_updates
)
;
return
$ref_updates
;
}
private
function
findGitMergeBases
(
array
$ref_updates
)
{
assert_instances_of
(
$ref_updates
,
'PhabricatorRepositoryPushLog'
)
;
$futures
=
array
(
)
;
foreach
(
$ref_updates
as
$key
=>
$ref_update
)
{
// If the old hash is "00000...", the ref is being created (either a new
// branch, or a new tag). If the new hash is "00000...", the ref is being
// deleted. If both are nonempty, the ref is being updated. For updates,
// we'll figure out the `merge-base` of the old and new objects here. This
// lets us reject non-FF changes cheaply; later, we'll figure out exactly
// which commits are new.
$ref_old
=
$ref_update
->
getRefOld
(
)
;
$ref_new
=
$ref_update
->
getRefNew
(
)
;
if
(
(
$ref_old
===
self
::
EMPTY_HASH
)
||
(
$ref_new
===
self
::
EMPTY_HASH
)
)
{
continue
;
}
$futures
[
$key
]
=
$this
->
getRepository
(
)
->
getLocalCommandFuture
(
'merge-base %s %s'
,
$ref_old
,
$ref_new
)
;
}
$futures
=
id
(
new
FutureIterator
(
$futures
)
)
->
limit
(
8
)
;
foreach
(
$futures
as
$key
=>
$future
)
{
// If 'old' and 'new' have no common ancestors (for example, a force push
// which completely rewrites a ref), `git merge-base` will exit with
// an error and no output. It would be nice to find a positive test
// for this instead, but I couldn't immediately come up with one. See
// T4224. Assume this means there are no ancestors.
list
(
$err
,
$stdout
)
=
$future
->
resolve
(
)
;
if
(
$err
)
{
$merge_base
=
null
;
}
else
{
$merge_base
=
rtrim
(
$stdout
,
"\n"
)
;
}
$ref_update
=
$ref_updates
[
$key
]
;
$ref_update
->
setMergeBase
(
$merge_base
)
;
}
return
$ref_updates
;
}
private
function
findGitChangeFlags
(
array
$ref_updates
)
{
assert_instances_of
(
$ref_updates
,
'PhabricatorRepositoryPushLog'
)
;
foreach
(
$ref_updates
as
$key
=>
$ref_update
)
{
$ref_old
=
$ref_update
->
getRefOld
(
)
;
$ref_new
=
$ref_update
->
getRefNew
(
)
;
$ref_type
=
$ref_update
->
getRefType
(
)
;
$ref_flags
=
0
;
$dangerous
=
null
;
if
(
(
$ref_old
===
self
::
EMPTY_HASH
)
&&
(
$ref_new
===
self
::
EMPTY_HASH
)
)
{
// This happens if you try to delete a tag or branch which does not
// exist by pushing directly to the ref. Git will warn about it but
// allow it. Just call it a delete, without flagging it as dangerous.
$ref_flags
|=
PhabricatorRepositoryPushLog
::
CHANGEFLAG_DELETE
;
}
else
if
(
$ref_old
===
self
::
EMPTY_HASH
)
{
$ref_flags
|=
PhabricatorRepositoryPushLog
::
CHANGEFLAG_ADD
;
}
else
if
(
$ref_new
===
self
::
EMPTY_HASH
)
{
$ref_flags
|=
PhabricatorRepositoryPushLog
::
CHANGEFLAG_DELETE
;
if
(
$ref_type
==
PhabricatorRepositoryPushLog
::
REFTYPE_BRANCH
)
{
$ref_flags
|=
PhabricatorRepositoryPushLog
::
CHANGEFLAG_DANGEROUS
;
$dangerous
=
pht
(
"The change you're attempting to push deletes the branch '%s'."
,
$ref_update
->
getRefName
(
)
)
;
}
}
else
{
$merge_base
=
$ref_update
->
getMergeBase
(
)
;
if
(
$merge_base
==
$ref_old
)
{
// This is a fast-forward update to an existing branch.
// These are safe.
$ref_flags
|=
PhabricatorRepositoryPushLog
::
CHANGEFLAG_APPEND
;
}
else
{
$ref_flags
|=
PhabricatorRepositoryPushLog
::
CHANGEFLAG_REWRITE
;
// For now, we don't consider deleting or moving tags to be a
// "dangerous" update. It's way harder to get wrong and should be easy
// to recover from once we have better logging. Only add the dangerous
// flag if this ref is a branch.
if
(
$ref_type
==
PhabricatorRepositoryPushLog
::
REFTYPE_BRANCH
)
{
$ref_flags
|=
PhabricatorRepositoryPushLog
::
CHANGEFLAG_DANGEROUS
;
$dangerous
=
pht
(
"The change you're attempting to push updates the branch '%s' "
.
"from '%s' to '%s', but this is not a fast-forward. Pushes "
.
"which rewrite published branch history are dangerous."
,
$ref_update
->
getRefName
(
)
,
$ref_update
->
getRefOldShort
(
)
,
$ref_update
->
getRefNewShort
(
)
)
;
}
}
}
$ref_update
->
setChangeFlags
(
$ref_flags
)
;
if
(
$dangerous
!==
null
)
{
$ref_update
->
attachDangerousChangeDescription
(
$dangerous
)
;
}
}
return
$ref_updates
;
}
private
function
findGitContentUpdates
(
array
$ref_updates
)
{
$flag_delete
=
PhabricatorRepositoryPushLog
::
CHANGEFLAG_DELETE
;
$futures
=
array
(
)
;
foreach
(
$ref_updates
as
$key
=>
$ref_update
)
{
if
(
$ref_update
->
hasChangeFlags
(
$flag_delete
)
)
{
// Deleting a branch or tag can never create any new commits.
continue
;
}
// NOTE: This piece of magic finds all new commits, by walking backward
// from the new value to the value of *any* existing ref in the
// repository. Particularly, this will cover the cases of a new branch, a
// completely moved tag, etc.
$futures
[
$key
]
=
$this
->
getRepository
(
)
->
getLocalCommandFuture
(
'log %s %s --not --all --'
,
'--format=%H'
,
gitsprintf
(
'%s'
,
$ref_update
->
getRefNew
(
)
)
)
;
}
$content_updates
=
array
(
)
;
$futures
=
id
(
new
FutureIterator
(
$futures
)
)
->
limit
(
8
)
;
foreach
(
$futures
as
$key
=>
$future
)
{
list
(
$stdout
)
=
$future
->
resolvex
(
)
;
if
(
!
strlen
(
trim
(
$stdout
)
)
)
{
// This change doesn't have any new commits. One common case of this
// is creating a new tag which points at an existing commit.
continue
;
}
$commits
=
phutil_split_lines
(
$stdout
,
$retain_newlines
=
false
)
;
// If we're looking at a branch, mark all of the new commits as on that
// branch. It's only possible for these commits to be on updated branches,
// since any other branch heads are necessarily behind them.
$branch_name
=
null
;
$ref_update
=
$ref_updates
[
$key
]
;
$type_branch
=
PhabricatorRepositoryPushLog
::
REFTYPE_BRANCH
;
if
(
$ref_update
->
getRefType
(
)
==
$type_branch
)
{
$branch_name
=
$ref_update
->
getRefName
(
)
;
}
foreach
(
$commits
as
$commit
)
{
if
(
$branch_name
)
{
$this
->
gitCommits
[
$commit
]
[
]
=
$branch_name
;
}
$content_updates
[
$commit
]
=
$this
->
newPushLog
(
)
->
setRefType
(
PhabricatorRepositoryPushLog
::
REFTYPE_COMMIT
)
->
setRefNew
(
$commit
)
->
setChangeFlags
(
PhabricatorRepositoryPushLog
::
CHANGEFLAG_ADD
)
;
}
}
return
$content_updates
;
}
/* -( Custom )------------------------------------------------------------- */
private
function
applyCustomHooks
(
array
$updates
)
{
$args
=
$this
->
getOriginalArgv
(
)
;
$stdin
=
$this
->
getStdin
(
)
;
$console
=
PhutilConsole
::
getConsole
(
)
;
$env
=
array
(
self
::
ENV_REPOSITORY
=>
$this
->
getRepository
(
)
->
getPHID
(
)
,
self
::
ENV_USER
=>
$this
->
getViewer
(
)
->
getUsername
(
)
,
self
::
ENV_REQUEST
=>
$this
->
getRequestIdentifier
(
)
,
self
::
ENV_REMOTE_PROTOCOL
=>
$this
->
getRemoteProtocol
(
)
,
self
::
ENV_REMOTE_ADDRESS
=>
$this
->
getRemoteAddress
(
)
,
)
;
$repository
=
$this
->
getRepository
(
)
;
$env
+=
$repository
->
getPassthroughEnvironmentalVariables
(
)
;
$directories
=
$repository
->
getHookDirectories
(
)
;
foreach
(
$directories
as
$directory
)
{
$hooks
=
$this
->
getExecutablesInDirectory
(
$directory
)
;
sort
(
$hooks
)
;
foreach
(
$hooks
as
$hook
)
{
// NOTE: We're explicitly running the hooks in sequential order to
// make this more predictable.
$future
=
id
(
new
ExecFuture
(
'%s %Ls'
,
$hook
,
$args
)
)
->
setEnv
(
$env
,
$wipe_process_env
=
false
)
->
write
(
$stdin
)
;
list
(
$err
,
$stdout
,
$stderr
)
=
$future
->
resolve
(
)
;
if
(
!
$err
)
{
// This hook ran OK, but echo its output in case there was something
// informative.
$console
->
writeOut
(
'%s'
,
$stdout
)
;
$console
->
writeErr
(
'%s'
,
$stderr
)
;
continue
;
}
$this
->
rejectCode
=
PhabricatorRepositoryPushLog
::
REJECT_EXTERNAL
;
$this
->
rejectDetails
=
basename
(
$hook
)
;
throw
new
DiffusionCommitHookRejectException
(
pht
(
"This push was rejected by custom hook script '%s':\n\n%s%s"
,
basename
(
$hook
)
,
$stdout
,
$stderr
)
)
;
}
}
}
private
function
getExecutablesInDirectory
(
$directory
)
{
$executables
=
array
(
)
;
if
(
!
Filesystem
::
pathExists
(
$directory
)
)
{
return
$executables
;
}
foreach
(
Filesystem
::
listDirectory
(
$directory
)
as
$path
)
{
$full_path
=
$directory
.
DIRECTORY_SEPARATOR
.
$path
;
if
(
!
is_executable
(
$full_path
)
)
{
// Don't include non-executable files.
continue
;
}
if
(
basename
(
$full_path
)
==
'README'
)
{
// Don't include README, even if it is marked as executable. It almost
// certainly got caught in the crossfire of a sweeping `chmod`, since
// users do this with some frequency.
continue
;
}
$executables
[
]
=
$full_path
;
}
return
$executables
;
}
/* -( Mercurial )---------------------------------------------------------- */
private
function
findMercurialRefUpdates
(
)
{
$hook
=
$this
->
getMercurialHook
(
)
;
switch
(
$hook
)
{
case
'pretxnchangegroup'
:
return
$this
->
findMercurialChangegroupRefUpdates
(
)
;
case
'prepushkey'
:
return
$this
->
findMercurialPushKeyRefUpdates
(
)
;
default
:
throw
new
Exception
(
pht
(
'Unrecognized hook "%s"!'
,
$hook
)
)
;
}
}
private
function
findMercurialChangegroupRefUpdates
(
)
{
$hg_node
=
getenv
(
'HG_NODE'
)
;
if
(
!
$hg_node
)
{
throw
new
Exception
(
pht
(
'Expected %s in environment!'
,
'HG_NODE'
)
)
;
}
// NOTE: We need to make sure this is passed to subprocesses, or they won't
// be able to see new commits. Mercurial uses this as a marker to determine
// whether the pending changes are visible or not.
$_ENV
[
'HG_PENDING'
]
=
getenv
(
'HG_PENDING'
)
;
$repository
=
$this
->
getRepository
(
)
;
$futures
=
array
(
)
;
foreach
(
array
(
'old'
,
'new'
)
as
$key
)
{
$futures
[
$key
]
=
$repository
->
getLocalCommandFuture
(
'heads --template %s'
,
'{node}\1{branch}\2'
)
;
}
// Wipe HG_PENDING out of the old environment so we see the pre-commit
// state of the repository.
$futures
[
'old'
]
->
updateEnv
(
'HG_PENDING'
,
null
)
;
$futures
[
'commits'
]
=
$repository
->
getLocalCommandFuture
(
'log --rev %s --template %s'
,
hgsprintf
(
'%s:%s'
,
$hg_node
,
'tip'
)
,
'{node}\1{branch}\2'
)
;
// Resolve all of the futures now. We don't need the 'commits' future yet,
// but it simplifies the logic to just get it out of the way.
foreach
(
new
FutureIterator
(
$futures
)
as
$future
)
{
$future
->
resolve
(
)
;
}
list
(
$commit_raw
)
=
$futures
[
'commits'
]
->
resolvex
(
)
;
$commit_map
=
$this
->
parseMercurialCommits
(
$commit_raw
)
;
$this
->
mercurialCommits
=
$commit_map
;
// NOTE: `hg heads` exits with an error code and no output if the repository
// has no heads. Most commonly this happens on a new repository. We know
// we can run `hg` successfully since the `hg log` above didn't error, so
// just ignore the error code.
list
(
$err
,
$old_raw
)
=
$futures
[
'old'
]
->
resolve
(
)
;
$old_refs
=
$this
->
parseMercurialHeads
(
$old_raw
)
;
list
(
$err
,
$new_raw
)
=
$futures
[
'new'
]
->
resolve
(
)
;
$new_refs
=
$this
->
parseMercurialHeads
(
$new_raw
)
;
$all_refs
=
array_keys
(
$old_refs
+
$new_refs
)
;
$ref_updates
=
array
(
)
;
foreach
(
$all_refs
as
$ref
)
{
$old_heads
=
idx
(
$old_refs
,
$ref
,
array
(
)
)
;
$new_heads
=
idx
(
$new_refs
,
$ref
,
array
(
)
)
;
sort
(
$old_heads
)
;
sort
(
$new_heads
)
;
if
(
!
$old_heads
&&
!
$new_heads
)
{
// This should never be possible, as it makes no sense. Explode.
throw
new
Exception
(
pht
(
'Mercurial repository has no new or old heads for branch "%s" '
.
'after push. This makes no sense; rejecting change.'
,
$ref
)
)
;
}
if
(
$old_heads
===
$new_heads
)
{
// No changes to this branch, so skip it.
continue
;
}
$stray_heads
=
array
(
)
;
$head_map
=
array
(
)
;
if
(
$old_heads
&&
!
$new_heads
)
{
// This is a branch deletion with "--close-branch".
foreach
(
$old_heads
as
$old_head
)
{
$head_map
[
$old_head
]
=
array
(
self
::
EMPTY_HASH
)
;
}
}
else
if
(
count
(
$old_heads
)
>
1
)
{
// HORRIBLE: In Mercurial, branches can have multiple heads. If the
// old branch had multiple heads, we need to figure out which new
// heads descend from which old heads, so we can tell whether you're
// actively creating new heads (dangerous) or just working in a
// repository that's already full of garbage (strongly discouraged but
// not as inherently dangerous). These cases should be very uncommon.
// NOTE: We're only looking for heads on the same branch. The old
// tip of the branch may be the branchpoint for other branches, but that
// is OK.
$dfutures
=
array
(
)
;
foreach
(
$old_heads
as
$old_head
)
{
$dfutures
[
$old_head
]
=
$repository
->
getLocalCommandFuture
(
'log --branch %s --rev %s --template %s'
,
$ref
,
hgsprintf
(
'(descendants(%s) and head())'
,
$old_head
)
,
'{node}\1'
)
;
}
foreach
(
new
FutureIterator
(
$dfutures
)
as
$future_head
=>
$dfuture
)
{
list
(
$stdout
)
=
$dfuture
->
resolvex
(
)
;
$descendant_heads
=
array_filter
(
explode
(
"\1"
,
$stdout
)
)
;
if
(
$descendant_heads
)
{
// This old head has at least one descendant in the push.
$head_map
[
$future_head
]
=
$descendant_heads
;
}
else
{
// This old head has no descendants, so it is being deleted.
$head_map
[
$future_head
]
=
array
(
self
::
EMPTY_HASH
)
;
}
}
// Now, find all the new stray heads this push creates, if any. These
// are new heads which do not descend from the old heads.
$seen
=
array_fuse
(
array_mergev
(
$head_map
)
)
;
foreach
(
$new_heads
as
$new_head
)
{
if
(
$new_head
===
self
::
EMPTY_HASH
)
{
// If a branch head is being deleted, don't insert it as an add.
continue
;
}
if
(
empty
(
$seen
[
$new_head
]
)
)
{
$head_map
[
self
::
EMPTY_HASH
]
[
]
=
$new_head
;
}
}
}
else
if
(
$old_heads
)
{
$head_map
[
head
(
$old_heads
)
]
=
$new_heads
;
}
else
{
$head_map
[
self
::
EMPTY_HASH
]
=
$new_heads
;
}
foreach
(
$head_map
as
$old_head
=>
$child_heads
)
{
foreach
(
$child_heads
as
$new_head
)
{
if
(
$new_head
===
$old_head
)
{
continue
;
}
$ref_flags
=
0
;
$dangerous
=
null
;
if
(
$old_head
==
self
::
EMPTY_HASH
)
{
$ref_flags
|=
PhabricatorRepositoryPushLog
::
CHANGEFLAG_ADD
;
}
else
{
$ref_flags
|=
PhabricatorRepositoryPushLog
::
CHANGEFLAG_APPEND
;
}
$deletes_existing_head
=
(
$new_head
==
self
::
EMPTY_HASH
)
;
$splits_existing_head
=
(
count
(
$child_heads
)
>
1
)
;
$creates_duplicate_head
=
(
$old_head
==
self
::
EMPTY_HASH
)
&&
(
count
(
$head_map
)
>
1
)
;
if
(
$splits_existing_head
||
$creates_duplicate_head
)
{
$readable_child_heads
=
array
(
)
;
foreach
(
$child_heads
as
$child_head
)
{
$readable_child_heads
[
]
=
substr
(
$child_head
,
0
,
12
)
;
}
$ref_flags
|=
PhabricatorRepositoryPushLog
::
CHANGEFLAG_DANGEROUS
;
if
(
$splits_existing_head
)
{
// We're splitting an existing head into two or more heads.
// This is dangerous, and a super bad idea. Note that we're only
// raising this if you're actively splitting a branch head. If a
// head split in the past, we don't consider appends to it
// to be dangerous.
$dangerous
=
pht
(
"The change you're attempting to push splits the head of "
.
"branch '%s' into multiple heads: %s. This is inadvisable "
.
"and dangerous."
,
$ref
,
implode
(
', '
,
$readable_child_heads
)
)
;
}
else
{
// We're adding a second (or more) head to a branch. The new
// head is not a descendant of any old head.
$dangerous
=
pht
(
"The change you're attempting to push creates new, divergent "
.
"heads for the branch '%s': %s. This is inadvisable and "
.
"dangerous."
,
$ref
,
implode
(
', '
,
$readable_child_heads
)
)
;
}
}
if
(
$deletes_existing_head
)
{
// TODO: Somewhere in here we should be setting CHANGEFLAG_REWRITE
// if we are also creating at least one other head to replace
// this one.
// NOTE: In Git, this is a dangerous change, but it is not dangerous
// in Mercurial. Mercurial branches are version controlled, and
// Mercurial does not prompt you for any special flags when pushing
// a `--close-branch` commit by default.
$ref_flags
|=
PhabricatorRepositoryPushLog
::
CHANGEFLAG_DELETE
;
}
$ref_update
=
$this
->
newPushLog
(
)
->
setRefType
(
PhabricatorRepositoryPushLog
::
REFTYPE_BRANCH
)
->
setRefName
(
$ref
)
->
setRefOld
(
$old_head
)
->
setRefNew
(
$new_head
)
->
setChangeFlags
(
$ref_flags
)
;
if
(
$dangerous
!==
null
)
{
$ref_update
->
attachDangerousChangeDescription
(
$dangerous
)
;
}
$ref_updates
[
]
=
$ref_update
;
}
}
}
return
$ref_updates
;
}
private
function
findMercurialPushKeyRefUpdates
(
)
{
$key_namespace
=
getenv
(
'HG_NAMESPACE'
)
;
if
(
$key_namespace
===
'phases'
)
{
// Mercurial changes commit phases as part of normal push operations. We
// just ignore these, as they don't seem to represent anything
// interesting.
return
array
(
)
;
}
$key_name
=
getenv
(
'HG_KEY'
)
;
$key_old
=
getenv
(
'HG_OLD'
)
;
if
(
!
strlen
(
$key_old
)
)
{
$key_old
=
null
;
}
$key_new
=
getenv
(
'HG_NEW'
)
;
if
(
!
strlen
(
$key_new
)
)
{
$key_new
=
null
;
}
if
(
$key_namespace
!==
'bookmarks'
)
{
throw
new
Exception
(
pht
(
"Unknown Mercurial key namespace '%s', with key '%s' (%s -> %s). "
.
"Rejecting push."
,
$key_namespace
,
$key_name
,
coalesce
(
$key_old
,
pht
(
'null'
)
)
,
coalesce
(
$key_new
,
pht
(
'null'
)
)
)
)
;
}
if
(
$key_old
===
$key_new
)
{
// We get a callback when the bookmark doesn't change. Just ignore this,
// as it's a no-op.
return
array
(
)
;
}
$ref_flags
=
0
;
$merge_base
=
null
;
if
(
$key_old
===
null
)
{
$ref_flags
|=
PhabricatorRepositoryPushLog
::
CHANGEFLAG_ADD
;
}
else
if
(
$key_new
===
null
)
{
$ref_flags
|=
PhabricatorRepositoryPushLog
::
CHANGEFLAG_DELETE
;
}
else
{
list
(
$merge_base_raw
)
=
$this
->
getRepository
(
)
->
execxLocalCommand
(
'log --template %s --rev %s'
,
'{node}'
,
hgsprintf
(
'ancestor(%s, %s)'
,
$key_old
,
$key_new
)
)
;
if
(
strlen
(
trim
(
$merge_base_raw
)
)
)
{
$merge_base
=
trim
(
$merge_base_raw
)
;
}
if
(
$merge_base
&&
(
$merge_base
===
$key_old
)
)
{
$ref_flags
|=
PhabricatorRepositoryPushLog
::
CHANGEFLAG_APPEND
;
}
else
{
$ref_flags
|=
PhabricatorRepositoryPushLog
::
CHANGEFLAG_REWRITE
;
}
}
$ref_update
=
$this
->
newPushLog
(
)
->
setRefType
(
PhabricatorRepositoryPushLog
::
REFTYPE_BOOKMARK
)
->
setRefName
(
$key_name
)
->
setRefOld
(
coalesce
(
$key_old
,
self
::
EMPTY_HASH
)
)
->
setRefNew
(
coalesce
(
$key_new
,
self
::
EMPTY_HASH
)
)
->
setChangeFlags
(
$ref_flags
)
;
return
array
(
$ref_update
)
;
}
private
function
findMercurialContentUpdates
(
array
$ref_updates
)
{
$content_updates
=
array
(
)
;
foreach
(
$this
->
mercurialCommits
as
$commit
=>
$branches
)
{
$content_updates
[
$commit
]
=
$this
->
newPushLog
(
)
->
setRefType
(
PhabricatorRepositoryPushLog
::
REFTYPE_COMMIT
)
->
setRefNew
(
$commit
)
->
setChangeFlags
(
PhabricatorRepositoryPushLog
::
CHANGEFLAG_ADD
)
;
}
return
$content_updates
;
}
private
function
parseMercurialCommits
(
$raw
)
{
$commits_lines
=
explode
(
"\2"
,
$raw
)
;
$commits_lines
=
array_filter
(
$commits_lines
)
;
$commit_map
=
array
(
)
;
foreach
(
$commits_lines
as
$commit_line
)
{
list
(
$node
,
$branch
)
=
explode
(
"\1"
,
$commit_line
)
;
$commit_map
[
$node
]
=
array
(
$branch
)
;
}
return
$commit_map
;
}
private
function
parseMercurialHeads
(
$raw
)
{
$heads_map
=
$this
->
parseMercurialCommits
(
$raw
)
;
$heads
=
array
(
)
;
foreach
(
$heads_map
as
$commit
=>
$branches
)
{
foreach
(
$branches
as
$branch
)
{
$heads
[
$branch
]
[
]
=
$commit
;
}
}
return
$heads
;
}
/* -( Subversion )--------------------------------------------------------- */
private
function
findSubversionRefUpdates
(
)
{
// Subversion doesn't have any kind of mutable ref metadata.
return
array
(
)
;
}
private
function
findSubversionContentUpdates
(
array
$ref_updates
)
{
list
(
$youngest
)
=
execx
(
'svnlook youngest %s'
,
$this
->
subversionRepository
)
;
$ref_new
=
(int)
$youngest
+
1
;
$ref_flags
=
0
;
$ref_flags
|=
PhabricatorRepositoryPushLog
::
CHANGEFLAG_ADD
;
$ref_flags
|=
PhabricatorRepositoryPushLog
::
CHANGEFLAG_APPEND
;
$ref_content
=
$this
->
newPushLog
(
)
->
setRefType
(
PhabricatorRepositoryPushLog
::
REFTYPE_COMMIT
)
->
setRefNew
(
$ref_new
)
->
setChangeFlags
(
$ref_flags
)
;
return
array
(
$ref_content
)
;
}
/* -( Internals )---------------------------------------------------------- */
private
function
newPushLog
(
)
{
// NOTE: We generate PHIDs up front so the Herald transcripts can pick them
// up.
$phid
=
id
(
new
PhabricatorRepositoryPushLog
(
)
)
->
generatePHID
(
)
;
$device
=
AlmanacKeys
::
getLiveDevice
(
)
;
if
(
$device
)
{
$device_phid
=
$device
->
getPHID
(
)
;
}
else
{
$device_phid
=
null
;
}
return
PhabricatorRepositoryPushLog
::
initializeNewLog
(
$this
->
getViewer
(
)
)
->
setPHID
(
$phid
)
->
setDevicePHID
(
$device_phid
)
->
setRepositoryPHID
(
$this
->
getRepository
(
)
->
getPHID
(
)
)
->
attachRepository
(
$this
->
getRepository
(
)
)
->
setEpoch
(
PhabricatorTime
::
getNow
(
)
)
;
}
private
function
newPushEvent
(
)
{
$viewer
=
$this
->
getViewer
(
)
;
$hook_start
=
$this
->
getStartTime
(
)
;
$event
=
PhabricatorRepositoryPushEvent
::
initializeNewEvent
(
$viewer
)
->
setRepositoryPHID
(
$this
->
getRepository
(
)
->
getPHID
(
)
)
->
setRemoteAddress
(
$this
->
getRemoteAddress
(
)
)
->
setRemoteProtocol
(
$this
->
getRemoteProtocol
(
)
)
->
setEpoch
(
PhabricatorTime
::
getNow
(
)
)
->
setHookWait
(
phutil_microseconds_since
(
$hook_start
)
)
;
$identifier
=
$this
->
getRequestIdentifier
(
)
;
if
(
strlen
(
$identifier
)
)
{
$event
->
setRequestIdentifier
(
$identifier
)
;
}
return
$event
;
}
private
function
rejectEnormousChanges
(
array
$content_updates
)
{
$repository
=
$this
->
getRepository
(
)
;
if
(
$repository
->
shouldAllowEnormousChanges
(
)
)
{
return
;
}
// See T13142. Don't cache more than 64MB of changesets. For normal small
// pushes, caching everything here can let us hit the cache from Herald if
// we need to run content rules, which speeds things up a bit. For large
// pushes, we may not be able to hold everything in memory.
$cache_limit
=
1024
*
1024
*
64
;
foreach
(
$content_updates
as
$update
)
{
$identifier
=
$update
->
getRefNew
(
)
;
try
{
$info
=
$this
->
loadChangesetsForCommit
(
$identifier
)
;
list
(
$changesets
,
$size
)
=
$info
;
if
(
$this
->
changesetsSize
+
$size
<=
$cache_limit
)
{
$this
->
changesets
[
$identifier
]
=
$changesets
;
$this
->
changesetsSize
+=
$size
;
}
}
catch
(
Exception
$ex
)
{
$this
->
changesets
[
$identifier
]
=
$ex
;
$message
=
pht
(
'ENORMOUS CHANGE'
.
"\n"
.
'Enormous change protection is enabled for this repository, but '
.
'you are pushing an enormous change ("%s"). Edit the repository '
.
'configuration before making enormous changes.'
.
"\n\n"
.
"Content Exception: %s"
,
$identifier
,
$ex
->
getMessage
(
)
)
;
throw
new
DiffusionCommitHookRejectException
(
$message
)
;
}
}
}
private
function
loadChangesetsForCommit
(
$identifier
)
{
$byte_limit
=
HeraldCommitAdapter
::
getEnormousByteLimit
(
)
;
$time_limit
=
HeraldCommitAdapter
::
getEnormousTimeLimit
(
)
;
$vcs
=
$this
->
getRepository
(
)
->
getVersionControlSystem
(
)
;
switch
(
$vcs
)
{
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
:
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_MERCURIAL
:
// For git and hg, we can use normal commands.
$drequest
=
DiffusionRequest
::
newFromDictionary
(
array
(
'repository'
=>
$this
->
getRepository
(
)
,
'user'
=>
$this
->
getViewer
(
)
,
'commit'
=>
$identifier
,
)
)
;
$raw_diff
=
DiffusionRawDiffQuery
::
newFromDiffusionRequest
(
$drequest
)
->
setTimeout
(
$time_limit
)
->
setByteLimit
(
$byte_limit
)
->
setLinesOfContext
(
0
)
->
executeInline
(
)
;
break
;
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_SVN
:
// TODO: This diff has 3 lines of context, which produces slightly
// incorrect "added file content" and "removed file content" results.
// This may also choke on binaries, but "svnlook diff" does not support
// the "--diff-cmd" flag.
// For subversion, we need to use `svnlook`.
$future
=
new
ExecFuture
(
'svnlook diff -t %s %s'
,
$this
->
subversionTransaction
,
$this
->
subversionRepository
)
;
$future
->
setTimeout
(
$time_limit
)
;
$future
->
setStdoutSizeLimit
(
$byte_limit
)
;
$future
->
setStderrSizeLimit
(
$byte_limit
)
;
list
(
$raw_diff
)
=
$future
->
resolvex
(
)
;
break
;
default
:
throw
new
Exception
(
pht
(
"Unknown VCS '%s!'"
,
$vcs
)
)
;
}
if
(
strlen
(
$raw_diff
)
>=
$byte_limit
)
{
throw
new
Exception
(
pht
(
'The raw text of this change ("%s") is enormous (larger than %s '
.
'bytes).'
,
$identifier
,
new
PhutilNumber
(
$byte_limit
)
)
)
;
}
if
(
!
strlen
(
$raw_diff
)
)
{
// If the commit is actually empty, just return no changesets.
return
array
(
array
(
)
,
0
)
;
}
$parser
=
new
ArcanistDiffParser
(
)
;
$changes
=
$parser
->
parseDiff
(
$raw_diff
)
;
$diff
=
DifferentialDiff
::
newEphemeralFromRawChanges
(
$changes
)
;
$changesets
=
$diff
->
getChangesets
(
)
;
$size
=
strlen
(
$raw_diff
)
;
return
array
(
$changesets
,
$size
)
;
}
public
function
getChangesetsForCommit
(
$identifier
)
{
if
(
isset
(
$this
->
changesets
[
$identifier
]
)
)
{
$cached
=
$this
->
changesets
[
$identifier
]
;
if
(
$cached
instanceof
Exception
)
{
throw
$cached
;
}
return
$cached
;
}
$info
=
$this
->
loadChangesetsForCommit
(
$identifier
)
;
list
(
$changesets
,
$size
)
=
$info
;
return
$changesets
;
}
private
function
rejectOversizedFiles
(
array
$content_updates
)
{
$repository
=
$this
->
getRepository
(
)
;
$limit
=
$repository
->
getFilesizeLimit
(
)
;
if
(
!
$limit
)
{
return
;
}
foreach
(
$content_updates
as
$update
)
{
$identifier
=
$update
->
getRefNew
(
)
;
$sizes
=
$this
->
getFileSizesForCommit
(
$identifier
)
;
foreach
(
$sizes
as
$path
=>
$size
)
{
if
(
$size
<=
$limit
)
{
continue
;
}
$message
=
pht
(
'OVERSIZED FILE'
.
"\n"
.
'This repository ("%s") is configured with a maximum individual '
.
'file size limit, but you are pushing a change ("%s") which causes '
.
'the size of a file ("%s") to exceed the limit. The commit makes '
.
'the file %s bytes long, but the limit for this repository is '
.
'%s bytes.'
,
$repository
->
getDisplayName
(
)
,
$identifier
,
$path
,
new
PhutilNumber
(
$size
)
,
new
PhutilNumber
(
$limit
)
)
;
throw
new
DiffusionCommitHookRejectException
(
$message
)
;
}
}
}
private
function
rejectCommitsAffectingTooManyPaths
(
array
$content_updates
)
{
$repository
=
$this
->
getRepository
(
)
;
$limit
=
$repository
->
getTouchLimit
(
)
;
if
(
!
$limit
)
{
return
;
}
foreach
(
$content_updates
as
$update
)
{
$identifier
=
$update
->
getRefNew
(
)
;
$sizes
=
$this
->
getFileSizesForCommit
(
$identifier
)
;
if
(
count
(
$sizes
)
>
$limit
)
{
$message
=
pht
(
'COMMIT AFFECTS TOO MANY PATHS'
.
"\n"
.
'This repository ("%s") is configured with a touched files limit '
.
'that caps the maximum number of paths any single commit may '
.
'affect. You are pushing a change ("%s") which exceeds this '
.
'limit: it affects %s paths, but the largest number of paths any '
.
'commit may affect is %s paths.'
,
$repository
->
getDisplayName
(
)
,
$identifier
,
phutil_count
(
$sizes
)
,
new
PhutilNumber
(
$limit
)
)
;
throw
new
DiffusionCommitHookRejectException
(
$message
)
;
}
}
}
public
function
getFileSizesForCommit
(
$identifier
)
{
if
(
!
isset
(
$this
->
filesizeCache
[
$identifier
]
)
)
{
$file_sizes
=
$this
->
loadFileSizesForCommit
(
$identifier
)
;
$this
->
filesizeCache
[
$identifier
]
=
$file_sizes
;
}
return
$this
->
filesizeCache
[
$identifier
]
;
}
private
function
loadFileSizesForCommit
(
$identifier
)
{
$repository
=
$this
->
getRepository
(
)
;
return
id
(
new
DiffusionLowLevelFilesizeQuery
(
)
)
->
setRepository
(
$repository
)
->
withIdentifier
(
$identifier
)
->
execute
(
)
;
}
public
function
loadCommitRefForCommit
(
$identifier
)
{
$repository
=
$this
->
getRepository
(
)
;
$vcs
=
$repository
->
getVersionControlSystem
(
)
;
switch
(
$vcs
)
{
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
:
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_MERCURIAL
:
return
id
(
new
DiffusionLowLevelCommitQuery
(
)
)
->
setRepository
(
$repository
)
->
withIdentifier
(
$identifier
)
->
execute
(
)
;
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_SVN
:
// For subversion, we need to use `svnlook`.
list
(
$message
)
=
execx
(
'svnlook log -t %s %s'
,
$this
->
subversionTransaction
,
$this
->
subversionRepository
)
;
return
id
(
new
DiffusionCommitRef
(
)
)
->
setMessage
(
$message
)
;
break
;
default
:
throw
new
Exception
(
pht
(
"Unknown VCS '%s!'"
,
$vcs
)
)
;
}
}
public
function
loadBranches
(
$identifier
)
{
$repository
=
$this
->
getRepository
(
)
;
$vcs
=
$repository
->
getVersionControlSystem
(
)
;
switch
(
$vcs
)
{
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
:
return
idx
(
$this
->
gitCommits
,
$identifier
,
array
(
)
)
;
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_MERCURIAL
:
// NOTE: This will be "the branch the commit was made to", not
// "a list of all branch heads which descend from the commit".
// This is consistent with Mercurial, but possibly confusing.
return
idx
(
$this
->
mercurialCommits
,
$identifier
,
array
(
)
)
;
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_SVN
:
// Subversion doesn't have branches.
return
array
(
)
;
}
}
private
function
loadCommitInfoForWorker
(
array
$all_updates
)
{
$type_commit
=
PhabricatorRepositoryPushLog
::
REFTYPE_COMMIT
;
$map
=
array
(
)
;
foreach
(
$all_updates
as
$update
)
{
if
(
$update
->
getRefType
(
)
!=
$type_commit
)
{
continue
;
}
$map
[
$update
->
getRefNew
(
)
]
=
array
(
)
;
}
foreach
(
$map
as
$identifier
=>
$info
)
{
$ref
=
$this
->
loadCommitRefForCommit
(
$identifier
)
;
$map
[
$identifier
]
+=
array
(
'summary'
=>
$ref
->
getSummary
(
)
,
'branches'
=>
$this
->
loadBranches
(
$identifier
)
,
)
;
}
return
$map
;
}
private
function
isInitialImport
(
array
$all_updates
)
{
$repository
=
$this
->
getRepository
(
)
;
$vcs
=
$repository
->
getVersionControlSystem
(
)
;
switch
(
$vcs
)
{
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_SVN
:
// There is no meaningful way to import history into Subversion by
// pushing.
return
false
;
default
:
break
;
}
// Now, apply a heuristic to guess whether this is a normal commit or
// an initial import. We guess something is an initial import if:
//
// - the repository is currently empty; and
// - it pushes more than 7 commits at once.
//
// The number "7" is chosen arbitrarily as seeming reasonable. We could
// also look at author data (do the commits come from multiple different
// authors?) and commit date data (is the oldest commit more than 48 hours
// old), but we don't have immediate access to those and this simple
// heuristic might be good enough.
$commit_count
=
0
;
$type_commit
=
PhabricatorRepositoryPushLog
::
REFTYPE_COMMIT
;
foreach
(
$all_updates
as
$update
)
{
if
(
$update
->
getRefType
(
)
!=
$type_commit
)
{
continue
;
}
$commit_count
++
;
}
if
(
$commit_count
<=
PhabricatorRepository
::
IMPORT_THRESHOLD
)
{
// If this pushes a very small number of commits, assume it's an
// initial commit or stack of a few initial commits.
return
false
;
}
$any_commits
=
id
(
new
DiffusionCommitQuery
(
)
)
->
setViewer
(
$this
->
getViewer
(
)
)
->
withRepository
(
$repository
)
->
setLimit
(
1
)
->
execute
(
)
;
if
(
$any_commits
)
{
// If the repository already has commits, this isn't an import.
return
false
;
}
return
true
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sun, Jan 19, 20:24 (1 w, 5 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1128420
Default Alt Text
DiffusionCommitHookEngine.php (47 KB)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment