Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php
index 477a89c88b..49a34ac250 100644
--- a/src/applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php
@@ -1,549 +1,559 @@
<?php
final class DiffusionBrowseQueryConduitAPIMethod
extends DiffusionQueryConduitAPIMethod {
public function getAPIMethodName() {
return 'diffusion.browsequery';
}
public function getMethodDescription() {
return pht(
'File(s) information for a repository at an (optional) path and '.
'(optional) commit.');
}
protected function defineReturnType() {
return 'array';
}
protected function defineCustomParamTypes() {
return array(
'path' => 'optional string',
'commit' => 'optional string',
'needValidityOnly' => 'optional bool',
'limit' => 'optional int',
'offset' => 'optional int',
);
}
protected function getResult(ConduitAPIRequest $request) {
$result = parent::getResult($request);
return $result->toDictionary();
}
protected function getGitResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
+
$path = $request->getValue('path');
+ if (!strlen($path)) {
+ $path = null;
+ }
+
$commit = $request->getValue('commit');
$offset = (int)$request->getValue('offset');
$limit = (int)$request->getValue('limit');
$result = $this->getEmptyResultSet();
- if ($path == '') {
+ if ($path === null) {
// Fast path to improve the performance of the repository view; we know
// the root is always a tree at any commit and always exists.
$stdout = 'tree';
} else {
try {
list($stdout) = $repository->execxLocalCommand(
- 'cat-file -t -- %s:%s',
- $commit,
- $path);
+ 'cat-file -t -- %s',
+ sprintf('%s:%s', $commit, $path));
} catch (CommandException $e) {
// The "cat-file" command may fail if the path legitimately does not
// exist, but it may also fail if the path is a submodule. This can
// produce either "Not a valid object name" or "could not get object
// info".
// To detect if we have a submodule, use `git ls-tree`. If the path
// is a submodule, we'll get a "160000" mode mask with type "commit".
list($sub_err, $sub_stdout) = $repository->execLocalCommand(
'ls-tree %s -- %s',
gitsprintf('%s', $commit),
$path);
if (!$sub_err) {
// If the path failed "cat-file" but "ls-tree" worked, we assume it
// must be a submodule. If it is, the output will look something
// like this:
//
// 160000 commit <hash> <path>
//
// We make sure it has the 160000 mode mask to confirm that it's
// definitely a submodule.
$mode = (int)$sub_stdout;
if ($mode & 160000) {
$submodule_reason = DiffusionBrowseResultSet::REASON_IS_SUBMODULE;
$result
->setReasonForEmptyResultSet($submodule_reason);
return $result;
}
}
$stderr = $e->getStderr();
if (preg_match('/^fatal: Not a valid object name/', $stderr)) {
// Grab two logs, since the first one is when the object was deleted.
list($stdout) = $repository->execxLocalCommand(
'log -n2 %s %s -- %s',
'--format=%H',
gitsprintf('%s', $commit),
$path);
$stdout = trim($stdout);
if ($stdout) {
$commits = explode("\n", $stdout);
$result
->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_DELETED)
->setDeletedAtCommit(idx($commits, 0))
->setExistedAtCommit(idx($commits, 1));
return $result;
}
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_NONEXISTENT);
return $result;
} else {
throw $e;
}
}
}
if (trim($stdout) == 'blob') {
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_FILE);
return $result;
}
$result->setIsValidResults(true);
if ($this->shouldOnlyTestValidity($request)) {
return $result;
}
- list($stdout) = $repository->execxLocalCommand(
- 'ls-tree -z -l %s -- %s',
- gitsprintf('%s', $commit),
- $path);
+ if ($path === null) {
+ list($stdout) = $repository->execxLocalCommand(
+ 'ls-tree -z -l %s --',
+ gitsprintf('%s', $commit));
+ } else {
+ list($stdout) = $repository->execxLocalCommand(
+ 'ls-tree -z -l %s -- %s',
+ gitsprintf('%s', $commit),
+ $path);
+ }
$submodules = array();
- if (strlen($path)) {
+ if ($path !== null) {
$prefix = rtrim($path, '/').'/';
} else {
$prefix = '';
}
$count = 0;
$results = array();
$lines = empty($stdout)
? array()
: explode("\0", rtrim($stdout));
foreach ($lines as $line) {
// NOTE: Limit to 5 components so we parse filenames with spaces in them
// correctly.
// NOTE: The output uses a mixture of tabs and one-or-more spaces to
// delimit fields.
$parts = preg_split('/\s+/', $line, 5);
if (count($parts) < 5) {
throw new Exception(
pht(
'Expected "<mode> <type> <hash> <size>\t<name>", for ls-tree of '.
'"%s:%s", got: %s',
$commit,
$path,
$line));
}
list($mode, $type, $hash, $size, $name) = $parts;
$path_result = new DiffusionRepositoryPath();
if ($type == 'tree') {
$file_type = DifferentialChangeType::FILE_DIRECTORY;
} else if ($type == 'commit') {
$file_type = DifferentialChangeType::FILE_SUBMODULE;
$submodules[] = $path_result;
} else {
$mode = intval($mode, 8);
if (($mode & 0120000) == 0120000) {
$file_type = DifferentialChangeType::FILE_SYMLINK;
} else {
$file_type = DifferentialChangeType::FILE_NORMAL;
}
}
$path_result->setFullPath($prefix.$name);
$path_result->setPath($name);
$path_result->setHash($hash);
$path_result->setFileType($file_type);
$path_result->setFileSize($size);
if ($count >= $offset) {
$results[] = $path_result;
}
$count++;
if ($limit && $count >= ($offset + $limit)) {
break;
}
}
// If we identified submodules, lookup the module info at this commit to
// find their source URIs.
if ($submodules) {
// NOTE: We need to read the file out of git and write it to a temporary
// location because "git config -f" doesn't accept a "commit:path"-style
// argument.
// NOTE: This file may not exist, e.g. because the commit author removed
// it when they added the submodule. See T1448. If it's not present, just
// show the submodule without enriching it. If ".gitmodules" was removed
// it seems to partially break submodules, but the repository as a whole
// continues to work fine and we've seen at least two cases of this in
// the wild.
list($err, $contents) = $repository->execLocalCommand(
'cat-file blob -- %s:.gitmodules',
$commit);
if (!$err) {
$tmp = new TempFile();
Filesystem::writeFile($tmp, $contents);
list($module_info) = $repository->execxLocalCommand(
'config -l -f %s',
$tmp);
$dict = array();
$lines = explode("\n", trim($module_info));
foreach ($lines as $line) {
list($key, $value) = explode('=', $line, 2);
$parts = explode('.', $key);
$dict[$key] = $value;
}
- foreach ($submodules as $path) {
- $full_path = $path->getFullPath();
+ foreach ($submodules as $submodule_path) {
+ $full_path = $submodule_path->getFullPath();
$key = 'submodule.'.$full_path.'.url';
if (isset($dict[$key])) {
$path->setExternalURI($dict[$key]);
}
}
}
}
return $result->setPaths($results);
}
protected function getMercurialResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$path = $request->getValue('path');
$commit = $request->getValue('commit');
$offset = (int)$request->getValue('offset');
$limit = (int)$request->getValue('limit');
$result = $this->getEmptyResultSet();
$entire_manifest = id(new DiffusionLowLevelMercurialPathsQuery())
->setRepository($repository)
->withCommit($commit)
->withPath($path)
->execute();
$results = array();
$match_against = trim($path, '/');
$match_len = strlen($match_against);
// For the root, don't trim. For other paths, trim the "/" after we match.
// We need this because Mercurial's canonical paths have no leading "/",
// but ours do.
$trim_len = $match_len ? $match_len + 1 : 0;
$count = 0;
foreach ($entire_manifest as $path) {
if (strncmp($path, $match_against, $match_len)) {
continue;
}
if (!strlen($path)) {
continue;
}
$remainder = substr($path, $trim_len);
if (!strlen($remainder)) {
// There is a file with this exact name in the manifest, so clearly
// it's a file.
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_FILE);
return $result;
}
$parts = explode('/', $remainder);
$name = reset($parts);
// If we've already seen this path component, we're looking at a file
// inside a directory we already processed. Just move on.
if (isset($results[$name])) {
continue;
}
if (count($parts) == 1) {
$type = DifferentialChangeType::FILE_NORMAL;
} else {
$type = DifferentialChangeType::FILE_DIRECTORY;
}
if ($count >= $offset) {
$results[$name] = $type;
}
$count++;
if ($limit && ($count >= ($offset + $limit))) {
break;
}
}
foreach ($results as $key => $type) {
$path_result = new DiffusionRepositoryPath();
$path_result->setPath($key);
$path_result->setFileType($type);
$path_result->setFullPath(ltrim($match_against.'/', '/').$key);
$results[$key] = $path_result;
}
$valid_results = true;
if (empty($results)) {
// TODO: Detect "deleted" by issuing "hg log"?
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_NONEXISTENT);
$valid_results = false;
}
return $result
->setPaths($results)
->setIsValidResults($valid_results);
}
protected function getSVNResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$path = $request->getValue('path');
$commit = $request->getValue('commit');
$offset = (int)$request->getValue('offset');
$limit = (int)$request->getValue('limit');
$result = $this->getEmptyResultSet();
$subpath = $repository->getDetail('svn-subpath');
if ($subpath && strncmp($subpath, $path, strlen($subpath))) {
// If we have a subpath and the path isn't a child of it, it (almost
// certainly) won't exist since we don't track commits which affect
// it. (Even if it exists, return a consistent result.)
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_UNTRACKED_PARENT);
return $result;
}
$conn_r = $repository->establishConnection('r');
$parent_path = DiffusionPathIDQuery::getParentPath($path);
$path_query = new DiffusionPathIDQuery(
array(
$path,
$parent_path,
));
$path_map = $path_query->loadPathIDs();
$path_id = $path_map[$path];
$parent_path_id = $path_map[$parent_path];
if (empty($path_id)) {
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_NONEXISTENT);
return $result;
}
if ($commit) {
$slice_clause = qsprintf($conn_r, 'AND svnCommit <= %d', $commit);
} else {
$slice_clause = qsprintf($conn_r, '');
}
$index = queryfx_all(
$conn_r,
'SELECT pathID, max(svnCommit) maxCommit FROM %T WHERE
repositoryID = %d AND parentID = %d
%Q GROUP BY pathID',
PhabricatorRepository::TABLE_FILESYSTEM,
$repository->getID(),
$path_id,
$slice_clause);
if (!$index) {
if ($path == '/') {
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_EMPTY);
} else {
// NOTE: The parent path ID is included so this query can take
// advantage of the table's primary key; it is uniquely determined by
// the pathID but if we don't do the lookup ourselves MySQL doesn't have
// the information it needs to avoid a table scan.
$reasons = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE repositoryID = %d
AND parentID = %d
AND pathID = %d
%Q ORDER BY svnCommit DESC LIMIT 2',
PhabricatorRepository::TABLE_FILESYSTEM,
$repository->getID(),
$parent_path_id,
$path_id,
$slice_clause);
$reason = reset($reasons);
if (!$reason) {
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_NONEXISTENT);
} else {
$file_type = $reason['fileType'];
if (empty($reason['existed'])) {
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_DELETED);
$result->setDeletedAtCommit($reason['svnCommit']);
if (!empty($reasons[1])) {
$result->setExistedAtCommit($reasons[1]['svnCommit']);
}
} else if ($file_type == DifferentialChangeType::FILE_DIRECTORY) {
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_EMPTY);
} else {
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_FILE);
}
}
}
return $result;
}
$result->setIsValidResults(true);
if ($this->shouldOnlyTestValidity($request)) {
return $result;
}
$sql = array();
foreach ($index as $row) {
$sql[] = qsprintf(
$conn_r,
'(pathID = %d AND svnCommit = %d)',
$row['pathID'],
$row['maxCommit']);
}
$browse = queryfx_all(
$conn_r,
'SELECT *, p.path pathName
FROM %T f JOIN %T p ON f.pathID = p.id
WHERE repositoryID = %d
AND parentID = %d
AND existed = 1
AND (%LO)
ORDER BY pathName',
PhabricatorRepository::TABLE_FILESYSTEM,
PhabricatorRepository::TABLE_PATH,
$repository->getID(),
$path_id,
$sql);
$loadable_commits = array();
foreach ($browse as $key => $file) {
// We need to strip out directories because we don't store last-modified
// in the filesystem table.
if ($file['fileType'] != DifferentialChangeType::FILE_DIRECTORY) {
$loadable_commits[] = $file['svnCommit'];
$browse[$key]['hasCommit'] = true;
}
}
$commits = array();
$commit_data = array();
if ($loadable_commits) {
// NOTE: Even though these are integers, use '%Ls' because MySQL doesn't
// use the second part of the key otherwise!
$commits = id(new PhabricatorRepositoryCommit())->loadAllWhere(
'repositoryID = %d AND commitIdentifier IN (%Ls)',
$repository->getID(),
$loadable_commits);
$commits = mpull($commits, null, 'getCommitIdentifier');
if ($commits) {
$commit_data = id(new PhabricatorRepositoryCommitData())->loadAllWhere(
'commitID in (%Ld)',
mpull($commits, 'getID'));
$commit_data = mpull($commit_data, null, 'getCommitID');
} else {
$commit_data = array();
}
}
$path_normal = DiffusionPathIDQuery::normalizePath($path);
$results = array();
$count = 0;
foreach ($browse as $file) {
$full_path = $file['pathName'];
$file_path = ltrim(substr($full_path, strlen($path_normal)), '/');
$full_path = ltrim($full_path, '/');
$result_path = new DiffusionRepositoryPath();
$result_path->setPath($file_path);
$result_path->setFullPath($full_path);
$result_path->setFileType($file['fileType']);
if (!empty($file['hasCommit'])) {
$commit = idx($commits, $file['svnCommit']);
if ($commit) {
$data = idx($commit_data, $commit->getID());
$result_path->setLastModifiedCommit($commit);
$result_path->setLastCommitData($data);
}
}
if ($count >= $offset) {
$results[] = $result_path;
}
$count++;
if ($limit && ($count >= ($offset + $limit))) {
break;
}
}
if (empty($results)) {
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_EMPTY);
}
return $result->setPaths($results);
}
private function getEmptyResultSet() {
return id(new DiffusionBrowseResultSet())
->setPaths(array())
->setReasonForEmptyResultSet(null)
->setIsValidResults(false);
}
private function shouldOnlyTestValidity(ConduitAPIRequest $request) {
return $request->getValue('needValidityOnly', false);
}
}
diff --git a/src/applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php
index 223aef6a7d..8c53ff0555 100644
--- a/src/applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php
@@ -1,286 +1,300 @@
<?php
final class DiffusionHistoryQueryConduitAPIMethod
extends DiffusionQueryConduitAPIMethod {
private $parents = array();
public function getAPIMethodName() {
return 'diffusion.historyquery';
}
public function getMethodDescription() {
return pht(
'Returns history information for a repository at a specific '.
'commit and path.');
}
protected function defineReturnType() {
return 'array';
}
protected function defineCustomParamTypes() {
return array(
'commit' => 'required string',
'against' => 'optional string',
'path' => 'required string',
'offset' => 'required int',
'limit' => 'required int',
'needDirectChanges' => 'optional bool',
'needChildChanges' => 'optional bool',
);
}
protected function getResult(ConduitAPIRequest $request) {
$path_changes = parent::getResult($request);
return array(
'pathChanges' => mpull($path_changes, 'toDictionary'),
'parents' => $this->parents,
);
}
protected function getGitResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$commit_hash = $request->getValue('commit');
$against_hash = $request->getValue('against');
+
$path = $request->getValue('path');
+ if (!strlen($path)) {
+ $path = null;
+ }
+
$offset = $request->getValue('offset');
$limit = $request->getValue('limit');
if (strlen($against_hash)) {
$commit_range = "${against_hash}..${commit_hash}";
} else {
$commit_range = $commit_hash;
}
+ $argv = array();
+
+ $argv[] = '--skip';
+ $argv[] = $offset;
+
+ $argv[] = '--max-count';
+ $argv[] = $limit;
+
+ $argv[] = '--format=%H:%P';
+
+ $argv[] = gitsprintf('%s', $commit_range);
+
+ $argv[] = '--';
+
+ if ($path !== null) {
+ $argv[] = $path;
+ }
+
list($stdout) = $repository->execxLocalCommand(
- 'log '.
- '--skip=%d '.
- '-n %d '.
- '--pretty=format:%s '.
- '%s -- %C',
- $offset,
- $limit,
- '%H:%P',
- gitsprintf('%s', $commit_range),
- // Git omits merge commits if the path is provided, even if it is empty.
- (strlen($path) ? csprintf('%s', $path) : ''));
+ 'log %Ls',
+ $argv);
$lines = explode("\n", trim($stdout));
$lines = array_filter($lines);
$hash_list = array();
$parent_map = array();
foreach ($lines as $line) {
list($hash, $parents) = explode(':', $line);
$hash_list[] = $hash;
$parent_map[$hash] = preg_split('/\s+/', $parents);
}
$this->parents = $parent_map;
if (!$hash_list) {
return array();
}
return DiffusionQuery::loadHistoryForCommitIdentifiers(
$hash_list,
$drequest);
}
protected function getMercurialResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$commit_hash = $request->getValue('commit');
$path = $request->getValue('path');
$offset = $request->getValue('offset');
$limit = $request->getValue('limit');
$path = DiffusionPathIDQuery::normalizePath($path);
$path = ltrim($path, '/');
// NOTE: Older versions of Mercurial give different results for these
// commands (see T1268):
//
// $ hg log -- ''
// $ hg log
//
// All versions of Mercurial give different results for these commands
// (merge commits are excluded with the "." version):
//
// $ hg log -- .
// $ hg log
//
// If we don't have a path component in the query, omit it from the command
// entirely to avoid these inconsistencies.
// NOTE: When viewing the history of a file, we don't use "-b", because
// Mercurial stops history at the branchpoint but we're interested in all
// ancestors. When viewing history of a branch, we do use "-b", and thus
// stop history (this is more consistent with the Mercurial worldview of
// branches).
if (strlen($path)) {
$path_arg = csprintf('%s', $path);
$revset_arg = hgsprintf(
'reverse(ancestors(%s))',
$commit_hash);
} else {
$path_arg = '';
$revset_arg = hgsprintf(
'reverse(ancestors(%s)) and branch(%s)',
$drequest->getBranch(),
$commit_hash);
}
list($stdout) = $repository->execxLocalCommand(
'log --debug --template %s --limit %d --rev %s -- %C',
'{node};{parents}\\n',
($offset + $limit), // No '--skip' in Mercurial.
$revset_arg,
$path_arg);
$stdout = DiffusionMercurialCommandEngine::filterMercurialDebugOutput(
$stdout);
$lines = explode("\n", trim($stdout));
$lines = array_slice($lines, $offset);
$hash_list = array();
$parent_map = array();
$last = null;
foreach (array_reverse($lines) as $line) {
list($hash, $parents) = explode(';', $line);
$parents = trim($parents);
if (!$parents) {
if ($last === null) {
$parent_map[$hash] = array('...');
} else {
$parent_map[$hash] = array($last);
}
} else {
$parents = preg_split('/\s+/', $parents);
foreach ($parents as $parent) {
list($plocal, $phash) = explode(':', $parent);
if (!preg_match('/^0+$/', $phash)) {
$parent_map[$hash][] = $phash;
}
}
// This may happen for the zeroth commit in repository, both hashes
// are "000000000...".
if (empty($parent_map[$hash])) {
$parent_map[$hash] = array('...');
}
}
// The rendering code expects the first commit to be "mainline", like
// Git. Flip the order so it does the right thing.
$parent_map[$hash] = array_reverse($parent_map[$hash]);
$hash_list[] = $hash;
$last = $hash;
}
$hash_list = array_reverse($hash_list);
$this->parents = array_reverse($parent_map, true);
return DiffusionQuery::loadHistoryForCommitIdentifiers(
$hash_list,
$drequest);
}
protected function getSVNResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$commit = $request->getValue('commit');
$path = $request->getValue('path');
$offset = $request->getValue('offset');
$limit = $request->getValue('limit');
$need_direct_changes = $request->getValue('needDirectChanges');
$need_child_changes = $request->getValue('needChildChanges');
$conn_r = $repository->establishConnection('r');
$paths = queryfx_all(
$conn_r,
'SELECT id, path FROM %T WHERE pathHash IN (%Ls)',
PhabricatorRepository::TABLE_PATH,
array(md5('/'.trim($path, '/'))));
$paths = ipull($paths, 'id', 'path');
$path_id = idx($paths, '/'.trim($path, '/'));
if (!$path_id) {
return array();
}
$filter_query = qsprintf($conn_r, '');
if ($need_direct_changes) {
if ($need_child_changes) {
$filter_query = qsprintf(
$conn_r,
'AND (isDirect = 1 OR changeType = %s)',
DifferentialChangeType::TYPE_CHILD);
} else {
$filter_query = qsprintf(
$conn_r,
'AND (isDirect = 1)');
}
}
$history_data = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE repositoryID = %d AND pathID = %d
AND commitSequence <= %d
%Q
ORDER BY commitSequence DESC
LIMIT %d, %d',
PhabricatorRepository::TABLE_PATHCHANGE,
$repository->getID(),
$path_id,
$commit ? $commit : 0x7FFFFFFF,
$filter_query,
$offset,
$limit);
$commits = array();
$commit_data = array();
$commit_ids = ipull($history_data, 'commitID');
if ($commit_ids) {
$commits = id(new PhabricatorRepositoryCommit())->loadAllWhere(
'id IN (%Ld)',
$commit_ids);
if ($commits) {
$commit_data = id(new PhabricatorRepositoryCommitData())->loadAllWhere(
'commitID in (%Ld)',
$commit_ids);
$commit_data = mpull($commit_data, null, 'getCommitID');
}
}
$history = array();
foreach ($history_data as $row) {
$item = new DiffusionPathChange();
$commit = idx($commits, $row['commitID']);
if ($commit) {
$item->setCommit($commit);
$item->setCommitIdentifier($commit->getCommitIdentifier());
$data = idx($commit_data, $commit->getID());
if ($data) {
$item->setCommitData($data);
}
}
$item->setChangeType($row['changeType']);
$item->setFileType($row['fileType']);
$history[] = $item;
}
return $history;
}
}
diff --git a/src/applications/diffusion/conduit/DiffusionInternalGitRawDiffQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionInternalGitRawDiffQueryConduitAPIMethod.php
index ac241a282f..ea94ff71c4 100644
--- a/src/applications/diffusion/conduit/DiffusionInternalGitRawDiffQueryConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionInternalGitRawDiffQueryConduitAPIMethod.php
@@ -1,123 +1,123 @@
<?php
final class DiffusionInternalGitRawDiffQueryConduitAPIMethod
extends DiffusionQueryConduitAPIMethod {
public function isInternalAPI() {
return true;
}
public function getAPIMethodName() {
return 'diffusion.internal.gitrawdiffquery';
}
public function getMethodDescription() {
return pht('Internal method for getting raw diff information.');
}
protected function defineReturnType() {
return 'string';
}
protected function defineCustomParamTypes() {
return array(
'commit' => 'required string',
);
}
protected function getResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$commit = $request->getValue('commit');
if (!$repository->isGit()) {
throw new Exception(
pht(
'This API method can only be called on Git repositories.'));
}
// Check if the commit has parents. We're testing to see whether it is the
// first commit in history (in which case we must use "git log") or some
// other commit (in which case we can use "git diff"). We'd rather use
// "git diff" because it has the right behavior for merge commits, but
// it requires the commit to have a parent that we can diff against. The
// first commit doesn't, so "commit^" is not a valid ref.
list($parents) = $repository->execxLocalCommand(
- 'log -n1 --format=%s %s',
- '%P',
- $commit);
+ 'log -n1 %s %s --',
+ '--format=%P',
+ gitsprintf('%s', $commit));
$use_log = !strlen(trim($parents));
// First, get a fast raw diff without "--find-copies-harder". This flag
// produces better results for moves and copies, but is explosively slow
// for large changes to large repositories. See T10423.
$raw = $this->getRawDiff($repository, $commit, $use_log, false);
// If we got a normal-sized diff (no more than 100 modified files), we'll
// try using "--find-copies-harder" to improve the output. This improved
// output is mostly useful for small changes anyway.
$try_harder = (substr_count($raw, "\n") <= 100);
if ($try_harder) {
try {
$raw = $this->getRawDiff($repository, $commit, $use_log, true);
} catch (Exception $ex) {
// Just ignore any exception we hit, we'll use the fast output
// instead.
}
}
return $raw;
}
private function getRawDiff(
PhabricatorRepository $repository,
$commit,
$use_log,
$try_harder) {
$flags = array(
'-n1',
'-M',
'-C',
'-B',
'--raw',
'-t',
'--abbrev=40',
);
if ($try_harder) {
$flags[] = '--find-copies-harder';
}
if ($use_log) {
// This is the first commit so we need to use "log". We know it's not a
// merge commit because it couldn't be merging anything, so this is safe.
// NOTE: "--pretty=format: " is to disable diff output, we only want the
// part we get from "--raw".
$future = $repository->getLocalCommandFuture(
- 'log %Ls --pretty=format: %s',
+ 'log %Ls --pretty=format: %s --',
$flags,
- $commit);
+ gitsprintf('%s', $commit));
} else {
// Otherwise, we can use "diff", which will give us output for merges.
// We diff against the first parent, as this is generally the expectation
// and results in sensible behavior.
$future = $repository->getLocalCommandFuture(
- 'diff %Ls %s^1 %s',
+ 'diff %Ls %s %s --',
$flags,
- $commit,
- $commit);
+ gitsprintf('%s^1', $commit),
+ gitsprintf('%s', $commit));
}
// Don't spend more than 30 seconds generating the slower output.
if ($try_harder) {
$future->setTimeout(30);
}
list($raw) = $future->resolvex();
return $raw;
}
}
diff --git a/src/applications/diffusion/conduit/DiffusionLastModifiedQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionLastModifiedQueryConduitAPIMethod.php
index a15000bd97..8d404c656a 100644
--- a/src/applications/diffusion/conduit/DiffusionLastModifiedQueryConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionLastModifiedQueryConduitAPIMethod.php
@@ -1,168 +1,169 @@
<?php
final class DiffusionLastModifiedQueryConduitAPIMethod
extends DiffusionQueryConduitAPIMethod {
public function getAPIMethodName() {
return 'diffusion.lastmodifiedquery';
}
public function getMethodDescription() {
return pht('Get the commits at which paths were last modified.');
}
protected function defineReturnType() {
return 'map<string, string>';
}
protected function defineCustomParamTypes() {
return array(
'paths' => 'required map<string, string>',
);
}
protected function getGitResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$paths = $request->getValue('paths');
$results = $this->loadCommitsFromCache($paths);
foreach ($paths as $path => $commit) {
if (array_key_exists($path, $results)) {
continue;
}
list($hash) = $repository->execxLocalCommand(
- 'log -n1 --format=%%H %s -- %s',
+ 'log -n1 %s %s -- %s',
+ '--format=%H',
gitsprintf('%s', $commit),
$path);
$results[$path] = trim($hash);
}
return $results;
}
protected function getSVNResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$results = array();
foreach ($request->getValue('paths') as $path => $commit) {
$history_result = DiffusionQuery::callConduitWithDiffusionRequest(
$request->getUser(),
$drequest,
'diffusion.historyquery',
array(
'commit' => $commit,
'path' => $path,
'limit' => 1,
'offset' => 0,
'needDirectChanges' => true,
'needChildChanges' => true,
));
$history_array = DiffusionPathChange::newFromConduit(
$history_result['pathChanges']);
if ($history_array) {
$results[$path] = head($history_array)
->getCommit()
->getCommitIdentifier();
}
}
return $results;
}
protected function getMercurialResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$paths = $request->getValue('paths');
$results = $this->loadCommitsFromCache($paths);
foreach ($paths as $path => $commit) {
if (array_key_exists($path, $results)) {
continue;
}
list($hash) = $repository->execxLocalCommand(
'log --template %s --limit 1 --removed --rev %s -- %s',
'{node}',
hgsprintf('reverse(ancestors(%s))', $commit),
nonempty(ltrim($path, '/'), '.'));
$results[$path] = trim($hash);
}
return $results;
}
private function loadCommitsFromCache(array $map) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$path_map = id(new DiffusionPathIDQuery(array_keys($map)))
->loadPathIDs();
$commit_query = id(new DiffusionCommitQuery())
->setViewer($drequest->getUser())
->withRepository($repository)
->withIdentifiers(array_values($map));
$commit_query->execute();
$commit_map = $commit_query->getIdentifierMap();
$commit_map = mpull($commit_map, 'getID');
$graph_cache = new PhabricatorRepositoryGraphCache();
$results = array();
// Spend no more than this many total seconds trying to satisfy queries
// via the graph cache.
$remaining_time = 10.0;
foreach ($map as $path => $commit) {
$path_id = idx($path_map, $path);
if (!$path_id) {
continue;
}
$commit_id = idx($commit_map, $commit);
if (!$commit_id) {
continue;
}
$t_start = microtime(true);
$cache_result = $graph_cache->loadLastModifiedCommitID(
$commit_id,
$path_id,
$remaining_time);
$t_end = microtime(true);
if ($cache_result !== false) {
$results[$path] = $cache_result;
}
$remaining_time -= ($t_end - $t_start);
if ($remaining_time <= 0) {
break;
}
}
if ($results) {
$commits = id(new DiffusionCommitQuery())
->setViewer($drequest->getUser())
->withRepository($repository)
->withIDs($results)
->execute();
foreach ($results as $path => $id) {
$commit = idx($commits, $id);
if ($commit) {
$results[$path] = $commit->getCommitIdentifier();
} else {
unset($results[$path]);
}
}
}
return $results;
}
}
diff --git a/src/applications/diffusion/conduit/DiffusionMergedCommitsQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionMergedCommitsQueryConduitAPIMethod.php
index e02580dc73..b3851802fc 100644
--- a/src/applications/diffusion/conduit/DiffusionMergedCommitsQueryConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionMergedCommitsQueryConduitAPIMethod.php
@@ -1,111 +1,111 @@
<?php
final class DiffusionMergedCommitsQueryConduitAPIMethod
extends DiffusionQueryConduitAPIMethod {
public function getAPIMethodName() {
return 'diffusion.mergedcommitsquery';
}
public function getMethodDescription() {
return pht(
'Merged commit information for a specific commit in a repository.');
}
protected function defineReturnType() {
return 'array';
}
protected function defineCustomParamTypes() {
return array(
'commit' => 'required string',
'limit' => 'optional int',
);
}
private function getLimit(ConduitAPIRequest $request) {
// TODO: Paginate this sensibly at some point.
return $request->getValue('limit', 4096);
}
protected function getGitResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$commit = $request->getValue('commit');
$limit = $this->getLimit($request);
list($parents) = $repository->execxLocalCommand(
- 'log -n 1 --format=%s %s --',
- '%P',
+ 'log -n 1 %s %s --',
+ '--format=%P',
gitsprintf('%s', $commit));
$parents = preg_split('/\s+/', trim($parents));
if (count($parents) < 2) {
// This is not a merge commit, so it doesn't merge anything.
return array();
}
// Get all of the commits which are not reachable from the first parent.
// These are the commits this change merges.
$first_parent = head($parents);
list($logs) = $repository->execxLocalCommand(
- 'log -n %d --format=%s %s %s --',
+ 'log -n %d %s %s %s --',
// NOTE: "+ 1" accounts for the merge commit itself.
$limit + 1,
- '%H',
+ '--format=%H',
gitsprintf('%s', $commit),
gitsprintf('%s', '^'.$first_parent));
$hashes = explode("\n", trim($logs));
// Remove the merge commit.
$hashes = array_diff($hashes, array($commit));
$history = DiffusionQuery::loadHistoryForCommitIdentifiers(
$hashes,
$drequest);
return mpull($history, 'toDictionary');
}
protected function getMercurialResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$commit = $request->getValue('commit');
$limit = $this->getLimit($request);
list($parents) = $repository->execxLocalCommand(
'parents --template=%s --rev %s',
'{node}\\n',
hgsprintf('%s', $commit));
$parents = explode("\n", trim($parents));
if (count($parents) < 2) {
// Not a merge commit.
return array();
}
// NOTE: In Git, the first parent is the "mainline". In Mercurial, the
// second parent is the "mainline" (the way 'git merge' and 'hg merge'
// work is also reversed).
$last_parent = last($parents);
list($logs) = $repository->execxLocalCommand(
'log --template=%s --follow --limit %d --rev %s:0 --prune %s --',
'{node}\\n',
$limit + 1,
$commit,
$last_parent);
$hashes = explode("\n", trim($logs));
// Remove the merge commit.
$hashes = array_diff($hashes, array($commit));
$history = DiffusionQuery::loadHistoryForCommitIdentifiers(
$hashes,
$drequest);
return mpull($history, 'toDictionary');
}
}
diff --git a/src/applications/diffusion/conduit/DiffusionRefsQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionRefsQueryConduitAPIMethod.php
index 00bb28b385..8bdd2d8571 100644
--- a/src/applications/diffusion/conduit/DiffusionRefsQueryConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionRefsQueryConduitAPIMethod.php
@@ -1,59 +1,59 @@
<?php
final class DiffusionRefsQueryConduitAPIMethod
extends DiffusionQueryConduitAPIMethod {
public function getAPIMethodName() {
return 'diffusion.refsquery';
}
public function getMethodDescription() {
return pht(
'Query a git repository for ref information at a specific commit.');
}
protected function defineReturnType() {
return 'array';
}
protected function defineCustomParamTypes() {
return array(
'commit' => 'required string',
);
}
protected function getGitResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$commit = $request->getValue('commit');
list($stdout) = $repository->execxLocalCommand(
- 'log --format=%s -n 1 %s --',
- '%d',
- $commit);
+ 'log -n 1 %s %s --',
+ '--format=%d',
+ gitsprintf('%s', $commit));
// %d, gives a weird output format
// similar to (remote/one, remote/two, remote/three)
$refs = trim($stdout, "() \n");
if (!$refs) {
return array();
}
$refs = explode(',', $refs);
$refs = array_map('trim', $refs);
$ref_links = array();
foreach ($refs as $ref) {
$ref_links[] = array(
'ref' => $ref,
'href' => $drequest->generateURI(
array(
'action' => 'browse',
'branch' => $ref,
)),
);
}
return $ref_links;
}
}
diff --git a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php
index 6ebaf6bdc7..0f6b05b578 100644
--- a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php
+++ b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php
@@ -1,1479 +1,1479 @@
<?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->openTransaction();
$event->save();
foreach ($all_updates as $update) {
$update->setPushEventPHID($event->getPHID());
$update->save();
}
$event->saveTransaction();
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 --format=%s %s --not --all',
- '%H',
- $ref_update->getRefNew());
+ '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;
}
}
diff --git a/src/applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php b/src/applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php
index 8c8e362e60..f450a8adeb 100644
--- a/src/applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php
+++ b/src/applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php
@@ -1,18 +1,17 @@
<?php
final class DiffusionGitFileContentQuery extends DiffusionFileContentQuery {
protected function newQueryFuture() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$path = $drequest->getPath();
$commit = $drequest->getCommit();
return $repository->getLocalCommandFuture(
- 'cat-file blob -- %s:%s',
- $commit,
- $path);
+ 'cat-file blob -- %s',
+ sprintf('%s:%s', $commit, $path));
}
}
diff --git a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelCommitQuery.php b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelCommitQuery.php
index f4c31f7c24..8b9233f128 100644
--- a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelCommitQuery.php
+++ b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelCommitQuery.php
@@ -1,213 +1,223 @@
<?php
/**
* Populate a @{class:DiffusionCommitRef} with information about a specific
* commit in a repository. This is a low-level query which talks directly to
* the underlying VCS.
*/
final class DiffusionLowLevelCommitQuery
extends DiffusionLowLevelQuery {
private $identifier;
public function withIdentifier($identifier) {
$this->identifier = $identifier;
return $this;
}
protected function executeQuery() {
if (!strlen($this->identifier)) {
throw new PhutilInvalidStateException('withIdentifier');
}
$type = $this->getRepository()->getVersionControlSystem();
switch ($type) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$result = $this->loadGitCommitRef();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$result = $this->loadMercurialCommitRef();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$result = $this->loadSubversionCommitRef();
break;
default:
throw new Exception(pht('Unsupported repository type "%s"!', $type));
}
return $result;
}
private function loadGitCommitRef() {
$repository = $this->getRepository();
// See T5028. The "%B" (raw body) mode is not present in very old versions
// of Git. Use "%s" and "%b" ("subject" and "wrapped body") as an
// approximation.
$git_binary = PhutilBinaryAnalyzer::getForBinary('git');
$git_version = $git_binary->getBinaryVersion();
if (version_compare($git_version, '1.7.2', '>=')) {
$body_format = '%B';
$split_body = false;
} else {
$body_format = '%s%x00%b';
$split_body = true;
}
- // Even though we pass --encoding here, git doesn't always succeed, so
- // we try a little harder, since git *does* tell us what the actual encoding
- // is correctly (unless it doesn't; encoding is sometimes empty).
- list($info) = $repository->execxLocalCommand(
- 'log -n 1 --encoding=%s --format=%s %s --',
- 'UTF-8',
+ $argv = array();
+
+ $argv[] = '-n';
+ $argv[] = '1';
+
+ $argv[] = '--encoding=UTF-8';
+
+ $argv[] = sprintf(
+ '--format=%s',
implode(
'%x00',
array(
'%e',
'%cn',
'%ce',
'%an',
'%ae',
'%T',
'%at',
$body_format,
// The "git log" output includes a trailing newline. We want to
// faithfully capture only the exact text of the commit message,
// so include an explicit terminator: this makes sure the exact
// body text is surrounded by "\0" characters.
'~',
- )),
- $this->identifier);
+ )));
+
+ // Even though we pass --encoding here, git doesn't always succeed, so
+ // we try a little harder, since git *does* tell us what the actual encoding
+ // is correctly (unless it doesn't; encoding is sometimes empty).
+ list($info) = $repository->execxLocalCommand(
+ 'log -n 1 %Ls %s --',
+ $argv,
+ gitsprintf('%s', $this->identifier));
$parts = explode("\0", $info);
$encoding = array_shift($parts);
foreach ($parts as $key => $part) {
if ($encoding) {
$part = phutil_utf8_convert($part, 'UTF-8', $encoding);
}
$parts[$key] = phutil_utf8ize($part);
if (!strlen($parts[$key])) {
$parts[$key] = null;
}
}
$hashes = array(
id(new DiffusionCommitHash())
->setHashType(ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT)
->setHashValue($this->identifier),
id(new DiffusionCommitHash())
->setHashType(ArcanistDifferentialRevisionHash::HASH_GIT_TREE)
->setHashValue($parts[4]),
);
$author_epoch = (int)$parts[5];
if (!$author_epoch) {
$author_epoch = null;
}
if ($split_body) {
// Here, the body is: "subject", "\0", "wrapped body". Stitch the
// pieces back together by putting a newline between them if both
// parts are nonempty.
$head = $parts[6];
$tail = $parts[7];
if (strlen($head) && strlen($tail)) {
$body = $head."\n\n".$tail;
} else if (strlen($head)) {
$body = $head;
} else if (strlen($tail)) {
$body = $tail;
} else {
$body = '';
}
} else {
// Here, the body is the raw unwrapped body.
$body = $parts[6];
}
return id(new DiffusionCommitRef())
->setCommitterName($parts[0])
->setCommitterEmail($parts[1])
->setAuthorName($parts[2])
->setAuthorEmail($parts[3])
->setHashes($hashes)
->setAuthorEpoch($author_epoch)
->setMessage($body);
}
private function loadMercurialCommitRef() {
$repository = $this->getRepository();
list($stdout) = $repository->execxLocalCommand(
'log --template %s --rev %s',
'{author}\\n{desc}',
hgsprintf('%s', $this->identifier));
list($author, $message) = explode("\n", $stdout, 2);
$author = phutil_utf8ize($author);
$message = phutil_utf8ize($message);
list($author_name, $author_email) = $this->splitUserIdentifier($author);
$hashes = array(
id(new DiffusionCommitHash())
->setHashType(ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT)
->setHashValue($this->identifier),
);
return id(new DiffusionCommitRef())
->setAuthorName($author_name)
->setAuthorEmail($author_email)
->setMessage($message)
->setHashes($hashes);
}
private function loadSubversionCommitRef() {
$repository = $this->getRepository();
list($xml) = $repository->execxRemoteCommand(
'log --xml --limit 1 %s',
$repository->getSubversionPathURI(null, $this->identifier));
// Subversion may send us back commit messages which won't parse because
// they have non UTF-8 garbage in them. Slam them into valid UTF-8.
$xml = phutil_utf8ize($xml);
$log = new SimpleXMLElement($xml);
$entry = $log->logentry[0];
$author = (string)$entry->author;
$message = (string)$entry->msg;
list($author_name, $author_email) = $this->splitUserIdentifier($author);
// No hashes in Subversion.
$hashes = array();
return id(new DiffusionCommitRef())
->setAuthorName($author_name)
->setAuthorEmail($author_email)
->setMessage($message)
->setHashes($hashes);
}
private function splitUserIdentifier($user) {
$email = new PhutilEmailAddress($user);
if ($email->getDisplayName() || $email->getDomainName()) {
$user_name = $email->getDisplayName();
$user_email = $email->getAddress();
} else {
$user_name = $email->getAddress();
$user_email = null;
}
return array($user_name, $user_email);
}
}
diff --git a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelFilesizeQuery.php b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelFilesizeQuery.php
index 9d97a134ca..8984003711 100644
--- a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelFilesizeQuery.php
+++ b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelFilesizeQuery.php
@@ -1,125 +1,125 @@
<?php
final class DiffusionLowLevelFilesizeQuery
extends DiffusionLowLevelQuery {
private $identifier;
public function withIdentifier($identifier) {
$this->identifier = $identifier;
return $this;
}
protected function executeQuery() {
if (!strlen($this->identifier)) {
throw new PhutilInvalidStateException('withIdentifier');
}
$type = $this->getRepository()->getVersionControlSystem();
switch ($type) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$result = $this->loadGitFilesizes();
break;
default:
throw new Exception(pht('Unsupported repository type "%s"!', $type));
}
return $result;
}
private function loadGitFilesizes() {
$repository = $this->getRepository();
$identifier = $this->identifier;
$paths_future = $repository->getLocalCommandFuture(
'diff-tree -z -r --no-commit-id %s --',
- $identifier);
+ gitsprintf('%s', $identifier));
// With "-z" we get "<fields>\0<filename>\0" for each line. Process the
// delimited text as "<fields>, <filename>" pairs.
$path_lines = id(new LinesOfALargeExecFuture($paths_future))
->setDelimiter("\0");
$paths = array();
$path_pairs = new PhutilChunkedIterator($path_lines, 2);
foreach ($path_pairs as $path_pair) {
if (count($path_pair) != 2) {
throw new Exception(
pht(
'Unexpected number of output lines from "git diff-tree" when '.
'processing commit ("%s"): expected an even number of lines.',
$identifier));
}
list($fields, $pathname) = array_values($path_pair);
$fields = explode(' ', $fields);
// Fields are:
//
// :100644 100644 aaaa bbbb M
//
// [0] Old file mode.
// [1] New file mode.
// [2] Old object hash.
// [3] New object hash.
// [4] Change mode.
$paths[] = array(
'path' => $pathname,
'newHash' => $fields[3],
);
}
$path_sizes = array();
if (!$paths) {
return $path_sizes;
}
$check_paths = array();
foreach ($paths as $path) {
if ($path['newHash'] === DiffusionCommitHookEngine::EMPTY_HASH) {
$path_sizes[$path['path']] = 0;
continue;
}
$check_paths[$path['newHash']][] = $path['path'];
}
if (!$check_paths) {
return $path_sizes;
}
$future = $repository->getLocalCommandFuture(
'cat-file --batch-check=%s',
'%(objectsize)');
$future->write(implode("\n", array_keys($check_paths)));
$size_lines = id(new LinesOfALargeExecFuture($future))
->setDelimiter("\n");
foreach ($size_lines as $line) {
$object_size = (int)$line;
$object_hash = head_key($check_paths);
$path_names = $check_paths[$object_hash];
unset($check_paths[$object_hash]);
foreach ($path_names as $path_name) {
$path_sizes[$path_name] = $object_size;
}
}
if ($check_paths) {
throw new Exception(
pht(
'Unexpected number of output lines from "git cat-file" when '.
'processing commit ("%s").',
$identifier));
}
return $path_sizes;
}
}
diff --git a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php
index e0d9ef14b6..193f45a029 100644
--- a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php
+++ b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php
@@ -1,99 +1,99 @@
<?php
final class DiffusionLowLevelParentsQuery
extends DiffusionLowLevelQuery {
private $identifier;
public function withIdentifier($identifier) {
$this->identifier = $identifier;
return $this;
}
protected function executeQuery() {
if (!strlen($this->identifier)) {
throw new PhutilInvalidStateException('withIdentifier');
}
$type = $this->getRepository()->getVersionControlSystem();
switch ($type) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$result = $this->loadGitParents();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$result = $this->loadMercurialParents();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$result = $this->loadSubversionParents();
break;
default:
throw new Exception(pht('Unsupported repository type "%s"!', $type));
}
return $result;
}
private function loadGitParents() {
$repository = $this->getRepository();
list($stdout) = $repository->execxLocalCommand(
- 'log -n 1 --format=%s %s --',
- '%P',
+ 'log -n 1 %s %s --',
+ '--format=%P',
gitsprintf('%s', $this->identifier));
return preg_split('/\s+/', trim($stdout));
}
private function loadMercurialParents() {
$repository = $this->getRepository();
list($stdout) = $repository->execxLocalCommand(
'log --debug --limit 1 --template={parents} --rev %s',
$this->identifier);
$stdout = DiffusionMercurialCommandEngine::filterMercurialDebugOutput(
$stdout);
$hashes = preg_split('/\s+/', trim($stdout));
foreach ($hashes as $key => $value) {
// Mercurial parents look like "23:ad9f769d6f786fad9f76d9a" -- we want
// to strip out the local rev part.
list($local, $global) = explode(':', $value);
$hashes[$key] = $global;
// With --debug we get 40-character hashes but also get the "000000..."
// hash for missing parents; ignore it.
if (preg_match('/^0+$/', $global)) {
unset($hashes[$key]);
}
}
return $hashes;
}
private function loadSubversionParents() {
$repository = $this->getRepository();
$identifier = $this->identifier;
$refs = id(new DiffusionCachedResolveRefsQuery())
->setRepository($repository)
->withRefs(array($identifier))
->execute();
if (!$refs) {
throw new Exception(
pht(
'No commit "%s" in this repository.',
$identifier));
}
$n = (int)$identifier;
if ($n > 1) {
$ids = array($n - 1);
} else {
$ids = array();
}
return $ids;
}
}
diff --git a/src/applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php b/src/applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php
index f535724093..5983b2723e 100644
--- a/src/applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php
+++ b/src/applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php
@@ -1,50 +1,50 @@
<?php
final class DiffusionGitRawDiffQuery extends DiffusionRawDiffQuery {
protected function newQueryFuture() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$commit = $this->getAnchorCommit();
$options = array(
'-M',
'-C',
'--no-ext-diff',
'--no-color',
'--src-prefix=a/',
'--dst-prefix=b/',
'-U'.(int)$this->getLinesOfContext(),
);
$against = $this->getAgainstCommit();
if ($against === null) {
// Check if this is the root commit by seeing if it has parents, since
// `git diff X^ X` does not work if "X" is the initial commit.
list($parents) = $repository->execxLocalCommand(
- 'log -n 1 --format=%s %s --',
- '%P',
+ 'log -n 1 %s --',
+ '--format=%P',
gitsprintf('%s', $commit));
if (strlen(trim($parents))) {
$against = $commit.'^';
} else {
$against = ArcanistGitAPI::GIT_MAGIC_ROOT_COMMIT;
}
}
$path = $drequest->getPath();
if (!strlen($path)) {
$path = '.';
}
return $repository->getLocalCommandFuture(
'diff %Ls %s %s -- %s',
$options,
gitsprintf('%s', $against),
gitsprintf('%s', $commit),
$path);
}
}
diff --git a/src/applications/repository/daemon/PhabricatorGitGraphStream.php b/src/applications/repository/daemon/PhabricatorGitGraphStream.php
index b175053528..39b1fe39ed 100644
--- a/src/applications/repository/daemon/PhabricatorGitGraphStream.php
+++ b/src/applications/repository/daemon/PhabricatorGitGraphStream.php
@@ -1,105 +1,105 @@
<?php
final class PhabricatorGitGraphStream
extends PhabricatorRepositoryGraphStream {
private $repository;
private $iterator;
private $startCommit;
private $parents = array();
private $dates = array();
public function __construct(
PhabricatorRepository $repository,
$start_commit = null) {
$this->repository = $repository;
$this->startCommit = $start_commit;
if ($start_commit !== null) {
$future = $repository->getLocalCommandFuture(
- 'log --format=%s %s --',
- '%H%x01%P%x01%ct',
- $start_commit);
+ 'log %s %s --',
+ '--format=%H%x01%P%x01%ct',
+ gitsprintf('%s', $start_commit));
} else {
$future = $repository->getLocalCommandFuture(
- 'log --format=%s --all --',
- '%H%x01%P%x01%ct');
+ 'log %s --all --',
+ '--format=%H%x01%P%x01%ct');
}
$this->iterator = new LinesOfALargeExecFuture($future);
$this->iterator->setDelimiter("\n");
$this->iterator->rewind();
}
public function getParents($commit) {
if (!isset($this->parents[$commit])) {
$this->parseUntil($commit);
}
$parents = $this->parents[$commit];
// NOTE: In Git, it is possible for a commit to list the same parent more
// than once. See T5226. Discard duplicate parents.
return array_unique($parents);
}
public function getCommitDate($commit) {
if (!isset($this->dates[$commit])) {
$this->parseUntil($commit);
}
return $this->dates[$commit];
}
private function parseUntil($commit) {
if ($this->isParsed($commit)) {
return;
}
$gitlog = $this->iterator;
while ($gitlog->valid()) {
$line = $gitlog->current();
$gitlog->next();
$line = trim($line);
if (!strlen($line)) {
break;
}
list($hash, $parents, $epoch) = explode("\1", $line);
if ($parents) {
$parents = explode(' ', $parents);
} else {
// First commit.
$parents = array();
}
$this->dates[$hash] = $epoch;
$this->parents[$hash] = $parents;
if ($this->isParsed($commit)) {
return;
}
}
if ($this->startCommit !== null) {
throw new Exception(
pht(
'Commit "%s" is not a reachable ancestor of "%s".',
$commit,
$this->startCommit));
} else {
throw new Exception(
pht(
'Commit "%s" is not a reachable ancestor of any ref.',
$commit));
}
}
private function isParsed($commit) {
return isset($this->dates[$commit]);
}
}
diff --git a/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php b/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php
index a1b7ae5f55..0166e8e53c 100644
--- a/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php
+++ b/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php
@@ -1,721 +1,721 @@
<?php
/**
* Update the ref cursors for a repository, which track the positions of
* branches, bookmarks, and tags.
*/
final class PhabricatorRepositoryRefEngine
extends PhabricatorRepositoryEngine {
private $newPositions = array();
private $deadPositions = array();
private $closeCommits = array();
private $rebuild;
public function setRebuild($rebuild) {
$this->rebuild = $rebuild;
return $this;
}
public function getRebuild() {
return $this->rebuild;
}
public function updateRefs() {
$this->newPositions = array();
$this->deadPositions = array();
$this->closeCommits = array();
$repository = $this->getRepository();
$viewer = $this->getViewer();
$branches_may_close = false;
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// No meaningful refs of any type in Subversion.
$maps = array();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$branches = $this->loadMercurialBranchPositions($repository);
$bookmarks = $this->loadMercurialBookmarkPositions($repository);
$maps = array(
PhabricatorRepositoryRefCursor::TYPE_BRANCH => $branches,
PhabricatorRepositoryRefCursor::TYPE_BOOKMARK => $bookmarks,
);
$branches_may_close = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$maps = $this->loadGitRefPositions($repository);
break;
default:
throw new Exception(pht('Unknown VCS "%s"!', $vcs));
}
// Fill in any missing types with empty lists.
$maps = $maps + array(
PhabricatorRepositoryRefCursor::TYPE_BRANCH => array(),
PhabricatorRepositoryRefCursor::TYPE_TAG => array(),
PhabricatorRepositoryRefCursor::TYPE_BOOKMARK => array(),
PhabricatorRepositoryRefCursor::TYPE_REF => array(),
);
$all_cursors = id(new PhabricatorRepositoryRefCursorQuery())
->setViewer($viewer)
->withRepositoryPHIDs(array($repository->getPHID()))
->needPositions(true)
->execute();
$cursor_groups = mgroup($all_cursors, 'getRefType');
// Find all the heads of permanent refs.
$all_closing_heads = array();
foreach ($all_cursors as $cursor) {
// See T13284. Note that we're considering whether this ref was a
// permanent ref or not the last time we updated refs for this
// repository. This allows us to handle things properly when a ref
// is reconfigured from non-permanent to permanent.
$was_permanent = $cursor->getIsPermanent();
if (!$was_permanent) {
continue;
}
foreach ($cursor->getPositionIdentifiers() as $identifier) {
$all_closing_heads[] = $identifier;
}
}
$all_closing_heads = array_unique($all_closing_heads);
$all_closing_heads = $this->removeMissingCommits($all_closing_heads);
foreach ($maps as $type => $refs) {
$cursor_group = idx($cursor_groups, $type, array());
$this->updateCursors($cursor_group, $refs, $type, $all_closing_heads);
}
if ($this->closeCommits) {
$this->setCloseFlagOnCommits($this->closeCommits);
}
$save_cursors = $this->getCursorsForUpdate($all_cursors);
if ($this->newPositions || $this->deadPositions || $save_cursors) {
$repository->openTransaction();
$this->saveNewPositions();
$this->deleteDeadPositions();
foreach ($save_cursors as $cursor) {
$cursor->save();
}
$repository->saveTransaction();
}
$branches = $maps[PhabricatorRepositoryRefCursor::TYPE_BRANCH];
if ($branches && $branches_may_close) {
$this->updateBranchStates($repository, $branches);
}
}
private function getCursorsForUpdate(array $cursors) {
assert_instances_of($cursors, 'PhabricatorRepositoryRefCursor');
$results = array();
foreach ($cursors as $cursor) {
$ref_type = $cursor->getRefType();
$ref_name = $cursor->getRefName();
$is_permanent = $this->isPermanentRef($ref_type, $ref_name);
if ($is_permanent == $cursor->getIsPermanent()) {
continue;
}
$cursor->setIsPermanent((int)$is_permanent);
$results[] = $cursor;
}
return $results;
}
private function updateBranchStates(
PhabricatorRepository $repository,
array $branches) {
assert_instances_of($branches, 'DiffusionRepositoryRef');
$viewer = $this->getViewer();
$all_cursors = id(new PhabricatorRepositoryRefCursorQuery())
->setViewer($viewer)
->withRepositoryPHIDs(array($repository->getPHID()))
->needPositions(true)
->execute();
$state_map = array();
$type_branch = PhabricatorRepositoryRefCursor::TYPE_BRANCH;
foreach ($all_cursors as $cursor) {
if ($cursor->getRefType() !== $type_branch) {
continue;
}
$raw_name = $cursor->getRefNameRaw();
foreach ($cursor->getPositions() as $position) {
$hash = $position->getCommitIdentifier();
$state_map[$raw_name][$hash] = $position;
}
}
$updates = array();
foreach ($branches as $branch) {
$position = idx($state_map, $branch->getShortName(), array());
$position = idx($position, $branch->getCommitIdentifier());
if (!$position) {
continue;
}
$fields = $branch->getRawFields();
$position_state = (bool)$position->getIsClosed();
$branch_state = (bool)idx($fields, 'closed');
if ($position_state != $branch_state) {
$updates[$position->getID()] = (int)$branch_state;
}
}
if ($updates) {
$position_table = id(new PhabricatorRepositoryRefPosition());
$conn = $position_table->establishConnection('w');
$position_table->openTransaction();
foreach ($updates as $position_id => $branch_state) {
queryfx(
$conn,
'UPDATE %T SET isClosed = %d WHERE id = %d',
$position_table->getTableName(),
$branch_state,
$position_id);
}
$position_table->saveTransaction();
}
}
private function markPositionNew(
PhabricatorRepositoryRefPosition $position) {
$this->newPositions[] = $position;
return $this;
}
private function markPositionDead(
PhabricatorRepositoryRefPosition $position) {
$this->deadPositions[] = $position;
return $this;
}
private function markCloseCommits(array $identifiers) {
foreach ($identifiers as $identifier) {
$this->closeCommits[$identifier] = $identifier;
}
return $this;
}
/**
* Remove commits which no longer exist in the repository from a list.
*
* After a force push and garbage collection, we may have branch cursors which
* point at commits which no longer exist. This can make commands issued later
* fail. See T5839 for discussion.
*
* @param list<string> List of commit identifiers.
* @return list<string> List with nonexistent identifiers removed.
*/
private function removeMissingCommits(array $identifiers) {
if (!$identifiers) {
return array();
}
$resolved = id(new DiffusionLowLevelResolveRefsQuery())
->setRepository($this->getRepository())
->withRefs($identifiers)
->execute();
foreach ($identifiers as $key => $identifier) {
if (empty($resolved[$identifier])) {
unset($identifiers[$key]);
}
}
return $identifiers;
}
private function updateCursors(
array $cursors,
array $new_refs,
$ref_type,
array $all_closing_heads) {
$repository = $this->getRepository();
// NOTE: Mercurial branches may have multiple branch heads; this logic
// is complex primarily to account for that.
$cursors = mpull($cursors, null, 'getRefNameRaw');
// Group all the new ref values by their name. As above, these groups may
// have multiple members in Mercurial.
$ref_groups = mgroup($new_refs, 'getShortName');
foreach ($ref_groups as $name => $refs) {
$new_commits = mpull($refs, 'getCommitIdentifier', 'getCommitIdentifier');
$ref_cursor = idx($cursors, $name);
if ($ref_cursor) {
$old_positions = $ref_cursor->getPositions();
} else {
$old_positions = array();
}
// We're going to delete all the cursors pointing at commits which are
// no longer associated with the refs. This primarily makes the Mercurial
// multiple head case easier, and means that when we update a ref we
// delete the old one and write a new one.
foreach ($old_positions as $old_position) {
$hash = $old_position->getCommitIdentifier();
if (isset($new_commits[$hash])) {
// This ref previously pointed at this commit, and still does.
$this->log(
pht(
'Ref %s "%s" still points at %s.',
$ref_type,
$name,
$hash));
continue;
}
// This ref previously pointed at this commit, but no longer does.
$this->log(
pht(
'Ref %s "%s" no longer points at %s.',
$ref_type,
$name,
$hash));
// Nuke the obsolete cursor.
$this->markPositionDead($old_position);
}
// Now, we're going to insert new cursors for all the commits which are
// associated with this ref that don't currently have cursors.
$old_commits = mpull($old_positions, 'getCommitIdentifier');
$old_commits = array_fuse($old_commits);
$added_commits = array_diff_key($new_commits, $old_commits);
foreach ($added_commits as $identifier) {
$this->log(
pht(
'Ref %s "%s" now points at %s.',
$ref_type,
$name,
$identifier));
if (!$ref_cursor) {
// If this is the first time we've seen a particular ref (for
// example, a new branch) we need to insert a RefCursor record
// for it before we can insert a RefPosition.
$ref_cursor = $this->newRefCursor(
$repository,
$ref_type,
$name);
}
$new_position = id(new PhabricatorRepositoryRefPosition())
->setCursorID($ref_cursor->getID())
->setCommitIdentifier($identifier)
->setIsClosed(0);
$this->markPositionNew($new_position);
}
if ($this->isPermanentRef($ref_type, $name)) {
// See T13284. If this cursor was already marked as permanent, we
// only need to publish the newly created ref positions. However, if
// this cursor was not previously permanent but has become permanent,
// we need to publish all the ref positions.
// This corresponds to users reconfiguring a branch to make it
// permanent without pushing any new commits to it.
$is_rebuild = $this->getRebuild();
$was_permanent = $ref_cursor->getIsPermanent();
if ($is_rebuild || !$was_permanent) {
$update_all = true;
} else {
$update_all = false;
}
if ($update_all) {
$update_commits = $new_commits;
} else {
$update_commits = $added_commits;
}
if ($is_rebuild) {
$exclude = array();
} else {
$exclude = $all_closing_heads;
}
foreach ($update_commits as $identifier) {
$new_identifiers = $this->loadNewCommitIdentifiers(
$identifier,
$exclude);
$this->markCloseCommits($new_identifiers);
}
}
}
// Find any cursors for refs which no longer exist. This happens when a
// branch, tag or bookmark is deleted.
foreach ($cursors as $name => $cursor) {
if (!empty($ref_groups[$name])) {
// This ref still has some positions, so we don't need to wipe it
// out. Try the next one.
continue;
}
foreach ($cursor->getPositions() as $position) {
$this->log(
pht(
'Ref %s "%s" no longer exists.',
$cursor->getRefType(),
$cursor->getRefName()));
$this->markPositionDead($position);
}
}
}
private function isPermanentRef($ref_type, $ref_name) {
if ($ref_type !== PhabricatorRepositoryRefCursor::TYPE_BRANCH) {
return false;
}
return $this->getRepository()->isBranchPermanentRef($ref_name);
}
/**
* Find all ancestors of a new closing branch head which are not ancestors
* of any old closing branch head.
*/
private function loadNewCommitIdentifiers(
$new_head,
array $all_closing_heads) {
$repository = $this->getRepository();
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
if ($all_closing_heads) {
$parts = array();
foreach ($all_closing_heads as $head) {
$parts[] = hgsprintf('%s', $head);
}
// See T5896. Mercurial can not parse an "X or Y or ..." rev list
// with more than about 300 items, because it exceeds the maximum
// allowed recursion depth. Split all the heads into chunks of
// 256, and build a query like this:
//
// ((1 or 2 or ... or 255) or (256 or 257 or ... 511))
//
// If we have more than 65535 heads, we'll do that again:
//
// (((1 or ...) or ...) or ((65536 or ...) or ...))
$chunk_size = 256;
while (count($parts) > $chunk_size) {
$chunks = array_chunk($parts, $chunk_size);
foreach ($chunks as $key => $chunk) {
$chunks[$key] = '('.implode(' or ', $chunk).')';
}
$parts = array_values($chunks);
}
$parts = '('.implode(' or ', $parts).')';
list($stdout) = $this->getRepository()->execxLocalCommand(
'log --template %s --rev %s',
'{node}\n',
hgsprintf('%s', $new_head).' - '.$parts);
} else {
list($stdout) = $this->getRepository()->execxLocalCommand(
'log --template %s --rev %s',
'{node}\n',
hgsprintf('%s', $new_head));
}
$stdout = trim($stdout);
if (!strlen($stdout)) {
return array();
}
return phutil_split_lines($stdout, $retain_newlines = false);
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
if ($all_closing_heads) {
// See PHI1474. This length of list may exceed the maximum size of
// a command line argument list, so pipe the list in using "--stdin"
// instead.
$ref_list = array();
$ref_list[] = $new_head;
foreach ($all_closing_heads as $old_head) {
$ref_list[] = '^'.$old_head;
}
$ref_list[] = '--';
$ref_list = implode("\n", $ref_list)."\n";
$future = $this->getRepository()->getLocalCommandFuture(
- 'log --format=%s --stdin',
- '%H');
+ 'log %s --stdin --',
+ '--format=%H');
list($stdout) = $future
->write($ref_list)
->resolvex();
} else {
list($stdout) = $this->getRepository()->execxLocalCommand(
- 'log --format=%s %s',
- '%H',
- $new_head);
+ 'log %s %s --',
+ '--format=%H',
+ gitsprintf('%s', $new_head));
}
$stdout = trim($stdout);
if (!strlen($stdout)) {
return array();
}
return phutil_split_lines($stdout, $retain_newlines = false);
default:
throw new Exception(pht('Unsupported VCS "%s"!', $vcs));
}
}
/**
* Mark a list of commits as closeable, and queue workers for those commits
* which don't already have the flag.
*/
private function setCloseFlagOnCommits(array $identifiers) {
$repository = $this->getRepository();
$commit_table = new PhabricatorRepositoryCommit();
$conn = $commit_table->establishConnection('w');
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$class = 'PhabricatorRepositoryGitCommitMessageParserWorker';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$class = 'PhabricatorRepositorySvnCommitMessageParserWorker';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$class = 'PhabricatorRepositoryMercurialCommitMessageParserWorker';
break;
default:
throw new Exception(pht("Unknown repository type '%s'!", $vcs));
}
$identifier_tokens = array();
foreach ($identifiers as $identifier) {
$identifier_tokens[] = qsprintf(
$conn,
'%s',
$identifier);
}
$all_commits = array();
foreach (PhabricatorLiskDAO::chunkSQL($identifier_tokens) as $chunk) {
$rows = queryfx_all(
$conn,
'SELECT id, phid, commitIdentifier, importStatus FROM %T
WHERE repositoryID = %d AND commitIdentifier IN (%LQ)',
$commit_table->getTableName(),
$repository->getID(),
$chunk);
foreach ($rows as $row) {
$all_commits[] = $row;
}
}
$closeable_flag = PhabricatorRepositoryCommit::IMPORTED_CLOSEABLE;
$published_flag = PhabricatorRepositoryCommit::IMPORTED_PUBLISH;
$all_commits = ipull($all_commits, null, 'commitIdentifier');
foreach ($identifiers as $identifier) {
$row = idx($all_commits, $identifier);
if (!$row) {
throw new Exception(
pht(
'Commit "%s" has not been discovered yet! Run discovery before '.
'updating refs.',
$identifier));
}
$import_status = $row['importStatus'];
if (!($import_status & $closeable_flag)) {
// Set the "closeable" flag.
$import_status = ($import_status | $closeable_flag);
// See T13580. Clear the "published" flag, so publishing executes
// again. We may have previously performed a no-op "publish" on the
// commit to make sure it has all bits in the "IMPORTED_ALL" bitmask.
$import_status = ($import_status & ~$published_flag);
queryfx(
$conn,
'UPDATE %T SET importStatus = %d WHERE id = %d',
$commit_table->getTableName(),
$import_status,
$row['id']);
$data = array(
'commitID' => $row['id'],
);
PhabricatorWorker::scheduleTask(
$class,
$data,
array(
'priority' => PhabricatorWorker::PRIORITY_COMMIT,
'objectPHID' => $row['phid'],
));
}
}
return $this;
}
private function newRefCursor(
PhabricatorRepository $repository,
$ref_type,
$ref_name) {
$is_permanent = $this->isPermanentRef($ref_type, $ref_name);
$cursor = id(new PhabricatorRepositoryRefCursor())
->setRepositoryPHID($repository->getPHID())
->setRefType($ref_type)
->setRefName($ref_name)
->setIsPermanent((int)$is_permanent);
try {
return $cursor->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
// If we raced another daemon to create this position and lost the race,
// load the cursor the other daemon created instead.
}
$viewer = $this->getViewer();
$cursor = id(new PhabricatorRepositoryRefCursorQuery())
->setViewer($viewer)
->withRepositoryPHIDs(array($repository->getPHID()))
->withRefTypes(array($ref_type))
->withRefNames(array($ref_name))
->needPositions(true)
->executeOne();
if (!$cursor) {
throw new Exception(
pht(
'Failed to create a new ref cursor (for "%s", of type "%s", in '.
'repository "%s") because it collided with an existing cursor, '.
'but then failed to load that cursor.',
$ref_name,
$ref_type,
$repository->getDisplayName()));
}
return $cursor;
}
private function saveNewPositions() {
$positions = $this->newPositions;
foreach ($positions as $position) {
try {
$position->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
// We may race another daemon to create this position. If we do, and
// we lose the race, that's fine: the other daemon did our work for
// us and we can continue.
}
}
$this->newPositions = array();
}
private function deleteDeadPositions() {
$positions = $this->deadPositions;
$repository = $this->getRepository();
foreach ($positions as $position) {
// Shove this ref into the old refs table so the discovery engine
// can check if any commits have been rendered unreachable.
id(new PhabricatorRepositoryOldRef())
->setRepositoryPHID($repository->getPHID())
->setCommitIdentifier($position->getCommitIdentifier())
->save();
$position->delete();
}
$this->deadPositions = array();
}
/* -( Updating Git Refs )-------------------------------------------------- */
/**
* @task git
*/
private function loadGitRefPositions(PhabricatorRepository $repository) {
$refs = id(new DiffusionLowLevelGitRefQuery())
->setRepository($repository)
->execute();
return mgroup($refs, 'getRefType');
}
/* -( Updating Mercurial Refs )-------------------------------------------- */
/**
* @task hg
*/
private function loadMercurialBranchPositions(
PhabricatorRepository $repository) {
return id(new DiffusionLowLevelMercurialBranchesQuery())
->setRepository($repository)
->execute();
}
/**
* @task hg
*/
private function loadMercurialBookmarkPositions(
PhabricatorRepository $repository) {
// TODO: Implement support for Mercurial bookmarks.
return array();
}
}
diff --git a/src/applications/repository/engine/__tests__/PhabricatorWorkingCopyDiscoveryTestCase.php b/src/applications/repository/engine/__tests__/PhabricatorWorkingCopyDiscoveryTestCase.php
index a15cb3d79f..cc94e558e4 100644
--- a/src/applications/repository/engine/__tests__/PhabricatorWorkingCopyDiscoveryTestCase.php
+++ b/src/applications/repository/engine/__tests__/PhabricatorWorkingCopyDiscoveryTestCase.php
@@ -1,55 +1,57 @@
<?php
final class PhabricatorWorkingCopyDiscoveryTestCase
extends PhabricatorWorkingCopyTestCase {
public function testSubversionCommitDiscovery() {
+ $this->requireBinaryForTest('svn');
+
$refs = $this->discoverRefs('ST');
$this->assertEqual(
array(
1368319433,
1368319448,
),
mpull($refs, 'getEpoch'),
pht('Commit Epochs'));
}
public function testMercurialCommitDiscovery() {
$this->requireBinaryForTest('hg');
$refs = $this->discoverRefs('HT');
$this->assertEqual(
array(
'4a110ae879f473f2e82ffd032475caedd6cdba91',
),
mpull($refs, 'getIdentifier'));
}
public function testGitCommitDiscovery() {
$refs = $this->discoverRefs('GT');
$this->assertEqual(
array(
'763d4ab372445551c95fb5cccd1a7a223f5b2ac8',
'41fa35914aa19c1aa6e57004d9745c05929c3563',
),
mpull($refs, 'getIdentifier'));
}
private function discoverRefs($callsign) {
$repo = $this->buildPulledRepository($callsign);
$engine = id(new PhabricatorRepositoryDiscoveryEngine())
->setRepository($repo);
$refs = $engine->discoverCommits($repo);
// The next time through, these should be cached as already discovered.
$new_refs = $engine->discoverCommits($repo);
$this->assertEqual(array(), $new_refs);
return $refs;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jul 27, 11:37 AM (6 d, 7 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
185670
Default Alt Text
(135 KB)

Event Timeline