Path: blob/master/src/applications/diffusion/request/DiffusionRequest.php
13401 views
<?php12/**3* Contains logic to parse Diffusion requests, which have a complicated URI4* structure.5*6* @task new Creating Requests7* @task uri Managing Diffusion URIs8*/9abstract class DiffusionRequest extends Phobject {1011protected $path;12protected $line;13protected $branch;14protected $lint;1516protected $symbolicCommit;17protected $symbolicType;18protected $stableCommit;1920protected $repository;21protected $repositoryCommit;22protected $repositoryCommitData;2324private $isClusterRequest = false;25private $initFromConduit = true;26private $user;27private $branchObject = false;28private $refAlternatives;2930final public function supportsBranches() {31return $this->getRepository()->supportsRefs();32}3334abstract protected function isStableCommit($symbol);3536protected function didInitialize() {37return null;38}394041/* -( Creating Requests )-------------------------------------------------- */424344/**45* Create a new synthetic request from a parameter dictionary. If you need46* a @{class:DiffusionRequest} object in order to issue a DiffusionQuery, you47* can use this method to build one.48*49* Parameters are:50*51* - `repository` Repository object or identifier.52* - `user` Viewing user. Required if `repository` is an identifier.53* - `branch` Optional, branch name.54* - `path` Optional, file path.55* - `commit` Optional, commit identifier.56* - `line` Optional, line range.57*58* @param map See documentation.59* @return DiffusionRequest New request object.60* @task new61*/62final public static function newFromDictionary(array $data) {63$repository_key = 'repository';64$identifier_key = 'callsign';65$viewer_key = 'user';6667$repository = idx($data, $repository_key);68$identifier = idx($data, $identifier_key);6970$have_repository = ($repository !== null);71$have_identifier = ($identifier !== null);7273if ($have_repository && $have_identifier) {74throw new Exception(75pht(76'Specify "%s" or "%s", but not both.',77$repository_key,78$identifier_key));79}8081if (!$have_repository && !$have_identifier) {82throw new Exception(83pht(84'One of "%s" and "%s" is required.',85$repository_key,86$identifier_key));87}8889if ($have_repository) {90if (!($repository instanceof PhabricatorRepository)) {91if (empty($data[$viewer_key])) {92throw new Exception(93pht(94'Parameter "%s" is required if "%s" is provided.',95$viewer_key,96$identifier_key));97}9899$identifier = $repository;100$repository = null;101}102}103104if ($identifier !== null) {105$object = self::newFromIdentifier(106$identifier,107$data[$viewer_key],108idx($data, 'edit'));109} else {110$object = self::newFromRepository($repository);111}112113if (!$object) {114return null;115}116117$object->initializeFromDictionary($data);118119return $object;120}121122/**123* Internal.124*125* @task new126*/127private function __construct() {128// <private>129}130131132/**133* Internal. Use @{method:newFromDictionary}, not this method.134*135* @param string Repository identifier.136* @param PhabricatorUser Viewing user.137* @return DiffusionRequest New request object.138* @task new139*/140private static function newFromIdentifier(141$identifier,142PhabricatorUser $viewer,143$need_edit = false) {144145$query = id(new PhabricatorRepositoryQuery())146->setViewer($viewer)147->withIdentifiers(array($identifier))148->needProfileImage(true)149->needURIs(true);150151if ($need_edit) {152$query->requireCapabilities(153array(154PhabricatorPolicyCapability::CAN_VIEW,155PhabricatorPolicyCapability::CAN_EDIT,156));157}158159$repository = $query->executeOne();160161if (!$repository) {162return null;163}164165return self::newFromRepository($repository);166}167168169/**170* Internal. Use @{method:newFromDictionary}, not this method.171*172* @param PhabricatorRepository Repository object.173* @return DiffusionRequest New request object.174* @task new175*/176private static function newFromRepository(177PhabricatorRepository $repository) {178179$map = array(180PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'DiffusionGitRequest',181PhabricatorRepositoryType::REPOSITORY_TYPE_SVN => 'DiffusionSvnRequest',182PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL =>183'DiffusionMercurialRequest',184);185186$class = idx($map, $repository->getVersionControlSystem());187188if (!$class) {189throw new Exception(pht('Unknown version control system!'));190}191192$object = new $class();193194$object->repository = $repository;195196return $object;197}198199200/**201* Internal. Use @{method:newFromDictionary}, not this method.202*203* @param map Map of parsed data.204* @return void205* @task new206*/207private function initializeFromDictionary(array $data) {208$blob = idx($data, 'blob');209if (phutil_nonempty_string($blob)) {210$blob = self::parseRequestBlob($blob, $this->supportsBranches());211$data = $blob + $data;212}213214$this->path = idx($data, 'path');215$this->line = idx($data, 'line');216$this->initFromConduit = idx($data, 'initFromConduit', true);217$this->lint = idx($data, 'lint');218219$this->symbolicCommit = idx($data, 'commit');220if ($this->supportsBranches()) {221$this->branch = idx($data, 'branch');222}223224if (!$this->getUser()) {225$user = idx($data, 'user');226if (!$user) {227throw new Exception(228pht(229'You must provide a %s in the dictionary!',230'PhabricatorUser'));231}232$this->setUser($user);233}234235$this->didInitialize();236}237238final public function setUser(PhabricatorUser $user) {239$this->user = $user;240return $this;241}242final public function getUser() {243return $this->user;244}245246public function getRepository() {247return $this->repository;248}249250public function setPath($path) {251$this->path = $path;252return $this;253}254255public function getPath() {256return $this->path;257}258259public function getLine() {260return $this->line;261}262263public function getCommit() {264265// TODO: Probably remove all of this.266267if ($this->getSymbolicCommit() !== null) {268return $this->getSymbolicCommit();269}270271return $this->getStableCommit();272}273274/**275* Get the symbolic commit associated with this request.276*277* A symbolic commit may be a commit hash, an abbreviated commit hash, a278* branch name, a tag name, or an expression like "HEAD^^^". The symbolic279* commit may also be absent.280*281* This method always returns the symbol present in the original request,282* in unmodified form.283*284* See also @{method:getStableCommit}.285*286* @return string|null Symbolic commit, if one was present in the request.287*/288public function getSymbolicCommit() {289return $this->symbolicCommit;290}291292293/**294* Modify the request to move the symbolic commit elsewhere.295*296* @param string New symbolic commit.297* @return this298*/299public function updateSymbolicCommit($symbol) {300$this->symbolicCommit = $symbol;301$this->symbolicType = null;302$this->stableCommit = null;303return $this;304}305306307/**308* Get the ref type (`commit` or `tag`) of the location associated with this309* request.310*311* If a symbolic commit is present in the request, this method identifies312* the type of the symbol. Otherwise, it identifies the type of symbol of313* the location the request is implicitly associated with. This will probably314* always be `commit`.315*316* @return string Symbolic commit type (`commit` or `tag`).317*/318public function getSymbolicType() {319if ($this->symbolicType === null) {320// As a side effect, this resolves the symbolic type.321$this->getStableCommit();322}323return $this->symbolicType;324}325326327/**328* Retrieve the stable, permanent commit name identifying the repository329* location associated with this request.330*331* This returns a non-symbolic identifier for the current commit: in Git and332* Mercurial, a 40-character SHA1; in SVN, a revision number.333*334* See also @{method:getSymbolicCommit}.335*336* @return string Stable commit name, like a git hash or SVN revision. Not337* a symbolic commit reference.338*/339public function getStableCommit() {340if (!$this->stableCommit) {341if ($this->isStableCommit($this->symbolicCommit)) {342$this->stableCommit = $this->symbolicCommit;343$this->symbolicType = 'commit';344} else {345$this->queryStableCommit();346}347}348return $this->stableCommit;349}350351352public function getBranch() {353return $this->branch;354}355356public function getLint() {357return $this->lint;358}359360protected function getArcanistBranch() {361return $this->getBranch();362}363364public function loadBranch() {365// TODO: Get rid of this and do real Queries on real objects.366367if ($this->branchObject === false) {368$this->branchObject = PhabricatorRepositoryBranch::loadBranch(369$this->getRepository()->getID(),370$this->getArcanistBranch());371}372373return $this->branchObject;374}375376public function loadCoverage() {377// TODO: This should also die.378$branch = $this->loadBranch();379if (!$branch) {380return;381}382383$path = $this->getPath();384$path_map = id(new DiffusionPathIDQuery(array($path)))->loadPathIDs();385386$coverage_row = queryfx_one(387id(new PhabricatorRepository())->establishConnection('r'),388'SELECT * FROM %T WHERE branchID = %d AND pathID = %d389ORDER BY commitID DESC LIMIT 1',390'repository_coverage',391$branch->getID(),392$path_map[$path]);393394if (!$coverage_row) {395return null;396}397398return idx($coverage_row, 'coverage');399}400401402public function loadCommit() {403if (empty($this->repositoryCommit)) {404$repository = $this->getRepository();405406$commit = id(new DiffusionCommitQuery())407->setViewer($this->getUser())408->withRepository($repository)409->withIdentifiers(array($this->getStableCommit()))410->executeOne();411if ($commit) {412$commit->attachRepository($repository);413}414$this->repositoryCommit = $commit;415}416return $this->repositoryCommit;417}418419public function loadCommitData() {420if (empty($this->repositoryCommitData)) {421$commit = $this->loadCommit();422$data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(423'commitID = %d',424$commit->getID());425if (!$data) {426$data = new PhabricatorRepositoryCommitData();427$data->setCommitMessage(428pht('(This commit has not been fully parsed yet.)'));429}430$this->repositoryCommitData = $data;431}432return $this->repositoryCommitData;433}434435/* -( Managing Diffusion URIs )-------------------------------------------- */436437438public function generateURI(array $params) {439if (empty($params['stable'])) {440$default_commit = $this->getSymbolicCommit();441} else {442$default_commit = $this->getStableCommit();443}444445$defaults = array(446'path' => $this->getPath(),447'branch' => $this->getBranch(),448'commit' => $default_commit,449'lint' => idx($params, 'lint', $this->getLint()),450);451452foreach ($defaults as $key => $val) {453if (!isset($params[$key])) { // Overwrite NULL.454$params[$key] = $val;455}456}457458return $this->getRepository()->generateURI($params);459}460461/**462* Internal. Public only for unit tests.463*464* Parse the request URI into components.465*466* @param string URI blob.467* @param bool True if this VCS supports branches.468* @return map Parsed URI.469*470* @task uri471*/472public static function parseRequestBlob($blob, $supports_branches) {473$result = array(474'branch' => null,475'path' => null,476'commit' => null,477'line' => null,478);479480$matches = null;481482if ($supports_branches) {483// Consume the front part of the URI, up to the first "/". This is the484// path-component encoded branch name.485if (preg_match('@^([^/]+)/@', $blob, $matches)) {486$result['branch'] = phutil_unescape_uri_path_component($matches[1]);487$blob = substr($blob, strlen($matches[1]) + 1);488}489}490491// Consume the back part of the URI, up to the first "$". Use a negative492// lookbehind to prevent matching '$$'. We double the '$' symbol when493// encoding so that files with names like "money/$100" will survive.494$pattern = '@(?:(?:^|[^$])(?:[$][$])*)[$]([\d,-]+)$@';495if (preg_match($pattern, $blob, $matches)) {496$result['line'] = $matches[1];497$blob = substr($blob, 0, -(strlen($matches[1]) + 1));498}499500// We've consumed the line number if it exists, so unescape "$" in the501// rest of the string.502$blob = str_replace('$$', '$', $blob);503504// Consume the commit name, stopping on ';;'. We allow any character to505// appear in commits names, as they can sometimes be symbolic names (like506// tag names or refs).507if (preg_match('@(?:(?:^|[^;])(?:;;)*);([^;].*)$@', $blob, $matches)) {508$result['commit'] = $matches[1];509$blob = substr($blob, 0, -(strlen($matches[1]) + 1));510}511512// We've consumed the commit if it exists, so unescape ";" in the rest513// of the string.514$blob = str_replace(';;', ';', $blob);515516if (strlen($blob)) {517$result['path'] = $blob;518}519520if ($result['path'] !== null) {521$parts = explode('/', $result['path']);522foreach ($parts as $part) {523// Prevent any hyjinx since we're ultimately shipping this to the524// filesystem under a lot of workflows.525if ($part == '..') {526throw new Exception(pht('Invalid path URI.'));527}528}529}530531return $result;532}533534/**535* Check that the working copy of the repository is present and readable.536*537* @param string Path to the working copy.538*/539protected function validateWorkingCopy($path) {540if (!is_readable(dirname($path))) {541$this->raisePermissionException();542}543544if (!Filesystem::pathExists($path)) {545$this->raiseCloneException();546}547}548549protected function raisePermissionException() {550$host = php_uname('n');551throw new DiffusionSetupException(552pht(553'The clone of this repository ("%s") on the local machine ("%s") '.554'could not be read. Ensure that the repository is in a '.555'location where the web server has read permissions.',556$this->getRepository()->getDisplayName(),557$host));558}559560protected function raiseCloneException() {561$host = php_uname('n');562throw new DiffusionSetupException(563pht(564'The working copy for this repository ("%s") has not been cloned yet '.565'on this machine ("%s"). Make sure you have started the '.566'daemons. If this problem persists for longer than a clone should '.567'take, check the daemon logs (in the Daemon Console) to see if there '.568'were errors cloning the repository. Consult the "Diffusion User '.569'Guide" in the documentation for help setting up repositories.',570$this->getRepository()->getDisplayName(),571$host));572}573574private function queryStableCommit() {575$types = array();576if ($this->symbolicCommit) {577$ref = $this->symbolicCommit;578} else {579if ($this->supportsBranches()) {580$ref = $this->getBranch();581$types = array(582PhabricatorRepositoryRefCursor::TYPE_BRANCH,583);584} else {585$ref = 'HEAD';586}587}588589$results = $this->resolveRefs(array($ref), $types);590591$matches = idx($results, $ref, array());592if (!$matches) {593$message = pht(594'Ref "%s" does not exist in this repository.',595$ref);596throw id(new DiffusionRefNotFoundException($message))597->setRef($ref);598}599600if (count($matches) > 1) {601$match = $this->chooseBestRefMatch($ref, $matches);602} else {603$match = head($matches);604}605606$this->stableCommit = $match['identifier'];607$this->symbolicType = $match['type'];608}609610public function getRefAlternatives() {611// Make sure we've resolved the reference into a stable commit first.612try {613$this->getStableCommit();614} catch (DiffusionRefNotFoundException $ex) {615// If we have a bad reference, just return the empty set of616// alternatives.617}618return $this->refAlternatives;619}620621private function chooseBestRefMatch($ref, array $results) {622// First, filter out less-desirable matches.623$candidates = array();624foreach ($results as $result) {625// Exclude closed heads.626if ($result['type'] == 'branch') {627if (idx($result, 'closed')) {628continue;629}630}631632$candidates[] = $result;633}634635// If we filtered everything, undo the filtering.636if (!$candidates) {637$candidates = $results;638}639640// TODO: Do a better job of selecting the best match?641$match = head($candidates);642643// After choosing the best alternative, save all the alternatives so the644// UI can show them to the user.645if (count($candidates) > 1) {646$this->refAlternatives = $candidates;647}648649return $match;650}651652public function resolveRefs(array $refs, array $types = array()) {653// First, try to resolve refs from fast cache sources.654$cached_query = id(new DiffusionCachedResolveRefsQuery())655->setRepository($this->getRepository())656->withRefs($refs);657658if ($types) {659$cached_query->withTypes($types);660}661662$cached_results = $cached_query->execute();663664// Throw away all the refs we resolved. Hopefully, we'll throw away665// everything here.666foreach ($refs as $key => $ref) {667if (isset($cached_results[$ref])) {668unset($refs[$key]);669}670}671672// If we couldn't pull everything out of the cache, execute the underlying673// VCS operation.674if ($refs) {675$vcs_results = DiffusionQuery::callConduitWithDiffusionRequest(676$this->getUser(),677$this,678'diffusion.resolverefs',679array(680'types' => $types,681'refs' => $refs,682));683} else {684$vcs_results = array();685}686687return $vcs_results + $cached_results;688}689690public function setIsClusterRequest($is_cluster_request) {691$this->isClusterRequest = $is_cluster_request;692return $this;693}694695public function getIsClusterRequest() {696return $this->isClusterRequest;697}698699}700701702