Summary:
Depends on D20530. Ref T13291. When users paste links to files in Diffusion into remarkup contexts, identify them and specialize the rendering.
When the URIs are embedded with `{...}`, parse them in more detail.
This is a lead-up to a `{src ...}` rule which will use the same `View` but give users more options to customize presentation.
Test Plan: {F6463580}
Reviewers: amckinley
Reviewed By: amckinley
Maniphest Tasks: T13291
Differential Revision: https://secure.phabricator.com/D20538
699 lines
18 KiB
PHP
699 lines
18 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Contains logic to parse Diffusion requests, which have a complicated URI
|
|
* structure.
|
|
*
|
|
* @task new Creating Requests
|
|
* @task uri Managing Diffusion URIs
|
|
*/
|
|
abstract class DiffusionRequest extends Phobject {
|
|
|
|
protected $path;
|
|
protected $line;
|
|
protected $branch;
|
|
protected $lint;
|
|
|
|
protected $symbolicCommit;
|
|
protected $symbolicType;
|
|
protected $stableCommit;
|
|
|
|
protected $repository;
|
|
protected $repositoryCommit;
|
|
protected $repositoryCommitData;
|
|
|
|
private $isClusterRequest = false;
|
|
private $initFromConduit = true;
|
|
private $user;
|
|
private $branchObject = false;
|
|
private $refAlternatives;
|
|
|
|
final public function supportsBranches() {
|
|
return $this->getRepository()->supportsRefs();
|
|
}
|
|
|
|
abstract protected function isStableCommit($symbol);
|
|
|
|
protected function didInitialize() {
|
|
return null;
|
|
}
|
|
|
|
|
|
/* -( Creating Requests )-------------------------------------------------- */
|
|
|
|
|
|
/**
|
|
* Create a new synthetic request from a parameter dictionary. If you need
|
|
* a @{class:DiffusionRequest} object in order to issue a DiffusionQuery, you
|
|
* can use this method to build one.
|
|
*
|
|
* Parameters are:
|
|
*
|
|
* - `repository` Repository object or identifier.
|
|
* - `user` Viewing user. Required if `repository` is an identifier.
|
|
* - `branch` Optional, branch name.
|
|
* - `path` Optional, file path.
|
|
* - `commit` Optional, commit identifier.
|
|
* - `line` Optional, line range.
|
|
*
|
|
* @param map See documentation.
|
|
* @return DiffusionRequest New request object.
|
|
* @task new
|
|
*/
|
|
final public static function newFromDictionary(array $data) {
|
|
$repository_key = 'repository';
|
|
$identifier_key = 'callsign';
|
|
$viewer_key = 'user';
|
|
|
|
$repository = idx($data, $repository_key);
|
|
$identifier = idx($data, $identifier_key);
|
|
|
|
$have_repository = ($repository !== null);
|
|
$have_identifier = ($identifier !== null);
|
|
|
|
if ($have_repository && $have_identifier) {
|
|
throw new Exception(
|
|
pht(
|
|
'Specify "%s" or "%s", but not both.',
|
|
$repository_key,
|
|
$identifier_key));
|
|
}
|
|
|
|
if (!$have_repository && !$have_identifier) {
|
|
throw new Exception(
|
|
pht(
|
|
'One of "%s" and "%s" is required.',
|
|
$repository_key,
|
|
$identifier_key));
|
|
}
|
|
|
|
if ($have_repository) {
|
|
if (!($repository instanceof PhabricatorRepository)) {
|
|
if (empty($data[$viewer_key])) {
|
|
throw new Exception(
|
|
pht(
|
|
'Parameter "%s" is required if "%s" is provided.',
|
|
$viewer_key,
|
|
$identifier_key));
|
|
}
|
|
|
|
$identifier = $repository;
|
|
$repository = null;
|
|
}
|
|
}
|
|
|
|
if ($identifier !== null) {
|
|
$object = self::newFromIdentifier(
|
|
$identifier,
|
|
$data[$viewer_key],
|
|
idx($data, 'edit'));
|
|
} else {
|
|
$object = self::newFromRepository($repository);
|
|
}
|
|
|
|
if (!$object) {
|
|
return null;
|
|
}
|
|
|
|
$object->initializeFromDictionary($data);
|
|
|
|
return $object;
|
|
}
|
|
|
|
/**
|
|
* Internal.
|
|
*
|
|
* @task new
|
|
*/
|
|
final private function __construct() {
|
|
// <private>
|
|
}
|
|
|
|
|
|
/**
|
|
* Internal. Use @{method:newFromDictionary}, not this method.
|
|
*
|
|
* @param string Repository identifier.
|
|
* @param PhabricatorUser Viewing user.
|
|
* @return DiffusionRequest New request object.
|
|
* @task new
|
|
*/
|
|
final private static function newFromIdentifier(
|
|
$identifier,
|
|
PhabricatorUser $viewer,
|
|
$need_edit = false) {
|
|
|
|
$query = id(new PhabricatorRepositoryQuery())
|
|
->setViewer($viewer)
|
|
->withIdentifiers(array($identifier))
|
|
->needProfileImage(true)
|
|
->needURIs(true);
|
|
|
|
if ($need_edit) {
|
|
$query->requireCapabilities(
|
|
array(
|
|
PhabricatorPolicyCapability::CAN_VIEW,
|
|
PhabricatorPolicyCapability::CAN_EDIT,
|
|
));
|
|
}
|
|
|
|
$repository = $query->executeOne();
|
|
|
|
if (!$repository) {
|
|
return null;
|
|
}
|
|
|
|
return self::newFromRepository($repository);
|
|
}
|
|
|
|
|
|
/**
|
|
* Internal. Use @{method:newFromDictionary}, not this method.
|
|
*
|
|
* @param PhabricatorRepository Repository object.
|
|
* @return DiffusionRequest New request object.
|
|
* @task new
|
|
*/
|
|
final private static function newFromRepository(
|
|
PhabricatorRepository $repository) {
|
|
|
|
$map = array(
|
|
PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'DiffusionGitRequest',
|
|
PhabricatorRepositoryType::REPOSITORY_TYPE_SVN => 'DiffusionSvnRequest',
|
|
PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL =>
|
|
'DiffusionMercurialRequest',
|
|
);
|
|
|
|
$class = idx($map, $repository->getVersionControlSystem());
|
|
|
|
if (!$class) {
|
|
throw new Exception(pht('Unknown version control system!'));
|
|
}
|
|
|
|
$object = new $class();
|
|
|
|
$object->repository = $repository;
|
|
|
|
return $object;
|
|
}
|
|
|
|
|
|
/**
|
|
* Internal. Use @{method:newFromDictionary}, not this method.
|
|
*
|
|
* @param map Map of parsed data.
|
|
* @return void
|
|
* @task new
|
|
*/
|
|
final private function initializeFromDictionary(array $data) {
|
|
$blob = idx($data, 'blob');
|
|
if (strlen($blob)) {
|
|
$blob = self::parseRequestBlob($blob, $this->supportsBranches());
|
|
$data = $blob + $data;
|
|
}
|
|
|
|
$this->path = idx($data, 'path');
|
|
$this->line = idx($data, 'line');
|
|
$this->initFromConduit = idx($data, 'initFromConduit', true);
|
|
$this->lint = idx($data, 'lint');
|
|
|
|
$this->symbolicCommit = idx($data, 'commit');
|
|
if ($this->supportsBranches()) {
|
|
$this->branch = idx($data, 'branch');
|
|
}
|
|
|
|
if (!$this->getUser()) {
|
|
$user = idx($data, 'user');
|
|
if (!$user) {
|
|
throw new Exception(
|
|
pht(
|
|
'You must provide a %s in the dictionary!',
|
|
'PhabricatorUser'));
|
|
}
|
|
$this->setUser($user);
|
|
}
|
|
|
|
$this->didInitialize();
|
|
}
|
|
|
|
final public function setUser(PhabricatorUser $user) {
|
|
$this->user = $user;
|
|
return $this;
|
|
}
|
|
final public function getUser() {
|
|
return $this->user;
|
|
}
|
|
|
|
public function getRepository() {
|
|
return $this->repository;
|
|
}
|
|
|
|
public function setPath($path) {
|
|
$this->path = $path;
|
|
return $this;
|
|
}
|
|
|
|
public function getPath() {
|
|
return $this->path;
|
|
}
|
|
|
|
public function getLine() {
|
|
return $this->line;
|
|
}
|
|
|
|
public function getCommit() {
|
|
|
|
// TODO: Probably remove all of this.
|
|
|
|
if ($this->getSymbolicCommit() !== null) {
|
|
return $this->getSymbolicCommit();
|
|
}
|
|
|
|
return $this->getStableCommit();
|
|
}
|
|
|
|
/**
|
|
* Get the symbolic commit associated with this request.
|
|
*
|
|
* A symbolic commit may be a commit hash, an abbreviated commit hash, a
|
|
* branch name, a tag name, or an expression like "HEAD^^^". The symbolic
|
|
* commit may also be absent.
|
|
*
|
|
* This method always returns the symbol present in the original request,
|
|
* in unmodified form.
|
|
*
|
|
* See also @{method:getStableCommit}.
|
|
*
|
|
* @return string|null Symbolic commit, if one was present in the request.
|
|
*/
|
|
public function getSymbolicCommit() {
|
|
return $this->symbolicCommit;
|
|
}
|
|
|
|
|
|
/**
|
|
* Modify the request to move the symbolic commit elsewhere.
|
|
*
|
|
* @param string New symbolic commit.
|
|
* @return this
|
|
*/
|
|
public function updateSymbolicCommit($symbol) {
|
|
$this->symbolicCommit = $symbol;
|
|
$this->symbolicType = null;
|
|
$this->stableCommit = null;
|
|
return $this;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the ref type (`commit` or `tag`) of the location associated with this
|
|
* request.
|
|
*
|
|
* If a symbolic commit is present in the request, this method identifies
|
|
* the type of the symbol. Otherwise, it identifies the type of symbol of
|
|
* the location the request is implicitly associated with. This will probably
|
|
* always be `commit`.
|
|
*
|
|
* @return string Symbolic commit type (`commit` or `tag`).
|
|
*/
|
|
public function getSymbolicType() {
|
|
if ($this->symbolicType === null) {
|
|
// As a side effect, this resolves the symbolic type.
|
|
$this->getStableCommit();
|
|
}
|
|
return $this->symbolicType;
|
|
}
|
|
|
|
|
|
/**
|
|
* Retrieve the stable, permanent commit name identifying the repository
|
|
* location associated with this request.
|
|
*
|
|
* This returns a non-symbolic identifier for the current commit: in Git and
|
|
* Mercurial, a 40-character SHA1; in SVN, a revision number.
|
|
*
|
|
* See also @{method:getSymbolicCommit}.
|
|
*
|
|
* @return string Stable commit name, like a git hash or SVN revision. Not
|
|
* a symbolic commit reference.
|
|
*/
|
|
public function getStableCommit() {
|
|
if (!$this->stableCommit) {
|
|
if ($this->isStableCommit($this->symbolicCommit)) {
|
|
$this->stableCommit = $this->symbolicCommit;
|
|
$this->symbolicType = 'commit';
|
|
} else {
|
|
$this->queryStableCommit();
|
|
}
|
|
}
|
|
return $this->stableCommit;
|
|
}
|
|
|
|
|
|
public function getBranch() {
|
|
return $this->branch;
|
|
}
|
|
|
|
public function getLint() {
|
|
return $this->lint;
|
|
}
|
|
|
|
protected function getArcanistBranch() {
|
|
return $this->getBranch();
|
|
}
|
|
|
|
public function loadBranch() {
|
|
// TODO: Get rid of this and do real Queries on real objects.
|
|
|
|
if ($this->branchObject === false) {
|
|
$this->branchObject = PhabricatorRepositoryBranch::loadBranch(
|
|
$this->getRepository()->getID(),
|
|
$this->getArcanistBranch());
|
|
}
|
|
|
|
return $this->branchObject;
|
|
}
|
|
|
|
public function loadCoverage() {
|
|
// TODO: This should also die.
|
|
$branch = $this->loadBranch();
|
|
if (!$branch) {
|
|
return;
|
|
}
|
|
|
|
$path = $this->getPath();
|
|
$path_map = id(new DiffusionPathIDQuery(array($path)))->loadPathIDs();
|
|
|
|
$coverage_row = queryfx_one(
|
|
id(new PhabricatorRepository())->establishConnection('r'),
|
|
'SELECT * FROM %T WHERE branchID = %d AND pathID = %d
|
|
ORDER BY commitID DESC LIMIT 1',
|
|
'repository_coverage',
|
|
$branch->getID(),
|
|
$path_map[$path]);
|
|
|
|
if (!$coverage_row) {
|
|
return null;
|
|
}
|
|
|
|
return idx($coverage_row, 'coverage');
|
|
}
|
|
|
|
|
|
public function loadCommit() {
|
|
if (empty($this->repositoryCommit)) {
|
|
$repository = $this->getRepository();
|
|
|
|
$commit = id(new DiffusionCommitQuery())
|
|
->setViewer($this->getUser())
|
|
->withRepository($repository)
|
|
->withIdentifiers(array($this->getStableCommit()))
|
|
->executeOne();
|
|
if ($commit) {
|
|
$commit->attachRepository($repository);
|
|
}
|
|
$this->repositoryCommit = $commit;
|
|
}
|
|
return $this->repositoryCommit;
|
|
}
|
|
|
|
public function loadCommitData() {
|
|
if (empty($this->repositoryCommitData)) {
|
|
$commit = $this->loadCommit();
|
|
$data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
|
|
'commitID = %d',
|
|
$commit->getID());
|
|
if (!$data) {
|
|
$data = new PhabricatorRepositoryCommitData();
|
|
$data->setCommitMessage(
|
|
pht('(This commit has not been fully parsed yet.)'));
|
|
}
|
|
$this->repositoryCommitData = $data;
|
|
}
|
|
return $this->repositoryCommitData;
|
|
}
|
|
|
|
/* -( Managing Diffusion URIs )-------------------------------------------- */
|
|
|
|
|
|
public function generateURI(array $params) {
|
|
if (empty($params['stable'])) {
|
|
$default_commit = $this->getSymbolicCommit();
|
|
} else {
|
|
$default_commit = $this->getStableCommit();
|
|
}
|
|
|
|
$defaults = array(
|
|
'path' => $this->getPath(),
|
|
'branch' => $this->getBranch(),
|
|
'commit' => $default_commit,
|
|
'lint' => idx($params, 'lint', $this->getLint()),
|
|
);
|
|
|
|
foreach ($defaults as $key => $val) {
|
|
if (!isset($params[$key])) { // Overwrite NULL.
|
|
$params[$key] = $val;
|
|
}
|
|
}
|
|
|
|
return $this->getRepository()->generateURI($params);
|
|
}
|
|
|
|
/**
|
|
* Internal. Public only for unit tests.
|
|
*
|
|
* Parse the request URI into components.
|
|
*
|
|
* @param string URI blob.
|
|
* @param bool True if this VCS supports branches.
|
|
* @return map Parsed URI.
|
|
*
|
|
* @task uri
|
|
*/
|
|
public static function parseRequestBlob($blob, $supports_branches) {
|
|
$result = array(
|
|
'branch' => null,
|
|
'path' => null,
|
|
'commit' => null,
|
|
'line' => null,
|
|
);
|
|
|
|
$matches = null;
|
|
|
|
if ($supports_branches) {
|
|
// Consume the front part of the URI, up to the first "/". This is the
|
|
// path-component encoded branch name.
|
|
if (preg_match('@^([^/]+)/@', $blob, $matches)) {
|
|
$result['branch'] = phutil_unescape_uri_path_component($matches[1]);
|
|
$blob = substr($blob, strlen($matches[1]) + 1);
|
|
}
|
|
}
|
|
|
|
// Consume the back part of the URI, up to the first "$". Use a negative
|
|
// lookbehind to prevent matching '$$'. We double the '$' symbol when
|
|
// encoding so that files with names like "money/$100" will survive.
|
|
$pattern = '@(?:(?:^|[^$])(?:[$][$])*)[$]([\d,-]+)$@';
|
|
if (preg_match($pattern, $blob, $matches)) {
|
|
$result['line'] = $matches[1];
|
|
$blob = substr($blob, 0, -(strlen($matches[1]) + 1));
|
|
}
|
|
|
|
// We've consumed the line number if it exists, so unescape "$" in the
|
|
// rest of the string.
|
|
$blob = str_replace('$$', '$', $blob);
|
|
|
|
// Consume the commit name, stopping on ';;'. We allow any character to
|
|
// appear in commits names, as they can sometimes be symbolic names (like
|
|
// tag names or refs).
|
|
if (preg_match('@(?:(?:^|[^;])(?:;;)*);([^;].*)$@', $blob, $matches)) {
|
|
$result['commit'] = $matches[1];
|
|
$blob = substr($blob, 0, -(strlen($matches[1]) + 1));
|
|
}
|
|
|
|
// We've consumed the commit if it exists, so unescape ";" in the rest
|
|
// of the string.
|
|
$blob = str_replace(';;', ';', $blob);
|
|
|
|
if (strlen($blob)) {
|
|
$result['path'] = $blob;
|
|
}
|
|
|
|
$parts = explode('/', $result['path']);
|
|
foreach ($parts as $part) {
|
|
// Prevent any hyjinx since we're ultimately shipping this to the
|
|
// filesystem under a lot of workflows.
|
|
if ($part == '..') {
|
|
throw new Exception(pht('Invalid path URI.'));
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Check that the working copy of the repository is present and readable.
|
|
*
|
|
* @param string Path to the working copy.
|
|
*/
|
|
protected function validateWorkingCopy($path) {
|
|
if (!is_readable(dirname($path))) {
|
|
$this->raisePermissionException();
|
|
}
|
|
|
|
if (!Filesystem::pathExists($path)) {
|
|
$this->raiseCloneException();
|
|
}
|
|
}
|
|
|
|
protected function raisePermissionException() {
|
|
$host = php_uname('n');
|
|
throw new DiffusionSetupException(
|
|
pht(
|
|
'The clone of this repository ("%s") on the local machine ("%s") '.
|
|
'could not be read. Ensure that the repository is in a '.
|
|
'location where the web server has read permissions.',
|
|
$this->getRepository()->getDisplayName(),
|
|
$host));
|
|
}
|
|
|
|
protected function raiseCloneException() {
|
|
$host = php_uname('n');
|
|
throw new DiffusionSetupException(
|
|
pht(
|
|
'The working copy for this repository ("%s") has not been cloned yet '.
|
|
'on this machine ("%s"). Make sure you havestarted the Phabricator '.
|
|
'daemons. If this problem persists for longer than a clone should '.
|
|
'take, check the daemon logs (in the Daemon Console) to see if there '.
|
|
'were errors cloning the repository. Consult the "Diffusion User '.
|
|
'Guide" in the documentation for help setting up repositories.',
|
|
$this->getRepository()->getDisplayName(),
|
|
$host));
|
|
}
|
|
|
|
private function queryStableCommit() {
|
|
$types = array();
|
|
if ($this->symbolicCommit) {
|
|
$ref = $this->symbolicCommit;
|
|
} else {
|
|
if ($this->supportsBranches()) {
|
|
$ref = $this->getBranch();
|
|
$types = array(
|
|
PhabricatorRepositoryRefCursor::TYPE_BRANCH,
|
|
);
|
|
} else {
|
|
$ref = 'HEAD';
|
|
}
|
|
}
|
|
|
|
$results = $this->resolveRefs(array($ref), $types);
|
|
|
|
$matches = idx($results, $ref, array());
|
|
if (!$matches) {
|
|
$message = pht(
|
|
'Ref "%s" does not exist in this repository.',
|
|
$ref);
|
|
throw id(new DiffusionRefNotFoundException($message))
|
|
->setRef($ref);
|
|
}
|
|
|
|
if (count($matches) > 1) {
|
|
$match = $this->chooseBestRefMatch($ref, $matches);
|
|
} else {
|
|
$match = head($matches);
|
|
}
|
|
|
|
$this->stableCommit = $match['identifier'];
|
|
$this->symbolicType = $match['type'];
|
|
}
|
|
|
|
public function getRefAlternatives() {
|
|
// Make sure we've resolved the reference into a stable commit first.
|
|
try {
|
|
$this->getStableCommit();
|
|
} catch (DiffusionRefNotFoundException $ex) {
|
|
// If we have a bad reference, just return the empty set of
|
|
// alternatives.
|
|
}
|
|
return $this->refAlternatives;
|
|
}
|
|
|
|
private function chooseBestRefMatch($ref, array $results) {
|
|
// First, filter out less-desirable matches.
|
|
$candidates = array();
|
|
foreach ($results as $result) {
|
|
// Exclude closed heads.
|
|
if ($result['type'] == 'branch') {
|
|
if (idx($result, 'closed')) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$candidates[] = $result;
|
|
}
|
|
|
|
// If we filtered everything, undo the filtering.
|
|
if (!$candidates) {
|
|
$candidates = $results;
|
|
}
|
|
|
|
// TODO: Do a better job of selecting the best match?
|
|
$match = head($candidates);
|
|
|
|
// After choosing the best alternative, save all the alternatives so the
|
|
// UI can show them to the user.
|
|
if (count($candidates) > 1) {
|
|
$this->refAlternatives = $candidates;
|
|
}
|
|
|
|
return $match;
|
|
}
|
|
|
|
public function resolveRefs(array $refs, array $types = array()) {
|
|
// First, try to resolve refs from fast cache sources.
|
|
$cached_query = id(new DiffusionCachedResolveRefsQuery())
|
|
->setRepository($this->getRepository())
|
|
->withRefs($refs);
|
|
|
|
if ($types) {
|
|
$cached_query->withTypes($types);
|
|
}
|
|
|
|
$cached_results = $cached_query->execute();
|
|
|
|
// Throw away all the refs we resolved. Hopefully, we'll throw away
|
|
// everything here.
|
|
foreach ($refs as $key => $ref) {
|
|
if (isset($cached_results[$ref])) {
|
|
unset($refs[$key]);
|
|
}
|
|
}
|
|
|
|
// If we couldn't pull everything out of the cache, execute the underlying
|
|
// VCS operation.
|
|
if ($refs) {
|
|
$vcs_results = DiffusionQuery::callConduitWithDiffusionRequest(
|
|
$this->getUser(),
|
|
$this,
|
|
'diffusion.resolverefs',
|
|
array(
|
|
'types' => $types,
|
|
'refs' => $refs,
|
|
));
|
|
} else {
|
|
$vcs_results = array();
|
|
}
|
|
|
|
return $vcs_results + $cached_results;
|
|
}
|
|
|
|
public function setIsClusterRequest($is_cluster_request) {
|
|
$this->isClusterRequest = $is_cluster_request;
|
|
return $this;
|
|
}
|
|
|
|
public function getIsClusterRequest() {
|
|
return $this->isClusterRequest;
|
|
}
|
|
|
|
}
|