2013-12-03 00:45:36 +01:00
|
|
|
<?php
|
|
|
|
|
2013-12-05 20:56:14 +01:00
|
|
|
/**
|
|
|
|
* @task git Git Hooks
|
|
|
|
* @task hg Mercurial Hooks
|
|
|
|
* @task svn Subversion Hooks
|
|
|
|
*/
|
2013-12-03 00:45:36 +01:00
|
|
|
final class DiffusionCommitHookEngine extends Phobject {
|
|
|
|
|
2013-12-05 20:59:22 +01:00
|
|
|
const ENV_USER = 'PHABRICATOR_USER';
|
|
|
|
const ENV_REMOTE_ADDRESS = 'PHABRICATOR_REMOTE_ADDRESS';
|
|
|
|
const ENV_REMOTE_PROTOCOL = 'PHABRICATOR_REMOTE_PROTOCOL';
|
|
|
|
|
2013-12-03 00:45:36 +01:00
|
|
|
private $viewer;
|
|
|
|
private $repository;
|
|
|
|
private $stdin;
|
2013-12-03 00:45:55 +01:00
|
|
|
private $subversionTransaction;
|
|
|
|
private $subversionRepository;
|
2013-12-05 20:59:22 +01:00
|
|
|
private $remoteAddress;
|
|
|
|
private $remoteProtocol;
|
2013-12-05 20:59:41 +01:00
|
|
|
private $transactionKey;
|
2013-12-05 20:59:22 +01:00
|
|
|
|
|
|
|
public function setRemoteProtocol($remote_protocol) {
|
|
|
|
$this->remoteProtocol = $remote_protocol;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getRemoteProtocol() {
|
|
|
|
return $this->remoteProtocol;
|
|
|
|
}
|
2013-12-03 00:45:55 +01:00
|
|
|
|
2013-12-05 20:59:22 +01:00
|
|
|
public function setRemoteAddress($remote_address) {
|
|
|
|
$this->remoteAddress = $remote_address;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getRemoteAddress() {
|
|
|
|
return $this->remoteAddress;
|
|
|
|
}
|
2013-12-03 00:45:55 +01:00
|
|
|
|
2013-12-05 20:59:41 +01:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2013-12-03 00:45:55 +01:00
|
|
|
public function setSubversionTransactionInfo($transaction, $repository) {
|
|
|
|
$this->subversionTransaction = $transaction;
|
|
|
|
$this->subversionRepository = $repository;
|
|
|
|
return $this;
|
|
|
|
}
|
2013-12-03 00:45:36 +01:00
|
|
|
|
|
|
|
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 execute() {
|
|
|
|
$type = $this->getRepository()->getVersionControlSystem();
|
|
|
|
switch ($type) {
|
|
|
|
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
|
|
|
|
$err = $this->executeGitHook();
|
|
|
|
break;
|
2013-12-03 00:45:55 +01:00
|
|
|
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
|
|
|
|
$err = $this->executeSubversionHook();
|
|
|
|
break;
|
2013-12-03 00:46:03 +01:00
|
|
|
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
|
|
|
|
$err = $this->executeMercurialHook();
|
|
|
|
break;
|
2013-12-03 00:45:36 +01:00
|
|
|
default:
|
|
|
|
throw new Exception(pht('Unsupported repository type "%s"!', $type));
|
|
|
|
}
|
|
|
|
|
|
|
|
return $err;
|
|
|
|
}
|
|
|
|
|
2013-12-05 20:59:41 +01:00
|
|
|
private function newPushLog() {
|
|
|
|
return PhabricatorRepositoryPushLog::initializeNewLog($this->getViewer())
|
|
|
|
->setRepositoryPHID($this->getRepository()->getPHID())
|
|
|
|
->setEpoch(time())
|
|
|
|
->setRemoteAddress($this->getRemoteAddressForLog())
|
|
|
|
->setRemoteProtocol($this->getRemoteProtocol())
|
|
|
|
->setTransactionKey($this->getTransactionKey())
|
|
|
|
->setRejectCode(PhabricatorRepositoryPushLog::REJECT_ACCEPT)
|
|
|
|
->setRejectDetails(null);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-12-03 19:27:45 +01:00
|
|
|
/**
|
|
|
|
* @task git
|
|
|
|
*/
|
2013-12-03 00:45:36 +01:00
|
|
|
private function executeGitHook() {
|
|
|
|
$updates = $this->parseGitUpdates($this->getStdin());
|
|
|
|
|
2013-12-03 19:28:39 +01:00
|
|
|
$this->rejectGitDangerousChanges($updates);
|
|
|
|
|
2013-12-03 19:27:45 +01:00
|
|
|
// TODO: Do cheap checks: non-ff commits, mutating refs without access,
|
|
|
|
// creating or deleting things you can't touch. We can do all non-content
|
|
|
|
// checks here.
|
2013-12-03 00:45:36 +01:00
|
|
|
|
2013-12-03 19:27:45 +01:00
|
|
|
$updates = $this->findGitNewCommits($updates);
|
2013-12-03 00:45:55 +01:00
|
|
|
|
2013-12-03 19:27:45 +01:00
|
|
|
// TODO: Now, do content checks.
|
2013-12-03 00:45:55 +01:00
|
|
|
|
2013-12-05 20:56:14 +01:00
|
|
|
// TODO: Generalize this; just getting some data in the database for now.
|
2013-12-05 20:59:22 +01:00
|
|
|
|
2013-12-05 20:56:14 +01:00
|
|
|
$logs = array();
|
|
|
|
foreach ($updates as $update) {
|
2013-12-05 20:59:41 +01:00
|
|
|
$log = $this->newPushLog()
|
2013-12-05 20:56:14 +01:00
|
|
|
->setRefType($update['type'])
|
|
|
|
->setRefNameHash(PhabricatorHash::digestForIndex($update['ref']))
|
|
|
|
->setRefNameRaw($update['ref'])
|
|
|
|
->setRefNameEncoding(phutil_is_utf8($update['ref']) ? 'utf8' : null)
|
|
|
|
->setRefOld($update['old'])
|
|
|
|
->setRefNew($update['new'])
|
2013-12-05 20:59:41 +01:00
|
|
|
->setMergeBase(idx($update, 'merge-base'));
|
2013-12-05 20:56:14 +01:00
|
|
|
|
|
|
|
$flags = 0;
|
|
|
|
if ($update['operation'] == 'create') {
|
|
|
|
$flags = $flags | PhabricatorRepositoryPushLog::CHANGEFLAG_ADD;
|
|
|
|
} else if ($update['operation'] == 'delete') {
|
|
|
|
$flags = $flags | PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE;
|
|
|
|
} else {
|
|
|
|
// TODO: This isn't correct; these might be APPEND or REWRITE, and
|
|
|
|
// if they're REWRITE they might be DANGEROUS. Fix this when this
|
|
|
|
// gets generalized.
|
|
|
|
$flags = $flags | PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND;
|
|
|
|
}
|
|
|
|
|
|
|
|
$log->setChangeFlags($flags);
|
|
|
|
$logs[] = $log;
|
|
|
|
}
|
|
|
|
|
2013-12-05 20:59:41 +01:00
|
|
|
// Now, build logs for all the commits.
|
|
|
|
// TODO: Generalize this, too.
|
|
|
|
$commits = array_mergev(ipull($updates, 'commits'));
|
|
|
|
$commits = array_unique($commits);
|
|
|
|
foreach ($commits as $commit) {
|
|
|
|
$log = $this->newPushLog()
|
|
|
|
->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT)
|
|
|
|
->setRefNew($commit)
|
|
|
|
->setChangeFlags(PhabricatorRepositoryPushLog::CHANGEFLAG_ADD);
|
|
|
|
$logs[] = $log;
|
|
|
|
}
|
|
|
|
|
2013-12-05 20:56:14 +01:00
|
|
|
foreach ($logs as $log) {
|
|
|
|
$log->save();
|
|
|
|
}
|
|
|
|
|
2013-12-03 00:45:55 +01:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2013-12-03 00:46:03 +01:00
|
|
|
|
2013-12-03 19:27:45 +01:00
|
|
|
/**
|
|
|
|
* @task git
|
|
|
|
*/
|
2013-12-03 00:45:36 +01:00
|
|
|
private function parseGitUpdates($stdin) {
|
|
|
|
$updates = array();
|
|
|
|
|
|
|
|
$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));
|
|
|
|
}
|
2013-12-03 19:27:45 +01:00
|
|
|
$update = array(
|
2013-12-03 00:45:36 +01:00
|
|
|
'old' => $parts[0],
|
2013-12-03 19:27:45 +01:00
|
|
|
'old.short' => substr($parts[0], 0, 8),
|
2013-12-03 00:45:36 +01:00
|
|
|
'new' => $parts[1],
|
2013-12-03 19:27:45 +01:00
|
|
|
'new.short' => substr($parts[1], 0, 8),
|
2013-12-03 00:45:36 +01:00
|
|
|
'ref' => $parts[2],
|
|
|
|
);
|
2013-12-03 19:27:45 +01:00
|
|
|
|
|
|
|
if (preg_match('(^refs/heads/)', $update['ref'])) {
|
|
|
|
$update['type'] = 'branch';
|
2013-12-03 19:28:39 +01:00
|
|
|
$update['ref.short'] = substr($update['ref'], strlen('refs/heads/'));
|
2013-12-03 19:27:45 +01:00
|
|
|
} else if (preg_match('(^refs/tags/)', $update['ref'])) {
|
|
|
|
$update['type'] = 'tag';
|
2013-12-03 19:28:39 +01:00
|
|
|
$update['ref.short'] = substr($update['ref'], strlen('refs/tags/'));
|
2013-12-03 19:27:45 +01:00
|
|
|
} else {
|
|
|
|
$update['type'] = 'unknown';
|
|
|
|
}
|
|
|
|
|
|
|
|
$updates[] = $update;
|
|
|
|
}
|
|
|
|
|
|
|
|
$updates = $this->findGitMergeBases($updates);
|
|
|
|
|
|
|
|
return $updates;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @task git
|
|
|
|
*/
|
|
|
|
private function findGitMergeBases(array $updates) {
|
|
|
|
$empty = str_repeat('0', 40);
|
|
|
|
|
|
|
|
$futures = array();
|
|
|
|
foreach ($updates as $key => $update) {
|
|
|
|
// Updates are in the form:
|
|
|
|
//
|
|
|
|
// <old hash> <new hash> <ref>
|
|
|
|
//
|
|
|
|
// 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.
|
|
|
|
|
|
|
|
if ($update['old'] == $empty) {
|
|
|
|
$updates[$key]['operation'] = 'create';
|
|
|
|
} else if ($update['new'] == $empty) {
|
|
|
|
$updates[$key]['operation'] = 'delete';
|
|
|
|
} else {
|
|
|
|
$updates[$key]['operation'] = 'update';
|
|
|
|
$futures[$key] = $this->getRepository()->getLocalCommandFuture(
|
|
|
|
'merge-base %s %s',
|
|
|
|
$update['old'],
|
|
|
|
$update['new']);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach (Futures($futures)->limit(8) as $key => $future) {
|
2013-12-11 23:46:46 +01:00
|
|
|
|
|
|
|
// 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) {
|
|
|
|
$updates[$key]['merge-base'] = null;
|
|
|
|
} else {
|
|
|
|
$updates[$key]['merge-base'] = rtrim($stdout, "\n");
|
|
|
|
}
|
2013-12-03 19:27:45 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return $updates;
|
|
|
|
}
|
|
|
|
|
|
|
|
private function findGitNewCommits(array $updates) {
|
|
|
|
$futures = array();
|
|
|
|
foreach ($updates as $key => $update) {
|
2013-12-03 19:28:39 +01:00
|
|
|
if ($update['operation'] == 'delete') {
|
2013-12-03 19:27:45 +01:00
|
|
|
// 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',
|
|
|
|
$update['new']);
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach (Futures($futures)->limit(8) as $key => $future) {
|
|
|
|
list($stdout) = $future->resolvex();
|
|
|
|
$commits = phutil_split_lines($stdout, $retain_newlines = false);
|
|
|
|
$updates[$key]['commits'] = $commits;
|
2013-12-03 00:45:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return $updates;
|
|
|
|
}
|
|
|
|
|
2013-12-03 19:28:39 +01:00
|
|
|
private function rejectGitDangerousChanges(array $updates) {
|
|
|
|
$repository = $this->getRepository();
|
|
|
|
if ($repository->shouldAllowDangerousChanges()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($updates as $update) {
|
|
|
|
if ($update['type'] != 'branch') {
|
|
|
|
// 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.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($update['operation'] == 'create') {
|
|
|
|
// Creating a branch is never dangerous.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2013-12-03 19:31:29 +01:00
|
|
|
if ($update['operation'] == 'update') {
|
2013-12-03 19:28:39 +01:00
|
|
|
if ($update['old'] == $update['merge-base']) {
|
|
|
|
// This is a fast-forward update to an existing branch.
|
|
|
|
// These are safe.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// We either have a branch deletion or a non fast-forward branch update.
|
|
|
|
// Format a message and reject the push.
|
|
|
|
|
|
|
|
if ($update['operation'] == 'delete') {
|
|
|
|
$message = pht(
|
|
|
|
"DANGEROUS CHANGE: The change you're attempting to push deletes ".
|
|
|
|
"the branch '%s'.",
|
|
|
|
$update['ref.short']);
|
|
|
|
} else {
|
|
|
|
$message = pht(
|
|
|
|
"DANGEROUS CHANGE: 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.",
|
|
|
|
$update['ref.short'],
|
|
|
|
$update['old.short'],
|
|
|
|
$update['new.short']);
|
|
|
|
}
|
|
|
|
|
|
|
|
$boilerplate = pht(
|
|
|
|
"Dangerous change protection is enabled for this repository.\n".
|
|
|
|
"Edit the repository configuration before making dangerous changes.");
|
|
|
|
|
|
|
|
$message = $message."\n".$boilerplate;
|
|
|
|
|
|
|
|
throw new DiffusionCommitHookRejectException($message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-12-03 19:27:45 +01:00
|
|
|
private function executeSubversionHook() {
|
|
|
|
|
|
|
|
// TODO: Do useful things here, too.
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
private function executeMercurialHook() {
|
|
|
|
|
|
|
|
// TODO: Here, too, useful things should be done.
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
2013-12-03 00:45:36 +01:00
|
|
|
}
|