Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php
index 667e2013b2..2b4a9ac33a 100644
--- a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php
+++ b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php
@@ -1,977 +1,1006 @@
<?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_USER = 'PHABRICATOR_USER';
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 $subversionTransaction;
private $subversionRepository;
private $remoteAddress;
private $remoteProtocol;
private $transactionKey;
private $mercurialHook;
private $mercurialCommits = array();
+ private $gitCommits = array();
private $heraldViewerProjects;
/* -( 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;
}
private function getRemoteAddressForLog() {
// If whatever we have here isn't a valid IPv4 address, just store `null`.
// Older versions of PHP return `-1` on failure instead of `false`.
$remote_address = $this->getRemoteAddress();
$remote_address = max(0, ip2long($remote_address));
$remote_address = nonempty($remote_address, null);
return $remote_address;
}
private function getTransactionKey() {
if (!$this->transactionKey) {
$entropy = Filesystem::readRandomBytes(64);
$this->transactionKey = PhabricatorHash::digestForIndex($entropy);
}
return $this->transactionKey;
}
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 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.
foreach ($all_updates as $update) {
$update->setRejectCode(
PhabricatorRepositoryPushLog::REJECT_DANGEROUS);
}
throw $ex;
}
$this->applyHeraldRefRules($ref_updates, $all_updates);
$content_updates = $this->findContentUpdates($ref_updates);
$all_updates = array_merge($all_updates, $content_updates);
$this->applyHeraldContentRules($content_updates, $all_updates);
// TODO: Fire external hooks.
// If we make it this far, we're accepting these changes. Mark all the
// logs as accepted.
foreach ($all_updates as $update) {
$update->setRejectCode(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.
foreach ($all_updates as $update) {
$update->save();
}
if ($caught) {
throw $caught;
}
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,
array $all_updates) {
$this->applyHeraldRules(
$ref_updates,
new HeraldPreCommitRefAdapter(),
$all_updates);
}
private function applyHeraldContentRules(
array $content_updates,
array $all_updates) {
$this->applyHeraldRules(
$content_updates,
new HeraldPreCommitContentAdapter(),
$all_updates);
}
private function applyHeraldRules(
array $updates,
HeraldAdapter $adapter_template,
array $all_updates) {
if (!$updates) {
return;
}
$adapter_template->setHookEngine($this);
$engine = new HeraldEngine();
$rules = null;
$blocking_effect = null;
$blocked_update = 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();
if ($blocking_effect === null) {
foreach ($effects as $effect) {
if ($effect->getAction() == HeraldAdapter::ACTION_BLOCK) {
$blocking_effect = $effect;
$blocked_update = $update;
break;
}
}
}
}
if ($blocking_effect) {
foreach ($all_updates as $update) {
$update->setRejectCode(PhabricatorRepositoryPushLog::REJECT_HERALD);
$update->setRejectDetails($blocking_effect->getRulePHID());
}
$message = $blocking_effect->getTarget();
if (!strlen($message)) {
$message = pht('(None.)');
}
$rules = mpull($rules, null, 'getID');
$rule = idx($rules, $effect->getRuleID());
if ($rule && strlen($rule->getName())) {
$rule_name = $rule->getName();
} else {
$rule_name = pht('Unnamed Herald Rule');
}
$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",
'H'.$blocking_effect->getRuleID(),
$blocked_name,
$rule_name,
$message));
}
}
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 {
throw new Exception(
pht(
"Unable to identify the reftype of '%s'. Rejecting push.",
$ref_raw));
}
$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);
}
foreach (Futures($futures)->limit(8) 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_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 --format=%s %s --not --all',
'%H',
$ref_update->getRefNew());
}
$content_updates = array();
foreach (Futures($futures)->limit(8) 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;
}
/* -( 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 HG_NODE in environment!'));
}
// 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{branches}\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 --rev tip --template %s",
hgsprintf('%s', $hg_node),
'{node}\1{branches}\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 (Futures($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) {
// No changes to this branch, so skip it.
continue;
}
if (!$new_heads) {
if ($old_heads) {
// It looks like this push deletes a branch, but that isn't possible
// in Mercurial, so something is going wrong here. Bail out.
throw new Exception(
pht(
'Mercurial repository has no new head for branch "%s" after '.
'push. This is unexpected; rejecting change.'));
} else {
// Obviously, this should never be possible either, 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.'));
}
}
$stray_heads = array();
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.
$dfutures = array();
foreach ($old_heads as $old_head) {
$dfutures[$old_head] = $repository->getLocalCommandFuture(
'log --rev %s --template %s',
hgsprintf('(descendants(%s) and head())', $old_head),
'{node}\1');
}
$head_map = array();
foreach (Futures($dfutures) as $future_head => $dfuture) {
list($stdout) = $dfuture->resolvex();
$head_map[$future_head] = array_filter(explode("\1", $stdout));
}
// 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 (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;
}
$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));
}
}
$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, $branches_raw) = explode("\1", $commit_line);
if (!strlen($branches_raw)) {
$branches = array('default');
} else {
$branches = explode(' ', $branches_raw);
}
$commit_map[$node] = $branches;
}
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: By default, we create these with REJECT_BROKEN as the reject
// code. This indicates a broken hook, and covers the case where we
// encounter some unexpected exception and consequently reject the changes.
// NOTE: We generate PHIDs up front so the Herald transcripts can pick them
// up.
$phid = id(new PhabricatorRepositoryPushLog())->generatePHID();
return PhabricatorRepositoryPushLog::initializeNewLog($this->getViewer())
->setPHID($phid)
->attachRepository($this->getRepository())
->setRepositoryPHID($this->getRepository()->getPHID())
->setEpoch(time())
->setRemoteAddress($this->getRemoteAddressForLog())
->setRemoteProtocol($this->getRemoteProtocol())
->setTransactionKey($this->getTransactionKey())
->setRejectCode(PhabricatorRepositoryPushLog::REJECT_BROKEN)
->setRejectDetails(null);
}
public function loadChangesetsForCommit($identifier) {
$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(5 * 60)
->setLinesOfContext(0)
->loadRawDiff();
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`.
list($raw_diff) = execx(
'svnlook diff -t %s %s',
$this->subversionTransaction,
$this->subversionRepository);
break;
default:
throw new Exception(pht("Unknown VCS '%s!'", $vcs));
}
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($raw_diff);
$diff = DifferentialDiff::newFromRawChanges($changes);
return $diff->getChangesets();
}
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:
+ return idx($this->mercurialCommits, $identifier, array());
+ case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
+ // Subversion doesn't have branches.
+ return array();
+ }
+ }
+
}
diff --git a/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php b/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php
index aee68f126d..95cd112fad 100644
--- a/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php
+++ b/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php
@@ -1,332 +1,339 @@
<?php
final class HeraldPreCommitContentAdapter extends HeraldAdapter {
private $log;
private $hookEngine;
private $changesets;
private $commitRef;
private $fields;
private $revision = false;
public function setPushLog(PhabricatorRepositoryPushLog $log) {
$this->log = $log;
return $this;
}
public function setHookEngine(DiffusionCommitHookEngine $engine) {
$this->hookEngine = $engine;
return $this;
}
public function getAdapterApplicationClass() {
return 'PhabricatorApplicationDiffusion';
}
public function getObject() {
return $this->log;
}
public function getAdapterContentName() {
return pht('Commit Hook: Commit Content');
}
public function getFieldNameMap() {
return array(
) + parent::getFieldNameMap();
}
public function getFields() {
return array_merge(
array(
self::FIELD_BODY,
self::FIELD_AUTHOR,
self::FIELD_COMMITTER,
+ self::FIELD_BRANCHES,
self::FIELD_DIFF_FILE,
self::FIELD_DIFF_CONTENT,
self::FIELD_DIFF_ADDED_CONTENT,
self::FIELD_DIFF_REMOVED_CONTENT,
self::FIELD_REPOSITORY,
self::FIELD_PUSHER,
self::FIELD_PUSHER_PROJECTS,
self::FIELD_DIFFERENTIAL_REVISION,
self::FIELD_DIFFERENTIAL_ACCEPTED,
self::FIELD_DIFFERENTIAL_REVIEWERS,
self::FIELD_DIFFERENTIAL_CCS,
self::FIELD_IS_MERGE_COMMIT,
self::FIELD_RULE,
),
parent::getFields());
}
public function getConditionsForField($field) {
switch ($field) {
}
return parent::getConditionsForField($field);
}
public function getActions($rule_type) {
switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
return array(
self::ACTION_BLOCK,
self::ACTION_NOTHING
);
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
return array(
self::ACTION_NOTHING,
);
}
}
public function getValueTypeForFieldAndCondition($field, $condition) {
return parent::getValueTypeForFieldAndCondition($field, $condition);
}
public function getPHID() {
return $this->getObject()->getPHID();
}
public function getHeraldName() {
return pht('Push Log');
}
public function getHeraldField($field) {
$log = $this->getObject();
switch ($field) {
case self::FIELD_BODY:
return $this->getCommitRef()->getMessage();
case self::FIELD_AUTHOR:
return $this->getAuthorPHID();
case self::FIELD_COMMITTER:
return $this->getCommitterPHID();
+ case self::FIELD_BRANCHES:
+ return $this->getBranches();
case self::FIELD_DIFF_FILE:
return $this->getDiffContent('name');
case self::FIELD_DIFF_CONTENT:
return $this->getDiffContent('*');
case self::FIELD_DIFF_ADDED_CONTENT:
return $this->getDiffContent('+');
case self::FIELD_DIFF_REMOVED_CONTENT:
return $this->getDiffContent('-');
case self::FIELD_REPOSITORY:
return $this->hookEngine->getRepository()->getPHID();
case self::FIELD_PUSHER:
return $this->hookEngine->getViewer()->getPHID();
case self::FIELD_PUSHER_PROJECTS:
return $this->hookEngine->loadViewerProjectPHIDsForHerald();
case self::FIELD_DIFFERENTIAL_REVISION:
$revision = $this->getRevision();
if (!$revision) {
return null;
}
return $revision->getPHID();
case self::FIELD_DIFFERENTIAL_ACCEPTED:
$revision = $this->getRevision();
if (!$revision) {
return null;
}
$status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED;
if ($revision->getStatus() != $status_accepted) {
return null;
}
return $revision->getPHID();
case self::FIELD_DIFFERENTIAL_REVIEWERS:
$revision = $this->getRevision();
if (!$revision) {
return array();
}
return $revision->getReviewers();
case self::FIELD_DIFFERENTIAL_CCS:
$revision = $this->getRevision();
if (!$revision) {
return array();
}
return $revision->getCCPHIDs();
case self::FIELD_IS_MERGE_COMMIT:
return $this->getIsMergeCommit();
}
return parent::getHeraldField($field);
}
public function applyHeraldEffects(array $effects) {
assert_instances_of($effects, 'HeraldEffect');
$result = array();
foreach ($effects as $effect) {
$action = $effect->getAction();
switch ($action) {
case self::ACTION_NOTHING:
$result[] = new HeraldApplyTranscript(
$effect,
true,
pht('Did nothing.'));
break;
case self::ACTION_BLOCK:
$result[] = new HeraldApplyTranscript(
$effect,
true,
pht('Blocked push.'));
break;
default:
throw new Exception(pht('No rules to handle action "%s"!', $action));
}
}
return $result;
}
private function getDiffContent($type) {
if ($this->changesets === null) {
try {
$this->changesets = $this->hookEngine->loadChangesetsForCommit(
$this->log->getRefNew());
} catch (Exception $ex) {
$this->changesets = $ex;
}
}
if ($this->changesets instanceof Exception) {
$ex_class = get_class($this->changesets);
$ex_message = $this->changesets->getmessage();
if ($type === 'name') {
return array("<{$ex_class}: {$ex_message}>");
} else {
return array("<{$ex_class}>" => $ex_message);
}
}
$result = array();
if ($type === 'name') {
foreach ($this->changesets as $change) {
$result[] = $change->getFilename();
}
} else {
foreach ($this->changesets as $change) {
$lines = array();
foreach ($change->getHunks() as $hunk) {
switch ($type) {
case '-':
$lines[] = $hunk->makeOldFile();
break;
case '+':
$lines[] = $hunk->makeNewFile();
break;
case '*':
default:
$lines[] = $hunk->makeChanges();
break;
}
}
$result[$change->getFilename()] = implode('', $lines);
}
}
return $result;
}
private function getCommitRef() {
if ($this->commitRef === null) {
$this->commitRef = $this->hookEngine->loadCommitRefForCommit(
$this->log->getRefNew());
}
return $this->commitRef;
}
private function getAuthorPHID() {
$repository = $this->hookEngine->getRepository();
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$ref = $this->getCommitRef();
$author = $ref->getAuthor();
if (!strlen($author)) {
return null;
}
return $this->lookupUser($author);
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// In Subversion, the pusher is always the author.
return $this->hookEngine->getViewer()->getPHID();
}
}
private function getCommitterPHID() {
$repository = $this->hookEngine->getRepository();
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
// Here, if there's no committer, we're going to return the author
// instead.
$ref = $this->getCommitRef();
$committer = $ref->getCommitter();
if (!strlen($committer)) {
return $this->getAuthorPHID();
}
$phid = $this->lookupUser($committer);
if (!$phid) {
return $this->getAuthorPHID();
}
return $phid;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// In Subversion, the pusher is always the committer.
return $this->hookEngine->getViewer()->getPHID();
}
}
private function lookupUser($author) {
return id(new DiffusionResolveUserQuery())
->withName($author)
->execute();
}
private function getCommitFields() {
if ($this->fields === null) {
$this->fields = id(new DiffusionLowLevelCommitFieldsQuery())
->setRepository($this->hookEngine->getRepository())
->withCommitRef($this->getCommitRef())
->execute();
}
return $this->fields;
}
private function getRevision() {
if ($this->revision === false) {
$fields = $this->getCommitFields();
$revision_id = idx($fields, 'revisionID');
if (!$revision_id) {
$this->revision = null;
} else {
$this->revision = id(new DifferentialRevisionQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIDs(array($revision_id))
->needRelationships(true)
->executeOne();
}
}
return $this->revision;
}
private function getIsMergeCommit() {
$repository = $this->hookEngine->getRepository();
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$parents = id(new DiffusionLowLevelParentsQuery())
->setRepository($repository)
->withIdentifier($this->log->getRefNew())
->execute();
return (count($parents) > 1);
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// NOTE: For now, we ignore "svn:mergeinfo" at all levels. We might
// change this some day, but it's not nearly as clear a signal as
// ancestry is in Git/Mercurial.
return false;
}
}
+ private function getBranches() {
+ return $this->hookEngine->loadBranches($this->log->getRefNew());
+ }
+
}
diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php
index e05b57f562..e71f60d96c 100644
--- a/src/applications/herald/adapter/HeraldAdapter.php
+++ b/src/applications/herald/adapter/HeraldAdapter.php
@@ -1,984 +1,987 @@
<?php
/**
* @group herald
*/
abstract class HeraldAdapter {
const FIELD_TITLE = 'title';
const FIELD_BODY = 'body';
const FIELD_AUTHOR = 'author';
const FIELD_ASSIGNEE = 'assignee';
const FIELD_REVIEWER = 'reviewer';
const FIELD_REVIEWERS = 'reviewers';
const FIELD_COMMITTER = 'committer';
const FIELD_CC = 'cc';
const FIELD_TAGS = 'tags';
const FIELD_DIFF_FILE = 'diff-file';
const FIELD_DIFF_CONTENT = 'diff-content';
const FIELD_DIFF_ADDED_CONTENT = 'diff-added-content';
const FIELD_DIFF_REMOVED_CONTENT = 'diff-removed-content';
const FIELD_REPOSITORY = 'repository';
const FIELD_RULE = 'rule';
const FIELD_AFFECTED_PACKAGE = 'affected-package';
const FIELD_AFFECTED_PACKAGE_OWNER = 'affected-package-owner';
const FIELD_CONTENT_SOURCE = 'contentsource';
const FIELD_ALWAYS = 'always';
const FIELD_AUTHOR_PROJECTS = 'authorprojects';
const FIELD_PROJECTS = 'projects';
const FIELD_PUSHER = 'pusher';
const FIELD_PUSHER_PROJECTS = 'pusher-projects';
const FIELD_DIFFERENTIAL_REVISION = 'differential-revision';
const FIELD_DIFFERENTIAL_REVIEWERS = 'differential-reviewers';
const FIELD_DIFFERENTIAL_CCS = 'differential-ccs';
const FIELD_DIFFERENTIAL_ACCEPTED = 'differential-accepted';
const FIELD_IS_MERGE_COMMIT = 'is-merge-commit';
+ const FIELD_BRANCHES = 'branches';
const CONDITION_CONTAINS = 'contains';
const CONDITION_NOT_CONTAINS = '!contains';
const CONDITION_IS = 'is';
const CONDITION_IS_NOT = '!is';
const CONDITION_IS_ANY = 'isany';
const CONDITION_IS_NOT_ANY = '!isany';
const CONDITION_INCLUDE_ALL = 'all';
const CONDITION_INCLUDE_ANY = 'any';
const CONDITION_INCLUDE_NONE = 'none';
const CONDITION_IS_ME = 'me';
const CONDITION_IS_NOT_ME = '!me';
const CONDITION_REGEXP = 'regexp';
const CONDITION_RULE = 'conditions';
const CONDITION_NOT_RULE = '!conditions';
const CONDITION_EXISTS = 'exists';
const CONDITION_NOT_EXISTS = '!exists';
const CONDITION_UNCONDITIONALLY = 'unconditionally';
const CONDITION_REGEXP_PAIR = 'regexp-pair';
const CONDITION_HAS_BIT = 'bit';
const CONDITION_NOT_BIT = '!bit';
const CONDITION_IS_TRUE = 'true';
const CONDITION_IS_FALSE = 'false';
const ACTION_ADD_CC = 'addcc';
const ACTION_REMOVE_CC = 'remcc';
const ACTION_EMAIL = 'email';
const ACTION_NOTHING = 'nothing';
const ACTION_AUDIT = 'audit';
const ACTION_FLAG = 'flag';
const ACTION_ASSIGN_TASK = 'assigntask';
const ACTION_ADD_PROJECTS = 'addprojects';
const ACTION_ADD_REVIEWERS = 'addreviewers';
const ACTION_ADD_BLOCKING_REVIEWERS = 'addblockingreviewers';
const ACTION_APPLY_BUILD_PLANS = 'applybuildplans';
const ACTION_BLOCK = 'block';
const VALUE_TEXT = 'text';
const VALUE_NONE = 'none';
const VALUE_EMAIL = 'email';
const VALUE_USER = 'user';
const VALUE_TAG = 'tag';
const VALUE_RULE = 'rule';
const VALUE_REPOSITORY = 'repository';
const VALUE_OWNERS_PACKAGE = 'package';
const VALUE_PROJECT = 'project';
const VALUE_FLAG_COLOR = 'flagcolor';
const VALUE_CONTENT_SOURCE = 'contentsource';
const VALUE_USER_OR_PROJECT = 'userorproject';
const VALUE_BUILD_PLAN = 'buildplan';
private $contentSource;
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function getContentSource() {
return $this->contentSource;
}
abstract public function getPHID();
abstract public function getHeraldName();
public function getHeraldField($field_name) {
switch ($field_name) {
case self::FIELD_RULE:
return null;
case self::FIELD_CONTENT_SOURCE:
return $this->getContentSource()->getSource();
case self::FIELD_ALWAYS:
return true;
default:
throw new Exception(
"Unknown field '{$field_name}'!");
}
}
abstract public function applyHeraldEffects(array $effects);
public function isAvailableToUser(PhabricatorUser $viewer) {
$applications = id(new PhabricatorApplicationQuery())
->setViewer($viewer)
->withInstalled(true)
->withClasses(array($this->getAdapterApplicationClass()))
->execute();
return !empty($applications);
}
/**
* NOTE: You generally should not override this; it exists to support legacy
* adapters which had hard-coded content types.
*/
public function getAdapterContentType() {
return get_class($this);
}
abstract public function getAdapterContentName();
abstract public function getAdapterApplicationClass();
abstract public function getObject();
/* -( Fields )------------------------------------------------------------- */
public function getFields() {
return array(
self::FIELD_ALWAYS,
);
}
public function getFieldNameMap() {
return array(
self::FIELD_TITLE => pht('Title'),
self::FIELD_BODY => pht('Body'),
self::FIELD_AUTHOR => pht('Author'),
self::FIELD_ASSIGNEE => pht('Assignee'),
self::FIELD_COMMITTER => pht('Committer'),
self::FIELD_REVIEWER => pht('Reviewer'),
self::FIELD_REVIEWERS => pht('Reviewers'),
self::FIELD_CC => pht('CCs'),
self::FIELD_TAGS => pht('Tags'),
self::FIELD_DIFF_FILE => pht('Any changed filename'),
self::FIELD_DIFF_CONTENT => pht('Any changed file content'),
self::FIELD_DIFF_ADDED_CONTENT => pht('Any added file content'),
self::FIELD_DIFF_REMOVED_CONTENT => pht('Any removed file content'),
self::FIELD_REPOSITORY => pht('Repository'),
self::FIELD_RULE => pht('Another Herald rule'),
self::FIELD_AFFECTED_PACKAGE => pht('Any affected package'),
self::FIELD_AFFECTED_PACKAGE_OWNER =>
pht("Any affected package's owner"),
self::FIELD_CONTENT_SOURCE => pht('Content Source'),
self::FIELD_ALWAYS => pht('Always'),
self::FIELD_AUTHOR_PROJECTS => pht("Author's projects"),
self::FIELD_PROJECTS => pht("Projects"),
self::FIELD_PUSHER => pht('Pusher'),
self::FIELD_PUSHER_PROJECTS => pht("Pusher's projects"),
self::FIELD_DIFFERENTIAL_REVISION => pht('Differential revision'),
self::FIELD_DIFFERENTIAL_REVIEWERS => pht('Differential reviewers'),
self::FIELD_DIFFERENTIAL_CCS => pht('Differential CCs'),
self::FIELD_DIFFERENTIAL_ACCEPTED
=> pht('Accepted Differential revision'),
self::FIELD_IS_MERGE_COMMIT => pht('Commit is a merge'),
+ self::FIELD_BRANCHES => pht('Commit\'s branches'),
);
}
/* -( Conditions )--------------------------------------------------------- */
public function getConditionNameMap() {
return array(
self::CONDITION_CONTAINS => pht('contains'),
self::CONDITION_NOT_CONTAINS => pht('does not contain'),
self::CONDITION_IS => pht('is'),
self::CONDITION_IS_NOT => pht('is not'),
self::CONDITION_IS_ANY => pht('is any of'),
self::CONDITION_IS_TRUE => pht('is true'),
self::CONDITION_IS_FALSE => pht('is false'),
self::CONDITION_IS_NOT_ANY => pht('is not any of'),
self::CONDITION_INCLUDE_ALL => pht('include all of'),
self::CONDITION_INCLUDE_ANY => pht('include any of'),
self::CONDITION_INCLUDE_NONE => pht('do not include'),
self::CONDITION_IS_ME => pht('is myself'),
self::CONDITION_IS_NOT_ME => pht('is not myself'),
self::CONDITION_REGEXP => pht('matches regexp'),
self::CONDITION_RULE => pht('matches:'),
self::CONDITION_NOT_RULE => pht('does not match:'),
self::CONDITION_EXISTS => pht('exists'),
self::CONDITION_NOT_EXISTS => pht('does not exist'),
self::CONDITION_UNCONDITIONALLY => '', // don't show anything!
self::CONDITION_REGEXP_PAIR => pht('matches regexp pair'),
self::CONDITION_HAS_BIT => pht('has bit'),
self::CONDITION_NOT_BIT => pht('lacks bit'),
);
}
public function getConditionsForField($field) {
switch ($field) {
case self::FIELD_TITLE:
case self::FIELD_BODY:
return array(
self::CONDITION_CONTAINS,
self::CONDITION_NOT_CONTAINS,
self::CONDITION_IS,
self::CONDITION_IS_NOT,
self::CONDITION_REGEXP,
);
case self::FIELD_AUTHOR:
case self::FIELD_COMMITTER:
case self::FIELD_REVIEWER:
case self::FIELD_PUSHER:
return array(
self::CONDITION_IS_ANY,
self::CONDITION_IS_NOT_ANY,
);
case self::FIELD_REPOSITORY:
case self::FIELD_ASSIGNEE:
return array(
self::CONDITION_IS_ANY,
self::CONDITION_IS_NOT_ANY,
self::CONDITION_EXISTS,
self::CONDITION_NOT_EXISTS,
);
case self::FIELD_TAGS:
case self::FIELD_REVIEWERS:
case self::FIELD_CC:
case self::FIELD_AUTHOR_PROJECTS:
case self::FIELD_PROJECTS:
case self::FIELD_AFFECTED_PACKAGE:
case self::FIELD_AFFECTED_PACKAGE_OWNER:
case self::FIELD_PUSHER_PROJECTS:
return array(
self::CONDITION_INCLUDE_ALL,
self::CONDITION_INCLUDE_ANY,
self::CONDITION_INCLUDE_NONE,
self::CONDITION_EXISTS,
self::CONDITION_NOT_EXISTS,
);
case self::FIELD_DIFF_FILE:
+ case self::FIELD_BRANCHES:
return array(
self::CONDITION_CONTAINS,
self::CONDITION_REGEXP,
);
case self::FIELD_DIFF_CONTENT:
case self::FIELD_DIFF_ADDED_CONTENT:
case self::FIELD_DIFF_REMOVED_CONTENT:
return array(
self::CONDITION_CONTAINS,
self::CONDITION_REGEXP,
self::CONDITION_REGEXP_PAIR,
);
case self::FIELD_RULE:
return array(
self::CONDITION_RULE,
self::CONDITION_NOT_RULE,
);
case self::FIELD_CONTENT_SOURCE:
return array(
self::CONDITION_IS,
self::CONDITION_IS_NOT,
);
case self::FIELD_ALWAYS:
return array(
self::CONDITION_UNCONDITIONALLY,
);
case self::FIELD_DIFFERENTIAL_REVIEWERS:
return array(
self::CONDITION_EXISTS,
self::CONDITION_NOT_EXISTS,
self::CONDITION_INCLUDE_ALL,
self::CONDITION_INCLUDE_ANY,
self::CONDITION_INCLUDE_NONE,
);
case self::FIELD_DIFFERENTIAL_CCS:
return array(
self::CONDITION_INCLUDE_ALL,
self::CONDITION_INCLUDE_ANY,
self::CONDITION_INCLUDE_NONE,
);
case self::FIELD_DIFFERENTIAL_REVISION:
case self::FIELD_DIFFERENTIAL_ACCEPTED:
return array(
self::CONDITION_EXISTS,
self::CONDITION_NOT_EXISTS,
);
case self::FIELD_IS_MERGE_COMMIT:
return array(
self::CONDITION_IS_TRUE,
self::CONDITION_IS_FALSE,
);
default:
throw new Exception(
"This adapter does not define conditions for field '{$field}'!");
}
}
public function doesConditionMatch(
HeraldEngine $engine,
HeraldRule $rule,
HeraldCondition $condition,
$field_value) {
$condition_type = $condition->getFieldCondition();
$condition_value = $condition->getValue();
switch ($condition_type) {
case self::CONDITION_CONTAINS:
// "Contains" can take an array of strings, as in "Any changed
// filename" for diffs.
foreach ((array)$field_value as $value) {
if (stripos($value, $condition_value) !== false) {
return true;
}
}
return false;
case self::CONDITION_NOT_CONTAINS:
return (stripos($field_value, $condition_value) === false);
case self::CONDITION_IS:
return ($field_value == $condition_value);
case self::CONDITION_IS_NOT:
return ($field_value != $condition_value);
case self::CONDITION_IS_ME:
return ($field_value == $rule->getAuthorPHID());
case self::CONDITION_IS_NOT_ME:
return ($field_value != $rule->getAuthorPHID());
case self::CONDITION_IS_ANY:
if (!is_array($condition_value)) {
throw new HeraldInvalidConditionException(
"Expected condition value to be an array.");
}
$condition_value = array_fuse($condition_value);
return isset($condition_value[$field_value]);
case self::CONDITION_IS_NOT_ANY:
if (!is_array($condition_value)) {
throw new HeraldInvalidConditionException(
"Expected condition value to be an array.");
}
$condition_value = array_fuse($condition_value);
return !isset($condition_value[$field_value]);
case self::CONDITION_INCLUDE_ALL:
if (!is_array($field_value)) {
throw new HeraldInvalidConditionException(
"Object produced non-array value!");
}
if (!is_array($condition_value)) {
throw new HeraldInvalidConditionException(
"Expected condition value to be an array.");
}
$have = array_select_keys(array_fuse($field_value), $condition_value);
return (count($have) == count($condition_value));
case self::CONDITION_INCLUDE_ANY:
return (bool)array_select_keys(
array_fuse($field_value),
$condition_value);
case self::CONDITION_INCLUDE_NONE:
return !array_select_keys(
array_fuse($field_value),
$condition_value);
case self::CONDITION_EXISTS:
case self::CONDITION_IS_TRUE:
return (bool)$field_value;
case self::CONDITION_NOT_EXISTS:
case self::CONDITION_IS_FALSE:
return !$field_value;
case self::CONDITION_UNCONDITIONALLY:
return (bool)$field_value;
case self::CONDITION_REGEXP:
foreach ((array)$field_value as $value) {
// We add the 'S' flag because we use the regexp multiple times.
// It shouldn't cause any troubles if the flag is already there
// - /.*/S is evaluated same as /.*/SS.
$result = @preg_match($condition_value . 'S', $value);
if ($result === false) {
throw new HeraldInvalidConditionException(
"Regular expression is not valid!");
}
if ($result) {
return true;
}
}
return false;
case self::CONDITION_REGEXP_PAIR:
// Match a JSON-encoded pair of regular expressions against a
// dictionary. The first regexp must match the dictionary key, and the
// second regexp must match the dictionary value. If any key/value pair
// in the dictionary matches both regexps, the condition is satisfied.
$regexp_pair = json_decode($condition_value, true);
if (!is_array($regexp_pair)) {
throw new HeraldInvalidConditionException(
"Regular expression pair is not valid JSON!");
}
if (count($regexp_pair) != 2) {
throw new HeraldInvalidConditionException(
"Regular expression pair is not a pair!");
}
$key_regexp = array_shift($regexp_pair);
$value_regexp = array_shift($regexp_pair);
foreach ((array)$field_value as $key => $value) {
$key_matches = @preg_match($key_regexp, $key);
if ($key_matches === false) {
throw new HeraldInvalidConditionException(
"First regular expression is invalid!");
}
if ($key_matches) {
$value_matches = @preg_match($value_regexp, $value);
if ($value_matches === false) {
throw new HeraldInvalidConditionException(
"Second regular expression is invalid!");
}
if ($value_matches) {
return true;
}
}
}
return false;
case self::CONDITION_RULE:
case self::CONDITION_NOT_RULE:
$rule = $engine->getRule($condition_value);
if (!$rule) {
throw new HeraldInvalidConditionException(
"Condition references a rule which does not exist!");
}
$is_not = ($condition_type == self::CONDITION_NOT_RULE);
$result = $engine->doesRuleMatch($rule, $this);
if ($is_not) {
$result = !$result;
}
return $result;
case self::CONDITION_HAS_BIT:
return (($condition_value & $field_value) === $condition_value);
case self::CONDITION_NOT_BIT:
return (($condition_value & $field_value) !== $condition_value);
default:
throw new HeraldInvalidConditionException(
"Unknown condition '{$condition_type}'.");
}
}
public function willSaveCondition(HeraldCondition $condition) {
$condition_type = $condition->getFieldCondition();
$condition_value = $condition->getValue();
switch ($condition_type) {
case self::CONDITION_REGEXP:
$ok = @preg_match($condition_value, '');
if ($ok === false) {
throw new HeraldInvalidConditionException(
pht(
'The regular expression "%s" is not valid. Regular expressions '.
'must have enclosing characters (e.g. "@/path/to/file@", not '.
'"/path/to/file") and be syntactically correct.',
$condition_value));
}
break;
case self::CONDITION_REGEXP_PAIR:
$json = json_decode($condition_value, true);
if (!is_array($json)) {
throw new HeraldInvalidConditionException(
pht(
'The regular expression pair "%s" is not valid JSON. Enter a '.
'valid JSON array with two elements.',
$condition_value));
}
if (count($json) != 2) {
throw new HeraldInvalidConditionException(
pht(
'The regular expression pair "%s" must have exactly two '.
'elements.',
$condition_value));
}
$key_regexp = array_shift($json);
$val_regexp = array_shift($json);
$key_ok = @preg_match($key_regexp, '');
if ($key_ok === false) {
throw new HeraldInvalidConditionException(
pht(
'The first regexp in the regexp pair, "%s", is not a valid '.
'regexp.',
$key_regexp));
}
$val_ok = @preg_match($val_regexp, '');
if ($val_ok === false) {
throw new HeraldInvalidConditionException(
pht(
'The second regexp in the regexp pair, "%s", is not a valid '.
'regexp.',
$val_regexp));
}
break;
case self::CONDITION_CONTAINS:
case self::CONDITION_NOT_CONTAINS:
case self::CONDITION_IS:
case self::CONDITION_IS_NOT:
case self::CONDITION_IS_ANY:
case self::CONDITION_IS_NOT_ANY:
case self::CONDITION_INCLUDE_ALL:
case self::CONDITION_INCLUDE_ANY:
case self::CONDITION_INCLUDE_NONE:
case self::CONDITION_IS_ME:
case self::CONDITION_IS_NOT_ME:
case self::CONDITION_RULE:
case self::CONDITION_NOT_RULE:
case self::CONDITION_EXISTS:
case self::CONDITION_NOT_EXISTS:
case self::CONDITION_UNCONDITIONALLY:
case self::CONDITION_HAS_BIT:
case self::CONDITION_NOT_BIT:
case self::CONDITION_IS_TRUE:
case self::CONDITION_IS_FALSE:
// No explicit validation for these types, although there probably
// should be in some cases.
break;
default:
throw new HeraldInvalidConditionException(
pht(
'Unknown condition "%s"!',
$condition_type));
}
}
/* -( Actions )------------------------------------------------------------ */
abstract public function getActions($rule_type);
public function getActionNameMap($rule_type) {
switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
return array(
self::ACTION_NOTHING => pht('Do nothing'),
self::ACTION_ADD_CC => pht('Add emails to CC'),
self::ACTION_REMOVE_CC => pht('Remove emails from CC'),
self::ACTION_EMAIL => pht('Send an email to'),
self::ACTION_AUDIT => pht('Trigger an Audit by'),
self::ACTION_FLAG => pht('Mark with flag'),
self::ACTION_ASSIGN_TASK => pht('Assign task to'),
self::ACTION_ADD_PROJECTS => pht('Add projects'),
self::ACTION_ADD_REVIEWERS => pht('Add reviewers'),
self::ACTION_ADD_BLOCKING_REVIEWERS => pht('Add blocking reviewers'),
self::ACTION_APPLY_BUILD_PLANS => pht('Apply build plans'),
self::ACTION_BLOCK => pht('Block change with message'),
);
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
return array(
self::ACTION_NOTHING => pht('Do nothing'),
self::ACTION_ADD_CC => pht('Add me to CC'),
self::ACTION_REMOVE_CC => pht('Remove me from CC'),
self::ACTION_EMAIL => pht('Send me an email'),
self::ACTION_AUDIT => pht('Trigger an Audit by me'),
self::ACTION_FLAG => pht('Mark with flag'),
self::ACTION_ASSIGN_TASK => pht('Assign task to me'),
self::ACTION_ADD_PROJECTS => pht('Add projects'),
self::ACTION_ADD_REVIEWERS => pht('Add me as a reviewer'),
self::ACTION_ADD_BLOCKING_REVIEWERS =>
pht('Add me as a blocking reviewer'),
);
default:
throw new Exception("Unknown rule type '{$rule_type}'!");
}
}
public function willSaveAction(
HeraldRule $rule,
HeraldAction $action) {
$target = $action->getTarget();
if (is_array($target)) {
$target = array_keys($target);
}
$author_phid = $rule->getAuthorPHID();
$rule_type = $rule->getRuleType();
if ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) {
switch ($action->getAction()) {
case self::ACTION_EMAIL:
case self::ACTION_ADD_CC:
case self::ACTION_REMOVE_CC:
case self::ACTION_AUDIT:
case self::ACTION_ASSIGN_TASK:
case self::ACTION_ADD_REVIEWERS:
case self::ACTION_ADD_BLOCKING_REVIEWERS:
// For personal rules, force these actions to target the rule owner.
$target = array($author_phid);
break;
case self::ACTION_FLAG:
// Make sure flag color is valid; set to blue if not.
$color_map = PhabricatorFlagColor::getColorNameMap();
if (empty($color_map[$target])) {
$target = PhabricatorFlagColor::COLOR_BLUE;
}
break;
case self::ACTION_BLOCK:
case self::ACTION_NOTHING:
break;
default:
throw new HeraldInvalidActionException(
pht(
'Unrecognized action type "%s"!',
$action->getAction()));
}
}
$action->setTarget($target);
}
/* -( Values )------------------------------------------------------------- */
public function getValueTypeForFieldAndCondition($field, $condition) {
switch ($condition) {
case self::CONDITION_CONTAINS:
case self::CONDITION_NOT_CONTAINS:
case self::CONDITION_REGEXP:
case self::CONDITION_REGEXP_PAIR:
return self::VALUE_TEXT;
case self::CONDITION_IS:
case self::CONDITION_IS_NOT:
switch ($field) {
case self::FIELD_CONTENT_SOURCE:
return self::VALUE_CONTENT_SOURCE;
default:
return self::VALUE_TEXT;
}
break;
case self::CONDITION_IS_ANY:
case self::CONDITION_IS_NOT_ANY:
switch ($field) {
case self::FIELD_REPOSITORY:
return self::VALUE_REPOSITORY;
default:
return self::VALUE_USER;
}
break;
case self::CONDITION_INCLUDE_ALL:
case self::CONDITION_INCLUDE_ANY:
case self::CONDITION_INCLUDE_NONE:
switch ($field) {
case self::FIELD_REPOSITORY:
return self::VALUE_REPOSITORY;
case self::FIELD_CC:
return self::VALUE_EMAIL;
case self::FIELD_TAGS:
return self::VALUE_TAG;
case self::FIELD_AFFECTED_PACKAGE:
return self::VALUE_OWNERS_PACKAGE;
case self::FIELD_AUTHOR_PROJECTS:
case self::FIELD_PUSHER_PROJECTS:
case self::FIELD_PROJECTS:
return self::VALUE_PROJECT;
case self::FIELD_REVIEWERS:
return self::VALUE_USER_OR_PROJECT;
default:
return self::VALUE_USER;
}
break;
case self::CONDITION_IS_ME:
case self::CONDITION_IS_NOT_ME:
case self::CONDITION_EXISTS:
case self::CONDITION_NOT_EXISTS:
case self::CONDITION_UNCONDITIONALLY:
case self::CONDITION_IS_TRUE:
case self::CONDITION_IS_FALSE:
return self::VALUE_NONE;
case self::CONDITION_RULE:
case self::CONDITION_NOT_RULE:
return self::VALUE_RULE;
default:
throw new Exception("Unknown condition '{$condition}'.");
}
}
public static function getValueTypeForAction($action, $rule_type) {
$is_personal = ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
if ($is_personal) {
switch ($action) {
case self::ACTION_ADD_CC:
case self::ACTION_REMOVE_CC:
case self::ACTION_EMAIL:
case self::ACTION_NOTHING:
case self::ACTION_AUDIT:
case self::ACTION_ASSIGN_TASK:
case self::ACTION_ADD_REVIEWERS:
case self::ACTION_ADD_BLOCKING_REVIEWERS:
return self::VALUE_NONE;
case self::ACTION_FLAG:
return self::VALUE_FLAG_COLOR;
case self::ACTION_ADD_PROJECTS:
return self::VALUE_PROJECT;
default:
throw new Exception("Unknown or invalid action '{$action}'.");
}
} else {
switch ($action) {
case self::ACTION_ADD_CC:
case self::ACTION_REMOVE_CC:
case self::ACTION_EMAIL:
return self::VALUE_EMAIL;
case self::ACTION_NOTHING:
return self::VALUE_NONE;
case self::ACTION_ADD_PROJECTS:
return self::VALUE_PROJECT;
case self::ACTION_FLAG:
return self::VALUE_FLAG_COLOR;
case self::ACTION_ASSIGN_TASK:
return self::VALUE_USER;
case self::ACTION_AUDIT:
case self::ACTION_ADD_REVIEWERS:
case self::ACTION_ADD_BLOCKING_REVIEWERS:
return self::VALUE_USER_OR_PROJECT;
case self::ACTION_APPLY_BUILD_PLANS:
return self::VALUE_BUILD_PLAN;
case self::ACTION_BLOCK:
return self::VALUE_TEXT;
default:
throw new Exception("Unknown or invalid action '{$action}'.");
}
}
}
/* -( Repetition )--------------------------------------------------------- */
public function getRepetitionOptions() {
return array(
HeraldRepetitionPolicyConfig::EVERY,
);
}
public static function applyFlagEffect(HeraldEffect $effect, $phid) {
$color = $effect->getTarget();
// TODO: Silly that we need to load this again here.
$rule = id(new HeraldRule())->load($effect->getRuleID());
$user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$rule->getAuthorPHID());
$flag = PhabricatorFlagQuery::loadUserFlag($user, $phid);
if ($flag) {
return new HeraldApplyTranscript(
$effect,
false,
pht('Object already flagged.'));
}
$handle = id(new PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs(array($phid))
->executeOne();
$flag = new PhabricatorFlag();
$flag->setOwnerPHID($user->getPHID());
$flag->setType($handle->getType());
$flag->setObjectPHID($handle->getPHID());
// TOOD: Should really be transcript PHID, but it doesn't exist yet.
$flag->setReasonPHID($user->getPHID());
$flag->setColor($color);
$flag->setNote(
pht('Flagged by Herald Rule "%s".', $rule->getName()));
$flag->save();
return new HeraldApplyTranscript(
$effect,
true,
pht('Added flag.'));
}
public static function getAllAdapters() {
static $adapters;
if (!$adapters) {
$adapters = id(new PhutilSymbolLoader())
->setAncestorClass(__CLASS__)
->loadObjects();
}
return $adapters;
}
public static function getAdapterForContentType($content_type) {
$adapters = self::getAllAdapters();
foreach ($adapters as $adapter) {
if ($adapter->getAdapterContentType() == $content_type) {
return $adapter;
}
}
throw new Exception(
pht(
'No adapter exists for Herald content type "%s".',
$content_type));
}
public static function getEnabledAdapterMap(PhabricatorUser $viewer) {
$map = array();
$adapters = HeraldAdapter::getAllAdapters();
foreach ($adapters as $adapter) {
if (!$adapter->isAvailableToUser($viewer)) {
continue;
}
$type = $adapter->getAdapterContentType();
$name = $adapter->getAdapterContentName();
$map[$type] = $name;
}
asort($map);
return $map;
}
public function renderRuleAsText(HeraldRule $rule, array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$out = array();
if ($rule->getMustMatchAll()) {
$out[] = pht('When all of these conditions are met:');
} else {
$out[] = pht('When any of these conditions are met:');
}
$out[] = null;
foreach ($rule->getConditions() as $condition) {
$out[] = $this->renderConditionAsText($condition, $handles);
}
$out[] = null;
$integer_code_for_every = HeraldRepetitionPolicyConfig::toInt(
HeraldRepetitionPolicyConfig::EVERY);
if ($rule->getRepetitionPolicy() == $integer_code_for_every) {
$out[] = pht('Take these actions every time this rule matches:');
} else {
$out[] = pht('Take these actions the first time this rule matches:');
}
$out[] = null;
foreach ($rule->getActions() as $action) {
$out[] = $this->renderActionAsText($action, $handles);
}
return phutil_implode_html("\n", $out);
}
private function renderConditionAsText(
HeraldCondition $condition,
array $handles) {
$field_type = $condition->getFieldName();
$field_name = idx($this->getFieldNameMap(), $field_type);
$condition_type = $condition->getFieldCondition();
$condition_name = idx($this->getConditionNameMap(), $condition_type);
$value = $this->renderConditionValueAsText($condition, $handles);
return hsprintf(' %s %s %s', $field_name, $condition_name, $value);
}
private function renderActionAsText(
HeraldAction $action,
array $handles) {
$rule_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL;
$action_type = $action->getAction();
$action_name = idx($this->getActionNameMap($rule_global), $action_type);
$target = $this->renderActionTargetAsText($action, $handles);
return hsprintf(' %s %s', $action_name, $target);
}
private function renderConditionValueAsText(
HeraldCondition $condition,
array $handles) {
$value = $condition->getValue();
if (!is_array($value)) {
$value = array($value);
}
foreach ($value as $index => $val) {
$handle = idx($handles, $val);
if ($handle) {
$value[$index] = $handle->renderLink();
}
}
$value = phutil_implode_html(', ', $value);
return $value;
}
private function renderActionTargetAsText(
HeraldAction $action,
array $handles) {
$target = $action->getTarget();
if (!is_array($target)) {
$target = array($target);
}
foreach ($target as $index => $val) {
$handle = idx($handles, $val);
if ($handle) {
$target[$index] = $handle->renderLink();
}
}
$target = phutil_implode_html(', ', $target);
return $target;
}
/**
* Given a @{class:HeraldRule}, this function extracts all the phids that
* we'll want to load as handles later.
*
* This function performs a somewhat hacky approach to figuring out what
* is and is not a phid - try to get the phid type and if the type is
* *not* unknown assume its a valid phid.
*
* Don't try this at home. Use more strongly typed data at home.
*
* Think of the children.
*/
public static function getHandlePHIDs(HeraldRule $rule) {
$phids = array($rule->getAuthorPHID());
foreach ($rule->getConditions() as $condition) {
$value = $condition->getValue();
if (!is_array($value)) {
$value = array($value);
}
foreach ($value as $val) {
if (phid_get_type($val) !=
PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
$phids[] = $val;
}
}
}
foreach ($rule->getActions() as $action) {
$target = $action->getTarget();
if (!is_array($target)) {
$target = array($target);
}
foreach ($target as $val) {
if (phid_get_type($val) !=
PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
$phids[] = $val;
}
}
}
return $phids;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Mar 17, 1:24 AM (12 h, 10 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
964080
Default Alt Text
(78 KB)

Event Timeline