diff --git a/src/applications/diffusion/controller/DiffusionRepositoryController.php b/src/applications/diffusion/controller/DiffusionRepositoryController.php index ec15c61355..8bc0e98872 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryController.php @@ -1,644 +1,710 @@ getRequest(); + $viewer = $request->getUser(); + $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $content = array(); $crumbs = $this->buildCrumbs(); $content[] = $crumbs; $content[] = $this->buildPropertiesTable($drequest->getRepository()); + + // Before we do any work, make sure we're looking at a some content: we're + // on a valid branch, and the repository is not empty. + $page_has_content = false; + $empty_title = null; + $empty_message = null; + + // If this VCS supports branches, check that the selected branch actually + // exists. + if ($drequest->supportsBranches()) { + $ref_cursor = id(new PhabricatorRepositoryRefCursorQuery()) + ->setViewer($viewer) + ->withRepositoryPHIDs(array($repository->getPHID())) + ->withRefTypes(array(PhabricatorRepositoryRefCursor::TYPE_BRANCH)) + ->withRefNames(array($drequest->getBranch())) + ->executeOne(); + if ($ref_cursor) { + // This is a valid branch, so we necessarily have some content. + $page_has_content = true; + } else { + $empty_title = pht('No Such Branch'); + $empty_message = pht( + 'There is no branch named "%s" in this repository.', + $drequest->getBranch()); + } + } + + // If we didn't find any branches, check if there are any commits at all. + // This can tailor the message for empty repositories. + if (!$page_has_content) { + $any_commit = id(new DiffusionCommitQuery()) + ->setViewer($viewer) + ->withRepository($repository) + ->setLimit(1) + ->execute(); + if ($any_commit) { + if (!$drequest->supportsBranches()) { + $page_has_content = true; + } + } else { + $empty_title = pht('Empty Repository'); + $empty_message = pht( + 'This repository does not have any commits yet.'); + } + } + + if ($page_has_content) { + $content[] = $this->buildNormalContent($drequest); + } else { + $content[] = id(new AphrontErrorView()) + ->setTitle($empty_title) + ->setSeverity(AphrontErrorView::SEVERITY_WARNING) + ->setErrors(array($empty_message)); + } + + return $this->buildApplicationPage( + $content, + array( + 'title' => $drequest->getRepository()->getName(), + )); + } + + + private function buildNormalContent(DiffusionRequest $drequest) { + $repository = $drequest->getRepository(); + $phids = array(); + $content = array(); try { $history_results = $this->callConduitWithDiffusionRequest( 'diffusion.historyquery', array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), 'offset' => 0, 'limit' => 15)); $history = DiffusionPathChange::newFromConduit( $history_results['pathChanges']); foreach ($history as $item) { $data = $item->getCommitData(); if ($data) { if ($data->getCommitDetail('authorPHID')) { $phids[$data->getCommitDetail('authorPHID')] = true; } if ($data->getCommitDetail('committerPHID')) { $phids[$data->getCommitDetail('committerPHID')] = true; } } } $history_exception = null; } catch (Exception $ex) { $history_results = null; $history = null; $history_exception = $ex; } try { $browse_results = DiffusionBrowseResultSet::newFromConduit( $this->callConduitWithDiffusionRequest( 'diffusion.browsequery', array( 'path' => $drequest->getPath(), 'commit' => $drequest->getCommit(), ))); $browse_paths = $browse_results->getPaths(); foreach ($browse_paths as $item) { $data = $item->getLastCommitData(); if ($data) { if ($data->getCommitDetail('authorPHID')) { $phids[$data->getCommitDetail('authorPHID')] = true; } if ($data->getCommitDetail('committerPHID')) { $phids[$data->getCommitDetail('committerPHID')] = true; } } } $browse_exception = null; } catch (Exception $ex) { $browse_results = null; $browse_paths = null; $browse_exception = $ex; } $phids = array_keys($phids); $handles = $this->loadViewerHandles($phids); if ($browse_results) { $readme = $this->callConduitWithDiffusionRequest( 'diffusion.readmequery', array( 'paths' => $browse_results->getPathDicts(), 'commit' => $drequest->getStableCommit(), )); } else { $readme = null; } $content[] = $this->buildBrowseTable( $browse_results, $browse_paths, $browse_exception, $handles); $content[] = $this->buildHistoryTable( $history_results, $history, $history_exception, $handles); try { $content[] = $this->buildTagListTable($drequest); } catch (Exception $ex) { if (!$repository->isImporting()) { $content[] = $this->renderStatusMessage( pht('Unable to Load Tags'), $ex->getMessage()); } } try { $content[] = $this->buildBranchListTable($drequest); } catch (Exception $ex) { if (!$repository->isImporting()) { $content[] = $this->renderStatusMessage( pht('Unable to Load Branches'), $ex->getMessage()); } } if ($readme) { $box = new PHUIBoxView(); $box->appendChild($readme); $box->addPadding(PHUI::PADDING_LARGE); $panel = new PHUIObjectBoxView(); $panel->setHeaderText(pht('README')); $panel->appendChild($box); $content[] = $panel; } - return $this->buildApplicationPage( - $content, - array( - 'title' => $drequest->getRepository()->getName(), - )); + return $content; } private function buildPropertiesTable(PhabricatorRepository $repository) { $user = $this->getRequest()->getUser(); $header = id(new PHUIHeaderView()) ->setHeader($repository->getName()) ->setUser($user) ->setPolicyObject($repository); if (!$repository->isTracked()) { $header->setStatus('fa-ban', 'dark', pht('Inactive')); } else if ($repository->isImporting()) { $header->setStatus('fa-clock-o', 'indigo', pht('Importing...')); } else { $header->setStatus('fa-check', 'bluegrey', pht('Active')); } $actions = $this->buildActionList($repository); $view = id(new PHUIPropertyListView()) ->setUser($user); $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $repository->getPHID(), PhabricatorEdgeConfig::TYPE_OBJECT_HAS_PROJECT); if ($project_phids) { $this->loadHandles($project_phids); $view->addProperty( pht('Projects'), $this->renderHandlesForPHIDs($project_phids)); } if ($repository->isHosted()) { $ssh_uri = $repository->getSSHCloneURIObject(); if ($ssh_uri) { $clone_uri = $this->renderCloneCommand( $repository, $ssh_uri, $repository->getServeOverSSH(), '/settings/panel/ssh/'); $view->addProperty( $repository->isSVN() ? pht('Checkout (SSH)') : pht('Clone (SSH)'), $clone_uri); } $http_uri = $repository->getHTTPCloneURIObject(); if ($http_uri) { $clone_uri = $this->renderCloneCommand( $repository, $http_uri, $repository->getServeOverHTTP(), PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth') ? '/settings/panel/vcspassword/' : null); $view->addProperty( $repository->isSVN() ? pht('Checkout (HTTP)') : pht('Clone (HTTP)'), $clone_uri); } } else { switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $view->addProperty( pht('Clone'), $this->renderCloneCommand( $repository, $repository->getPublicCloneURI())); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $view->addProperty( pht('Checkout'), $this->renderCloneCommand( $repository, $repository->getPublicCloneURI())); break; } } $description = $repository->getDetail('description'); if (strlen($description)) { $description = PhabricatorMarkupEngine::renderOneObject( $repository, 'description', $user); $view->addSectionHeader(pht('Description')); $view->addTextContent($description); } $view->setActionList($actions); return id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($view); } private function buildBranchListTable(DiffusionRequest $drequest) { $viewer = $this->getRequest()->getUser(); if ($drequest->getBranch() === null) { return null; } $limit = 15; $branches = $this->callConduitWithDiffusionRequest( 'diffusion.branchquery', array( 'limit' => $limit + 1, )); if (!$branches) { return null; } $more_branches = (count($branches) > $limit); $branches = array_slice($branches, 0, $limit); $branches = DiffusionRepositoryRef::loadAllFromDictionaries($branches); $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withIdentifiers(mpull($branches, 'getCommitIdentifier')) ->withRepository($drequest->getRepository()) ->execute(); $table = id(new DiffusionBranchTableView()) ->setUser($viewer) ->setDiffusionRequest($drequest) ->setBranches($branches) ->setCommits($commits); $panel = new PHUIObjectBoxView(); $header = new PHUIHeaderView(); $header->setHeader(pht('Branches')); if ($more_branches) { $header->setSubHeader(pht('Showing %d branches.', $limit)); } $icon = id(new PHUIIconView()) ->setIconFont('fa-code-fork'); $button = new PHUIButtonView(); $button->setText(pht('Show All Branches')); $button->setTag('a'); $button->setIcon($icon); $button->setHref($drequest->generateURI( array( 'action' => 'branches', ))); $header->addActionLink($button); $panel->setHeader($header); $panel->appendChild($table); return $panel; } private function buildTagListTable(DiffusionRequest $drequest) { $viewer = $this->getRequest()->getUser(); $tag_limit = 15; $tags = array(); try { $tags = DiffusionRepositoryTag::newFromConduit( $this->callConduitWithDiffusionRequest( 'diffusion.tagsquery', array( // On the home page, we want to find tags on any branch. 'commit' => null, 'limit' => $tag_limit + 1, ))); } catch (ConduitException $e) { if ($e->getMessage() != 'ERR-UNSUPPORTED-VCS') { throw $e; } } if (!$tags) { return null; } $more_tags = (count($tags) > $tag_limit); $tags = array_slice($tags, 0, $tag_limit); $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withIdentifiers(mpull($tags, 'getCommitIdentifier')) ->withRepository($drequest->getRepository()) ->needCommitData(true) ->execute(); $view = id(new DiffusionTagListView()) ->setUser($viewer) ->setDiffusionRequest($drequest) ->setTags($tags) ->setCommits($commits); $phids = $view->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); $panel = new PHUIObjectBoxView(); $header = new PHUIHeaderView(); $header->setHeader(pht('Tags')); if ($more_tags) { $header->setSubHeader( pht('Showing the %d most recent tags.', $tag_limit)); } $icon = id(new PHUIIconView()) ->setIconFont('fa-tag'); $button = new PHUIButtonView(); $button->setText(pht('Show All Tags')); $button->setTag('a'); $button->setIcon($icon); $button->setHref($drequest->generateURI( array( 'action' => 'tags', ))); $header->addActionLink($button); $panel->setHeader($header); $panel->appendChild($view); return $panel; } private function buildActionList(PhabricatorRepository $repository) { $viewer = $this->getRequest()->getUser(); $view_uri = $this->getApplicationURI($repository->getCallsign().'/'); $edit_uri = $this->getApplicationURI($repository->getCallsign().'/edit/'); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($repository) ->setObjectURI($view_uri); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $repository, PhabricatorPolicyCapability::CAN_EDIT); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Repository')) ->setIcon('fa-pencil') ->setHref($edit_uri) ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit)); if ($repository->isHosted()) { $callsign = $repository->getCallsign(); $push_uri = $this->getApplicationURI( 'pushlog/?repositories=r'.$callsign); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('View Push Logs')) ->setIcon('fa-list-alt') ->setHref($push_uri)); } return $view; } private function buildHistoryTable( $history_results, $history, $history_exception, array $handles) { $request = $this->getRequest(); $viewer = $request->getUser(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); if ($history_exception) { if ($repository->isImporting()) { return $this->renderStatusMessage( pht('Still Importing...'), pht( 'This repository is still importing. History is not yet '. 'available.')); } else { return $this->renderStatusMessage( pht('Unable to Retrieve History'), $history_exception->getMessage()); } } $history_table = id(new DiffusionHistoryTableView()) ->setUser($viewer) ->setDiffusionRequest($drequest) ->setHandles($handles) ->setHistory($history); // TODO: Super sketchy. $history_table->loadRevisions(); if ($history_results) { $history_table->setParents($history_results['parents']); } $history_table->setIsHead(true); $callsign = $drequest->getRepository()->getCallsign(); $icon = id(new PHUIIconView()) ->setIconFont('fa-list-alt'); $button = id(new PHUIButtonView()) ->setText(pht('View Full History')) ->setHref($drequest->generateURI( array( 'action' => 'history', ))) ->setTag('a') ->setIcon($icon); $panel = new PHUIObjectBoxView(); $header = id(new PHUIHeaderView()) ->setHeader(pht('Recent Commits')) ->addActionLink($button); $panel->setHeader($header); $panel->appendChild($history_table); return $panel; } private function buildBrowseTable( $browse_results, $browse_paths, $browse_exception, array $handles) { require_celerity_resource('diffusion-icons-css'); $request = $this->getRequest(); $viewer = $request->getUser(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); if ($browse_exception) { if ($repository->isImporting()) { // The history table renders a useful message. return null; } else { return $this->renderStatusMessage( pht('Unable to Retrieve Paths'), $browse_exception->getMessage()); } } $browse_table = id(new DiffusionBrowseTableView()) ->setUser($viewer) ->setDiffusionRequest($drequest) ->setHandles($handles); if ($browse_paths) { $browse_table->setPaths($browse_paths); } else { $browse_table->setPaths(array()); } $browse_uri = $drequest->generateURI(array('action' => 'browse')); $browse_panel = new PHUIObjectBoxView(); $header = id(new PHUIHeaderView()) ->setHeader(pht('Repository')); $icon = id(new PHUIIconView()) ->setIconFont('fa-folder-open'); $button = new PHUIButtonView(); $button->setText(pht('Browse Repository')); $button->setTag('a'); $button->setIcon($icon); $button->setHref($browse_uri); $header->addActionLink($button); $browse_panel->setHeader($header); if ($repository->canUsePathTree()) { Javelin::initBehavior( 'diffusion-locate-file', array( 'controlID' => 'locate-control', 'inputID' => 'locate-input', 'browseBaseURI' => (string)$drequest->generateURI( array( 'action' => 'browse', )), 'uri' => (string)$drequest->generateURI( array( 'action' => 'pathtree', )), )); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTypeaheadControl()) ->setHardpointID('locate-control') ->setID('locate-input') ->setLabel(pht('Locate File'))); $form_box = id(new PHUIBoxView()) ->addClass('diffusion-locate-file-view') ->appendChild($form->buildLayoutView()); $browse_panel->appendChild($form_box); } $browse_panel->appendChild($browse_table); return $browse_panel; } private function renderCloneCommand( PhabricatorRepository $repository, $uri, $serve_mode = null, $manage_uri = null) { require_celerity_resource('diffusion-icons-css'); Javelin::initBehavior('select-on-click'); switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $command = csprintf( 'git clone %R', $uri); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $command = csprintf( 'hg clone %R', $uri); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: if ($repository->isHosted()) { $command = csprintf( 'svn checkout %R %R', $uri, $repository->getCloneName()); } else { $command = csprintf( 'svn checkout %R', $uri); } break; } $input = javelin_tag( 'input', array( 'type' => 'text', 'value' => (string)$command, 'class' => 'diffusion-clone-uri', 'sigil' => 'select-on-click', 'readonly' => 'true', )); $extras = array(); if ($serve_mode) { if ($serve_mode === PhabricatorRepository::SERVE_READONLY) { $extras[] = pht('(Read Only)'); } } if ($manage_uri) { if ($this->getRequest()->getUser()->isLoggedIn()) { $extras[] = phutil_tag( 'a', array( 'href' => $manage_uri, ), pht('Manage Credentials')); } } if ($extras) { $extras = phutil_implode_html(' ', $extras); $extras = phutil_tag( 'div', array( 'class' => 'diffusion-clone-extras', ), $extras); } return array($input, $extras); } } diff --git a/src/applications/diffusion/request/DiffusionGitRequest.php b/src/applications/diffusion/request/DiffusionGitRequest.php index d8c9e932e1..b020bb4c4b 100644 --- a/src/applications/diffusion/request/DiffusionGitRequest.php +++ b/src/applications/diffusion/request/DiffusionGitRequest.php @@ -1,32 +1,32 @@ branch) { return $this->branch; } if ($this->repository) { return $this->repository->getDefaultBranch(); } throw new Exception('Unable to determine branch!'); } protected function getResolvableBranchName($branch) { if ($this->repository->isWorkingCopyBare()) { return $branch; } else { $remote = DiffusionGitBranch::DEFAULT_GIT_REMOTE; return $remote.'/'.$branch; } } } diff --git a/src/applications/diffusion/request/DiffusionMercurialRequest.php b/src/applications/diffusion/request/DiffusionMercurialRequest.php index d518f3b70d..5966ffaaa0 100644 --- a/src/applications/diffusion/request/DiffusionMercurialRequest.php +++ b/src/applications/diffusion/request/DiffusionMercurialRequest.php @@ -1,25 +1,25 @@ branch) { return $this->branch; } if ($this->repository) { return $this->repository->getDefaultBranch(); } throw new Exception('Unable to determine branch!'); } } diff --git a/src/applications/diffusion/request/DiffusionRequest.php b/src/applications/diffusion/request/DiffusionRequest.php index a98a7080b6..e50732599b 100644 --- a/src/applications/diffusion/request/DiffusionRequest.php +++ b/src/applications/diffusion/request/DiffusionRequest.php @@ -1,759 +1,759 @@ initializeFromDictionary($data); return $object; } /** * Create a new request from an Aphront request dictionary. This is an * internal method that you generally should not call directly; instead, * call @{method:newFromDictionary}. * * @param map Map of Aphront request data. * @return DiffusionRequest New request object. * @task new */ final public static function newFromAphrontRequestDictionary( array $data, AphrontRequest $request) { $callsign = phutil_unescape_uri_path_component(idx($data, 'callsign')); $object = self::newFromCallsign($callsign, $request->getUser()); - $use_branches = $object->getSupportsBranches(); + $use_branches = $object->supportsBranches(); $parsed = self::parseRequestBlob(idx($data, 'dblob'), $use_branches); $object->setUser($request->getUser()); $object->initializeFromDictionary($parsed); $object->lint = $request->getStr('lint'); return $object; } /** * Internal. * * @task new */ final private function __construct() { // } /** * Internal. Use @{method:newFromDictionary}, not this method. * * @param string Repository callsign. * @param PhabricatorUser Viewing user. * @return DiffusionRequest New request object. * @task new */ final private static function newFromCallsign( $callsign, PhabricatorUser $viewer) { $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withCallsigns(array($callsign)) ->executeOne(); if (!$repository) { throw new Exception("No such repository '{$callsign}'."); } 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('Unknown version control system!'); } $object = new $class(); $object->repository = $repository; $object->callsign = $repository->getCallsign(); 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) { $this->path = idx($data, 'path'); $this->line = idx($data, 'line'); $this->initFromConduit = idx($data, 'initFromConduit', true); $this->symbolicCommit = idx($data, 'commit'); - if ($this->getSupportsBranches()) { + if ($this->supportsBranches()) { $this->branch = idx($data, 'branch'); } if (!$this->getUser()) { $user = idx($data, 'user'); if (!$user) { throw new Exception( 'You must provide a PhabricatorUser in the dictionary!'); } $this->setUser($user); } $this->didInitialize(); } final protected function shouldInitFromConduit() { return $this->initFromConduit; } 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 getCallsign() { return $this->callsign; } 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(); // TODO: (T603) This should be a real query, but we need to sort out // the viewer. $commit = id(new PhabricatorRepositoryCommit())->loadOneWhere( 'repositoryID = %d AND commitIdentifier = %s', $repository->getID(), $this->getStableCommit()); if ($commit) { $commit->attachRepository($repository); } $this->repositoryCommit = $commit; } return $this->repositoryCommit; } public function loadArcanistProjects() { if (empty($this->arcanistProjects)) { $projects = id(new PhabricatorRepositoryArcanistProject())->loadAllWhere( 'repositoryID = %d', $this->getRepository()->getID()); $this->arcanistProjects = $projects; } return $this->arcanistProjects; } 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( '(This commit has not been fully parsed yet.)'); } $this->repositoryCommitData = $data; } return $this->repositoryCommitData; } /* -( Managing Diffusion URIs )-------------------------------------------- */ /** * Generate a Diffusion URI using this request to provide defaults. See * @{method:generateDiffusionURI} for details. This method is the same, but * preserves the request parameters if they are not overridden. * * @param map See @{method:generateDiffusionURI}. * @return PhutilURI Generated URI. * @task uri */ public function generateURI(array $params) { if (empty($params['stable'])) { $default_commit = $this->getSymbolicCommit(); } else { $default_commit = $this->getStableCommit(); } $defaults = array( 'callsign' => $this->getCallsign(), '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 self::generateDiffusionURI($params); } /** * Generate a Diffusion URI from a parameter map. Applies the correct encoding * and formatting to the URI. Parameters are: * * - `action` One of `history`, `browse`, `change`, `lastmodified`, * `branch`, `tags`, `branches`, or `revision-ref`. The action specified * by the URI. * - `callsign` Repository callsign. * - `branch` Optional if action is not `branch`, branch name. * - `path` Optional, path to file. * - `commit` Optional, commit identifier. * - `line` Optional, line range. * - `lint` Optional, lint code. * - `params` Optional, query parameters. * * The function generates the specified URI and returns it. * * @param map See documentation. * @return PhutilURI Generated URI. * @task uri */ public static function generateDiffusionURI(array $params) { $action = idx($params, 'action'); $callsign = idx($params, 'callsign'); $path = idx($params, 'path'); $branch = idx($params, 'branch'); $commit = idx($params, 'commit'); $line = idx($params, 'line'); if (strlen($callsign)) { $callsign = phutil_escape_uri_path_component($callsign).'/'; } if (strlen($branch)) { $branch = phutil_escape_uri_path_component($branch).'/'; } if (strlen($path)) { $path = ltrim($path, '/'); $path = str_replace(array(';', '$'), array(';;', '$$'), $path); $path = phutil_escape_uri($path); } $path = "{$branch}{$path}"; if (strlen($commit)) { $commit = str_replace('$', '$$', $commit); $commit = ';'.phutil_escape_uri($commit); } if (strlen($line)) { $line = '$'.phutil_escape_uri($line); } $req_callsign = false; $req_branch = false; $req_commit = false; switch ($action) { case 'history': case 'browse': case 'change': case 'lastmodified': case 'tags': case 'branches': case 'lint': $req_callsign = true; break; case 'branch': $req_callsign = true; $req_branch = true; break; case 'commit': $req_callsign = true; $req_commit = true; break; } if ($req_callsign && !strlen($callsign)) { throw new Exception( "Diffusion URI action '{$action}' requires callsign!"); } if ($req_commit && !strlen($commit)) { throw new Exception( "Diffusion URI action '{$action}' requires commit!"); } switch ($action) { case 'change': case 'history': case 'browse': case 'lastmodified': case 'tags': case 'branches': case 'lint': case 'pathtree': $uri = "/diffusion/{$callsign}{$action}/{$path}{$commit}{$line}"; break; case 'branch': if (strlen($path)) { $uri = "/diffusion/{$callsign}repository/{$path}"; } else { $uri = "/diffusion/{$callsign}"; } break; case 'external': $commit = ltrim($commit, ';'); $uri = "/diffusion/external/{$commit}/"; break; case 'rendering-ref': // This isn't a real URI per se, it's passed as a query parameter to // the ajax changeset stuff but then we parse it back out as though // it came from a URI. $uri = rawurldecode("{$path}{$commit}"); break; case 'commit': $commit = ltrim($commit, ';'); $callsign = rtrim($callsign, '/'); $uri = "/r{$callsign}{$commit}"; break; default: throw new Exception("Unknown Diffusion URI action '{$action}'!"); } if ($action == 'rendering-ref') { return $uri; } $uri = new PhutilURI($uri); if (isset($params['lint'])) { $params['params'] = idx($params, 'params', array()) + array( 'lint' => $params['lint'], ); } if (idx($params, 'params')) { $uri->setQueryParams($params['params']); } return $uri; } /** * 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('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'); $callsign = $this->getRepository()->getCallsign(); throw new DiffusionSetupException( "The clone of this repository ('{$callsign}') on the local machine ". "('{$host}') could not be read. Ensure that the repository is in a ". "location where the web server has read permissions."); } protected function raiseCloneException() { $host = php_uname('n'); $callsign = $this->getRepository()->getCallsign(); throw new DiffusionSetupException( "The working copy for this repository ('{$callsign}') hasn't been ". "cloned yet on this machine ('{$host}'). Make sure you've started 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."); } private function queryStableCommit() { if ($this->symbolicCommit) { $ref = $this->symbolicCommit; } else { - if ($this->getSupportsBranches()) { + if ($this->supportsBranches()) { $ref = $this->getResolvableBranchName($this->getBranch()); } else { $ref = 'HEAD'; } } $results = $this->resolveRefs(array($ref)); $matches = idx($results, $ref, array()); if (count($matches) !== 1) { $message = pht('Ref "%s" is ambiguous or does not exist.', $ref); throw id(new DiffusionRefNotFoundException($message)) ->setRef($ref); } $match = head($matches); $this->stableCommit = $match['identifier']; $this->symbolicType = $match['type']; } protected function getResolvableBranchName($branch) { return $branch; } private function resolveRefs(array $refs) { if ($this->shouldInitFromConduit()) { return DiffusionQuery::callConduitWithDiffusionRequest( $this->getUser(), $this, 'diffusion.resolverefs', array( 'refs' => $refs, )); } else { return id(new DiffusionLowLevelResolveRefsQuery()) ->setRepository($this->getRepository()) ->withRefs($refs) ->execute(); } } } diff --git a/src/applications/diffusion/request/DiffusionSvnRequest.php b/src/applications/diffusion/request/DiffusionSvnRequest.php index 10d75d60bd..9ebefd01ab 100644 --- a/src/applications/diffusion/request/DiffusionSvnRequest.php +++ b/src/applications/diffusion/request/DiffusionSvnRequest.php @@ -1,26 +1,26 @@ path === null) { $subpath = $this->repository->getDetail('svn-subpath'); if ($subpath) { $this->path = $subpath; } } } protected function getArcanistBranch() { return 'svn'; } } diff --git a/src/applications/repository/query/PhabricatorRepositoryRefCursorQuery.php b/src/applications/repository/query/PhabricatorRepositoryRefCursorQuery.php index c7231bdc27..84b8b144ec 100644 --- a/src/applications/repository/query/PhabricatorRepositoryRefCursorQuery.php +++ b/src/applications/repository/query/PhabricatorRepositoryRefCursorQuery.php @@ -1,83 +1,101 @@ repositoryPHIDs = $phids; return $this; } public function withRefTypes(array $types) { $this->refTypes = $types; return $this; } + public function withRefNames(array $names) { + $this->refNames = $names; + return $this; + } + protected function loadPage() { $table = new PhabricatorRepositoryRefCursor(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T r %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } public function willFilterPage(array $refs) { $repository_phids = mpull($refs, 'getRepositoryPHID'); $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withPHIDs($repository_phids) ->execute(); $repositories = mpull($repositories, null, 'getPHID'); foreach ($refs as $key => $ref) { $repository = idx($repositories, $ref->getRepositoryPHID()); if (!$repository) { unset($refs[$key]); continue; } $ref->attachRepository($repository); } return $refs; } private function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); - if ($this->repositoryPHIDs) { + if ($this->repositoryPHIDs !== null) { $where[] = qsprintf( $conn_r, 'repositoryPHID IN (%Ls)', $this->repositoryPHIDs); } - if ($this->refTypes) { + if ($this->refTypes !== null) { $where[] = qsprintf( $conn_r, 'refType IN (%Ls)', $this->refTypes); } + if ($this->refNames !== null) { + $name_hashes = array(); + foreach ($this->refNames as $name) { + $name_hashes[] = PhabricatorHash::digestForIndex($name); + } + + $where[] = qsprintf( + $conn_r, + 'refNameHash IN (%Ls)', + $name_hashes); + } + $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } public function getQueryApplicationClass() { return 'PhabricatorApplicationDiffusion'; } }