diff --git a/src/applications/diffusion/controller/DiffusionMirrorEditController.php b/src/applications/diffusion/controller/DiffusionMirrorEditController.php index a3a6471184..4066cea856 100644 --- a/src/applications/diffusion/controller/DiffusionMirrorEditController.php +++ b/src/applications/diffusion/controller/DiffusionMirrorEditController.php @@ -1,121 +1,127 @@ id = idx($data, 'id'); parent::willProcessRequest($data); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $drequest = $this->diffusionRequest; $repository = $drequest->getRepository(); if ($this->id) { $mirror = id(new PhabricatorRepositoryMirrorQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->executeOne(); if (!$mirror) { return new Aphront404Response(); } $is_new = false; } else { $mirror = PhabricatorRepositoryMirror::initializeNewMirror($viewer) ->setRepositoryPHID($repository->getPHID()) ->attachRepository($repository); $is_new = true; } $edit_uri = $this->getRepositoryControllerURI($repository, 'edit/#mirrors'); $v_remote = $mirror->getRemoteURI(); $e_remote = true; $v_credentials = $mirror->getCredentialPHID(); $e_credentials = null; $credentials = id(new PassphraseCredentialQuery()) ->setViewer($viewer) ->withIsDestroyed(false) ->execute(); $errors = array(); if ($request->isFormPost()) { $v_remote = $request->getStr('remoteURI'); if (strlen($v_remote)) { - $e_remote = null; + try { + PhabricatorRepository::assertValidRemoteURI($v_remote); + $e_remote = null; + } catch (Exception $ex) { + $e_remote = pht('Invalid'); + $errors[] = $ex->getMessage(); + } } else { $e_remote = pht('Required'); $errors[] = pht('You must provide a remote URI.'); } $v_credentials = $request->getStr('credential'); if ($v_credentials) { $phids = mpull($credentials, null, 'getPHID'); if (empty($phids[$v_credentials])) { $e_credentials = pht('Invalid'); $errors[] = pht( 'You do not have permission to use those credentials.'); } } if (!$errors) { $mirror ->setRemoteURI($v_remote) ->setCredentialPHID($v_credentials) ->save(); return id(new AphrontReloadResponse())->setURI($edit_uri); } } $form_errors = null; if ($errors) { $form_errors = id(new AphrontErrorView()) ->setErrors($errors); } if ($is_new) { $title = pht('Create Mirror'); $submit = pht('Create Mirror'); } else { $title = pht('Edit Mirror'); $submit = pht('Save Changes'); } $form = id(new PHUIFormLayoutView()) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Remote URI')) ->setName('remoteURI') ->setValue($v_remote) ->setError($e_remote)) ->appendChild( id(new PassphraseCredentialControl()) ->setLabel(pht('Credentials')) ->setName('credential') ->setAllowNull(true) ->setValue($v_credentials) ->setError($e_credentials) ->setOptions($credentials)); $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle($title) ->setWidth(AphrontDialogView::WIDTH_FORM) ->appendChild($form_errors) ->appendChild($form) ->addSubmitButton($submit) ->addCancelButton($edit_uri); return id(new AphrontDialogResponse()) ->setDialog($dialog); } } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php b/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php index 9a65a5bfd0..b44f9124c3 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php @@ -1,920 +1,854 @@ edit = $data['edit']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); // NOTE: We can end up here via either "Create Repository", or via // "Import Repository", or via "Edit Remote", or via "Edit Policies". In // the latter two cases, we show only a few of the pages. $repository = null; switch ($this->edit) { case 'remote': case 'policy': $repository = $this->getDiffusionRequest()->getRepository(); // Make sure we have CAN_EDIT. PhabricatorPolicyFilter::requireCapability( $viewer, $repository, PhabricatorPolicyCapability::CAN_EDIT); $this->setRepository($repository); $cancel_uri = $this->getRepositoryControllerURI($repository, 'edit/'); break; case 'import': case 'create': $this->requireApplicationCapability( DiffusionCapabilityCreateRepositories::CAPABILITY); $cancel_uri = $this->getApplicationURI('new/'); break; default: throw new Exception('Invalid edit operation!'); } $form = id(new PHUIPagedFormView()) ->setUser($viewer) ->setCancelURI($cancel_uri); switch ($this->edit) { case 'remote': $title = pht('Edit Remote'); $form ->addPage('remote-uri', $this->buildRemoteURIPage()) ->addPage('auth', $this->buildAuthPage()); break; case 'policy': $title = pht('Edit Policies'); $form ->addPage('policy', $this->buildPolicyPage()); break; case 'create': $title = pht('Create Repository'); $form ->addPage('vcs', $this->buildVCSPage()) ->addPage('name', $this->buildNamePage()) ->addPage('policy', $this->buildPolicyPage()) ->addPage('done', $this->buildDonePage()); break; case 'import': $title = pht('Import Repository'); $form ->addPage('vcs', $this->buildVCSPage()) ->addPage('name', $this->buildNamePage()) ->addPage('remote-uri', $this->buildRemoteURIPage()) ->addPage('auth', $this->buildAuthPage()) ->addPage('policy', $this->buildPolicyPage()) ->addPage('done', $this->buildDonePage()); break; } if ($request->isFormPost()) { $form->readFromRequest($request); if ($form->isComplete()) { $is_create = ($this->edit === 'import' || $this->edit === 'create'); $is_auth = ($this->edit == 'import' || $this->edit == 'remote'); $is_policy = ($this->edit != 'remote'); $is_init = ($this->edit == 'create'); if ($is_create) { $repository = PhabricatorRepository::initializeNewRepository( $viewer); } $template = id(new PhabricatorRepositoryTransaction()); $type_name = PhabricatorRepositoryTransaction::TYPE_NAME; $type_vcs = PhabricatorRepositoryTransaction::TYPE_VCS; $type_activate = PhabricatorRepositoryTransaction::TYPE_ACTIVATE; $type_local_path = PhabricatorRepositoryTransaction::TYPE_LOCAL_PATH; $type_remote_uri = PhabricatorRepositoryTransaction::TYPE_REMOTE_URI; $type_hosting = PhabricatorRepositoryTransaction::TYPE_HOSTING; $type_credential = PhabricatorRepositoryTransaction::TYPE_CREDENTIAL; $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY; $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY; $type_push = PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY; $xactions = array(); // If we're creating a new repository, set all this core stuff. if ($is_create) { $callsign = $form->getPage('name') ->getControl('callsign')->getValue(); // We must set this to a unique value to save the repository // initially, and it's immutable, so we don't bother using // transactions to apply this change. $repository->setCallsign($callsign); // Put the repository in "Importing" mode until we finish // parsing it. $repository->setDetail('importing', true); $xactions[] = id(clone $template) ->setTransactionType($type_name) ->setNewValue( $form->getPage('name')->getControl('name')->getValue()); $xactions[] = id(clone $template) ->setTransactionType($type_vcs) ->setNewValue( $form->getPage('vcs')->getControl('vcs')->getValue()); $activate = $form->getPage('done') ->getControl('activate')->getValue(); $xactions[] = id(clone $template) ->setTransactionType($type_activate) ->setNewValue( ($activate == 'start')); $default_local_path = PhabricatorEnv::getEnvConfig( 'repository.default-local-path'); $default_local_path = rtrim($default_local_path, '/'); $default_local_path = $default_local_path.'/'.$callsign.'/'; $xactions[] = id(clone $template) ->setTransactionType($type_local_path) ->setNewValue($default_local_path); } if ($is_init) { $xactions[] = id(clone $template) ->setTransactionType($type_hosting) ->setNewValue(true); } if ($is_auth) { $xactions[] = id(clone $template) ->setTransactionType($type_remote_uri) ->setNewValue( $form->getPage('remote-uri')->getControl('remoteURI') ->getValue()); $xactions[] = id(clone $template) ->setTransactionType($type_credential) ->setNewValue( $form->getPage('auth')->getControl('credential')->getValue()); } if ($is_policy) { $xactions[] = id(clone $template) ->setTransactionType($type_view) ->setNewValue( $form->getPage('policy')->getControl('viewPolicy')->getValue()); $xactions[] = id(clone $template) ->setTransactionType($type_edit) ->setNewValue( $form->getPage('policy')->getControl('editPolicy')->getValue()); if ($is_init || $repository->isHosted()) { $xactions[] = id(clone $template) ->setTransactionType($type_push) ->setNewValue( $form->getPage('policy')->getControl('pushPolicy')->getValue()); } } id(new PhabricatorRepositoryEditor()) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->setActor($viewer) ->applyTransactions($repository, $xactions); $repo_uri = $this->getRepositoryControllerURI($repository, 'edit/'); return id(new AphrontRedirectResponse())->setURI($repo_uri); } } else { $dict = array(); if ($repository) { $dict = array( 'remoteURI' => $repository->getRemoteURI(), 'credential' => $repository->getCredentialPHID(), 'viewPolicy' => $repository->getViewPolicy(), 'editPolicy' => $repository->getEditPolicy(), 'pushPolicy' => $repository->getPushPolicy(), ); } $form->readFromObject($dict); } $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($title); return $this->buildApplicationPage( array( $crumbs, $form, ), array( 'title' => $title, 'device' => true, )); } /* -( Page: VCS Type )----------------------------------------------------- */ private function buildVCSPage() { $is_import = ($this->edit == 'import'); if ($is_import) { $git_str = pht( 'Import a Git repository (for example, a repository hosted '. 'on GitHub).'); $hg_str = pht( 'Import a Mercurial repository (for example, a repository '. 'hosted on Bitbucket).'); $svn_str = pht('Import a Subversion repository.'); } else { $git_str = pht('Create a new, empty Git repository.'); $hg_str = pht('Create a new, empty Mercurial repository.'); $svn_str = pht('Create a new, empty Subversion repository.'); } $control = id(new AphrontFormRadioButtonControl()) ->setName('vcs') ->setLabel(pht('Type')) ->addButton( PhabricatorRepositoryType::REPOSITORY_TYPE_GIT, pht('Git'), $git_str) ->addButton( PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL, pht('Mercurial'), $hg_str) ->addButton( PhabricatorRepositoryType::REPOSITORY_TYPE_SVN, pht('Subversion'), $svn_str); if ($is_import) { $control->addButton( PhabricatorRepositoryType::REPOSITORY_TYPE_PERFORCE, pht('Perforce'), pht( 'Perforce is not directly supported, but you can import '. 'a Perforce repository as a Git repository using %s.', phutil_tag( 'a', array( 'href' => 'http://www.perforce.com/product/components/git-fusion', 'target' => '_blank', ), pht('Perforce Git Fusion'))), 'disabled', $disabled = true); } return id(new PHUIFormPageView()) ->setPageName(pht('Repository Type')) ->setUser($this->getRequest()->getUser()) ->setValidateFormPageCallback(array($this, 'validateVCSPage')) ->addControl($control); } public function validateVCSPage(PHUIFormPageView $page) { $valid = array( PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => true, PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => true, PhabricatorRepositoryType::REPOSITORY_TYPE_SVN => true, ); $c_vcs = $page->getControl('vcs'); $v_vcs = $c_vcs->getValue(); if (!$v_vcs) { $c_vcs->setError(pht('Required')); $page->addPageError( pht('You must select a version control system.')); } else if (empty($valid[$v_vcs])) { $c_vcs->setError(pht('Invalid')); $page->addPageError( pht('You must select a valid version control system.')); } return $c_vcs->isValid(); } /* -( Page: Name and Callsign )-------------------------------------------- */ private function buildNamePage() { return id(new PHUIFormPageView()) ->setUser($this->getRequest()->getUser()) ->setPageName(pht('Repository Name and Location')) ->setValidateFormPageCallback(array($this, 'validateNamePage')) ->addRemarkupInstructions( pht( '**Choose a human-readable name for this repository**, like '. '"CompanyName Mobile App" or "CompanyName Backend Server". You '. 'can change this later.')) ->addControl( id(new AphrontFormTextControl()) ->setName('name') ->setLabel(pht('Name')) ->setCaption(pht('Human-readable repository name.'))) ->addRemarkupInstructions( pht( '**Choose a "Callsign" for the repository.** This is a short, '. 'unique string which identifies commits elsewhere in Phabricator. '. 'For example, you might use `M` for your mobile app repository '. 'and `B` for your backend repository.'. "\n\n". '**Callsigns must be UPPERCASE**, and can not be edited after the '. 'repository is created. Generally, you should choose short '. 'callsigns.')) ->addControl( id(new AphrontFormTextControl()) ->setName('callsign') ->setLabel(pht('Callsign')) ->setCaption(pht('Short UPPERCASE identifier.'))); } public function validateNamePage(PHUIFormPageView $page) { $c_name = $page->getControl('name'); $v_name = $c_name->getValue(); if (!strlen($v_name)) { $c_name->setError(pht('Required')); $page->addPageError( pht('You must choose a name for this repository.')); } $c_call = $page->getControl('callsign'); $v_call = $c_call->getValue(); if (!strlen($v_call)) { $c_call->setError(pht('Required')); $page->addPageError( pht('You must choose a callsign for this repository.')); } else if (!preg_match('/^[A-Z]+\z/', $v_call)) { $c_call->setError(pht('Invalid')); $page->addPageError( pht('The callsign must contain only UPPERCASE letters.')); } else { $exists = false; try { $repo = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getRequest()->getUser()) ->withCallsigns(array($v_call)) ->executeOne(); $exists = (bool)$repo; } catch (PhabricatorPolicyException $ex) { $exists = true; } if ($exists) { $c_call->setError(pht('Not Unique')); $page->addPageError( pht( 'Another repository already uses that callsign. You must choose '. 'a unique callsign.')); } } return $c_name->isValid() && $c_call->isValid(); } /* -( Page: Remote URI )--------------------------------------------------- */ private function buildRemoteURIPage() { return id(new PHUIFormPageView()) ->setUser($this->getRequest()->getUser()) ->setPageName(pht('Repository Remote URI')) ->setValidateFormPageCallback(array($this, 'validateRemoteURIPage')) ->setAdjustFormPageCallback(array($this, 'adjustRemoteURIPage')) ->addControl( id(new AphrontFormTextControl()) ->setName('remoteURI')); } public function adjustRemoteURIPage(PHUIFormPageView $page) { $form = $page->getForm(); $is_git = false; $is_svn = false; $is_mercurial = false; if ($this->getRepository()) { $vcs = $this->getRepository()->getVersionControlSystem(); } else { $vcs = $form->getPage('vcs')->getControl('vcs')->getValue(); } switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $is_git = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $is_svn = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $is_mercurial = true; break; default: throw new Exception('Unsupported VCS!'); } $has_local = ($is_git || $is_mercurial); if ($is_git) { $uri_label = pht('Remote URI'); $instructions = pht( 'Enter the URI to clone this Git repository from. It should usually '. 'look like one of these examples:'. "\n\n". "| Example Git Remote URIs |\n". "| ----------------------- |\n". "| `git@github.com:example/example.git` |\n". "| `ssh://user@host.com/git/example.git` |\n". - "| `file:///local/path/to/repo` |\n". "| `https://example.com/repository.git` |\n"); } else if ($is_mercurial) { $uri_label = pht('Remote URI'); $instructions = pht( 'Enter the URI to clone this Mercurial repository from. It should '. 'usually look like one of these examples:'. "\n\n". "| Example Mercurial Remote URIs |\n". "| ----------------------- |\n". "| `ssh://hg@bitbucket.org/example/repository` |\n". - "| `file:///local/path/to/repo` |\n"); + "| `https://bitbucket.org/example/repository` |\n"); } else if ($is_svn) { $uri_label = pht('Repository Root'); $instructions = pht( 'Enter the **Repository Root** for this Subversion repository. '. 'You can figure this out by running `svn info` in a working copy '. 'and looking at the value in the `Repository Root` field. It '. 'should be a URI and will usually look like these:'. "\n\n". "| Example Subversion Repository Root URIs |\n". "| ------------------------------ |\n". "| `http://svn.example.org/svnroot/` |\n". "| `svn+ssh://svn.example.com/svnroot/` |\n". "| `svn://svn.example.net/svnroot/` |\n". - "| `file:///local/path/to/svnroot/` |\n". "\n\n". "You **MUST** specify the root of the repository, not a ". "subdirectory. (If you want to import only part of a Subversion ". "repository, use the //Import Only// option at the end of this ". "workflow.)"); } else { throw new Exception('Unsupported VCS!'); } $page->addRemarkupInstructions($instructions, 'remoteURI'); $page->getControl('remoteURI')->setLabel($uri_label); } public function validateRemoteURIPage(PHUIFormPageView $page) { $c_remote = $page->getControl('remoteURI'); $v_remote = $c_remote->getValue(); if (!strlen($v_remote)) { $c_remote->setError(pht('Required')); $page->addPageError( pht('You must specify a URI.')); } else { - $proto = $this->getRemoteURIProtocol($v_remote); - - if ($proto === 'file') { - if (!preg_match('@^file:///@', $v_remote)) { - $c_remote->setError(pht('Invalid')); - $page->addPageError( - pht( - "URIs using the 'file://' protocol should have three slashes ". - "(e.g., 'file:///absolute/path/to/file'). You only have two. ". - "Add another one.")); - } - } - - // Catch confusion between Git/SCP-style URIs and normal URIs. See T3619 - // for discussion. This is usually a user adding "ssh://" to an implicit - // SSH Git URI. - if ($proto == 'ssh') { - if (preg_match('(^[^:@]+://[^/:]+:[^\d])', $v_remote)) { - $c_remote->setError(pht('Invalid')); - $page->addPageError( - pht( - "The Remote URI is not formatted correctly. Remote URIs ". - "with an explicit protocol should be in the form ". - "'proto://domain/path', not 'proto://domain:/path'. ". - "The ':/path' syntax is only valid in SCP-style URIs.")); - } - } - - switch ($proto) { - case 'ssh': - case 'http': - case 'https': - case 'file': - case 'git': - case 'svn': - case 'svn+ssh': - break; - default: - $c_remote->setError(pht('Invalid')); - $page->addPageError( - pht( - "The URI protocol is unrecognized. It should begin ". - "'ssh://', 'http://', 'https://', 'file://', 'git://', ". - "'svn://', 'svn+ssh://', or be in the form ". - "'git@domain.com:path'.")); - break; + try { + PhabricatorRepository::assertValidRemoteURI($v_remote); + } catch (Exception $ex) { + $c_remote->setError(pht('Invalid')); + $page->addPageError($ex->getMessage()); } } return $c_remote->isValid(); } /* -( Page: Authentication )----------------------------------------------- */ public function buildAuthPage() { return id(new PHUIFormPageView()) ->setPageName(pht('Authentication')) ->setUser($this->getRequest()->getUser()) ->setAdjustFormPageCallback(array($this, 'adjustAuthPage')) ->addControl( id(new PassphraseCredentialControl()) ->setName('credential')); } public function adjustAuthPage($page) { $form = $page->getForm(); if ($this->getRepository()) { $vcs = $this->getRepository()->getVersionControlSystem(); } else { $vcs = $form->getPage('vcs')->getControl('vcs')->getValue(); } $remote_uri = $form->getPage('remote-uri') ->getControl('remoteURI') ->getValue(); - $proto = $this->getRemoteURIProtocol($remote_uri); + + $proto = PhabricatorRepository::getRemoteURIProtocol($remote_uri); $remote_user = $this->getRemoteURIUser($remote_uri); $c_credential = $page->getControl('credential'); $c_credential->setDefaultUsername($remote_user); if ($this->isSSHProtocol($proto)) { $c_credential->setLabel(pht('SSH Key')); $c_credential->setCredentialType( PassphraseCredentialTypeSSHPrivateKeyText::CREDENTIAL_TYPE); $provides_type = PassphraseCredentialTypeSSHPrivateKey::PROVIDES_TYPE; $page->addRemarkupInstructions( pht( 'Choose or add the SSH credentials to use to connect to the the '. 'repository hosted at:'. "\n\n". " lang=text\n". " %s", $remote_uri), 'credential'); } else if ($this->isUsernamePasswordProtocol($proto)) { $c_credential->setLabel(pht('Password')); $c_credential->setAllowNull(true); $c_credential->setCredentialType( PassphraseCredentialTypePassword::CREDENTIAL_TYPE); $provides_type = PassphraseCredentialTypePassword::PROVIDES_TYPE; $page->addRemarkupInstructions( pht( 'Choose the username and password used to connect to the '. 'repository hosted at:'. "\n\n". " lang=text\n". " %s". "\n\n". "If this repository does not require a username or password, ". "you can continue to the next step.", $remote_uri), 'credential'); - } else if ($proto == 'file') { - $c_credential->setHidden(true); - $provides_type = null; - - $page->addRemarkupInstructions( - pht( - 'You do not need to configure any credentials for repositories '. - 'accessed over the `file://` protocol. Continue to the next step.'), - 'credential'); } else { throw new Exception('Unknown URI protocol!'); } if ($provides_type) { $viewer = $this->getRequest()->getUser(); $options = id(new PassphraseCredentialQuery()) ->setViewer($viewer) ->withIsDestroyed(false) ->withProvidesTypes(array($provides_type)) ->execute(); $c_credential->setOptions($options); } } public function validateAuthPage(PHUIFormPageView $page) { $form = $page->getForm(); $remote_uri = $form->getPage('remote')->getControl('remoteURI')->getValue(); $proto = $this->getRemoteURIProtocol($remote_uri); $c_credential = $page->getControl('credential'); $v_credential = $c_credential->getValue(); // NOTE: We're using the omnipotent user here because the viewer might be // editing a repository they're allowed to edit which uses a credential they // are not allowed to see. This is fine, as long as they don't change it. $credential = id(new PassphraseCredentialQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($v_credential)) ->executeOne(); if ($this->isSSHProtocol($proto)) { if (!$credential) { $c_credential->setError(pht('Required')); $page->addPageError( pht('You must choose an SSH credential to connect over SSH.')); } $ssh_type = PassphraseCredentialTypeSSHPrivateKey::PROVIDES_TYPE; if ($credential->getProvidesType() !== $ssh_type) { $c_credential->setError(pht('Invalid')); $page->addPageError( pht( 'You must choose an SSH credential, not some other type '. 'of credential.')); } } else if ($this->isUsernamePasswordProtocol($proto)) { if ($credential) { $password_type = PassphraseCredentialTypePassword::PROVIDES_TYPE; if ($credential->getProvidesType() !== $password_type) { $c_credential->setError(pht('Invalid')); $page->addPageError( pht( 'You must choose a username/password credential, not some other '. 'type of credential.')); } } return $c_credential->isValid(); } else { return true; } } /* -( Page: Policy )------------------------------------------------------- */ private function buildPolicyPage() { $viewer = $this->getRequest()->getUser(); if ($this->getRepository()) { $repository = $this->getRepository(); } else { $repository = PhabricatorRepository::initializeNewRepository($viewer); } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($repository) ->execute(); $view_policy = id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($repository) ->setPolicies($policies) ->setName('viewPolicy'); $edit_policy = id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($repository) ->setPolicies($policies) ->setName('editPolicy'); $push_policy = id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setCapability(DiffusionCapabilityPush::CAPABILITY) ->setPolicyObject($repository) ->setPolicies($policies) ->setName('pushPolicy'); return id(new PHUIFormPageView()) ->setPageName(pht('Policies')) ->setValidateFormPageCallback(array($this, 'validatePolicyPage')) ->setAdjustFormPageCallback(array($this, 'adjustPolicyPage')) ->setUser($viewer) ->addRemarkupInstructions( pht( 'Select access policies for this repository.')) ->addControl($view_policy) ->addControl($edit_policy) ->addControl($push_policy); } public function adjustPolicyPage(PHUIFormPageView $page) { if ($this->getRepository()) { $repository = $this->getRepository(); $show_push = $repository->isHosted(); } else { $show_push = ($this->edit == 'create'); } if (!$show_push) { $c_push = $page->getControl('pushPolicy'); $c_push->setHidden(true); } } public function validatePolicyPage(PHUIFormPageView $page) { $form = $page->getForm(); $viewer = $this->getRequest()->getUser(); $c_view = $page->getControl('viewPolicy'); $c_edit = $page->getControl('editPolicy'); $c_push = $page->getControl('pushPolicy'); $v_view = $c_view->getValue(); $v_edit = $c_edit->getValue(); $v_push = $c_push->getValue(); if ($this->getRepository()) { $repository = $this->getRepository(); } else { $repository = PhabricatorRepository::initializeNewRepository($viewer); } $proxy = clone $repository; $proxy->setViewPolicy($v_view); $proxy->setEditPolicy($v_edit); $can_view = PhabricatorPolicyFilter::hasCapability( $viewer, $proxy, PhabricatorPolicyCapability::CAN_VIEW); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $proxy, PhabricatorPolicyCapability::CAN_EDIT); if (!$can_view) { $c_view->setError(pht('Invalid')); $page->addPageError( pht( 'You can not use the selected policy, because you would be unable '. 'to see the repository.')); } if (!$can_edit) { $c_edit->setError(pht('Invalid')); $page->addPageError( pht( 'You can not use the selected edit policy, because you would be '. 'unable to edit the repository.')); } return $c_view->isValid() && $c_edit->isValid(); } /* -( Page: Done )--------------------------------------------------------- */ private function buildDonePage() { $is_create = ($this->edit == 'create'); if ($is_create) { $now_label = pht('Create Repository Now'); $now_caption = pht( 'Create the repository right away. This will create the repository '. 'using default settings.'); $wait_label = pht('Configure More Options First'); $wait_caption = pht( 'Configure more options before creating the repository. '. 'This will let you fine-tune settings. You can create the repository '. 'whenever you are ready.'); } else { $now_label = pht('Start Import Now'); $now_caption = pht( 'Start importing the repository right away. This will import '. 'the entire repository using default settings.'); $wait_label = pht('Configure More Options First'); $wait_caption = pht( 'Configure more options before beginning the repository '. 'import. This will let you fine-tune settings. You can '. 'start the import whenever you are ready.'); } return id(new PHUIFormPageView()) ->setPageName(pht('Repository Ready!')) ->setValidateFormPageCallback(array($this, 'validateDonePage')) ->setUser($this->getRequest()->getUser()) ->addControl( id(new AphrontFormRadioButtonControl()) ->setName('activate') ->setLabel(pht('Start Now')) ->addButton( 'start', $now_label, $now_caption) ->addButton( 'wait', $wait_label, $wait_caption)); } public function validateDonePage(PHUIFormPageView $page) { $c_activate = $page->getControl('activate'); $v_activate = $c_activate->getValue(); if ($v_activate != 'start' && $v_activate != 'wait') { $c_activate->setError(pht('Required')); $page->addPageError( pht('Make a choice about repository activation.')); } return $c_activate->isValid(); } /* -( Internal )----------------------------------------------------------- */ - - private function getRemoteURIProtocol($raw_uri) { - $uri = new PhutilURI($raw_uri); - if ($uri->getProtocol()) { - return strtolower($uri->getProtocol()); - } - - $git_uri = new PhutilGitURI($raw_uri); - if (strlen($git_uri->getDomain()) && strlen($git_uri->getPath())) { - return 'ssh'; - } - - return null; - } - private function getRemoteURIUser($raw_uri) { $uri = new PhutilURI($raw_uri); if ($uri->getUser()) { return $uri->getUser(); } $git_uri = new PhutilGitURI($raw_uri); if (strlen($git_uri->getDomain()) && strlen($git_uri->getPath())) { return $git_uri->getUser(); } return null; } private function isSSHProtocol($proto) { return ($proto == 'git' || $proto == 'ssh' || $proto == 'svn+ssh'); } private function isUsernamePasswordProtocol($proto) { return ($proto == 'http' || $proto == 'https' || $proto == 'svn'); } private function setRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; } private function getRepository() { return $this->repository; } } diff --git a/src/applications/repository/conduit/ConduitAPI_repository_create_Method.php b/src/applications/repository/conduit/ConduitAPI_repository_create_Method.php index b18f935929..87dd64b8dc 100644 --- a/src/applications/repository/conduit/ConduitAPI_repository_create_Method.php +++ b/src/applications/repository/conduit/ConduitAPI_repository_create_Method.php @@ -1,136 +1,137 @@ formatStringConstants(array('git', 'hg', 'svn')); return array( 'name' => 'required string', 'vcs' => 'required '.$vcs_const, 'callsign' => 'required string', 'description' => 'optional string', 'encoding' => 'optional string', 'tracking' => 'optional bool', 'uri' => 'optional string', 'credentialPHID' => 'optional string', - 'localPath' => 'optional string', 'svnSubpath' => 'optional string', 'branchFilter' => 'optional list', 'closeCommitsFilter' => 'optional list', 'pullFrequency' => 'optional int', 'defaultBranch' => 'optional string', 'heraldEnabled' => 'optional bool, default = true', 'autocloseEnabled' => 'optional bool, default = true', 'svnUUID' => 'optional string', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array( 'ERR-PERMISSIONS' => 'You do not have the authority to call this method.', 'ERR-DUPLICATE' => 'Duplicate repository callsign.', 'ERR-BAD-CALLSIGN' => 'Callsign is required and must be ALL UPPERCASE LETTERS.', 'ERR-UNKNOWN-REPOSITORY-VCS' => 'Unknown repository VCS type.', ); } protected function execute(ConduitAPIRequest $request) { $application = id(new PhabricatorApplicationQuery()) ->setViewer($request->getUser()) ->withClasses(array('PhabricatorApplicationDiffusion')) ->executeOne(); PhabricatorPolicyFilter::requireCapability( $request->getUser(), $application, DiffusionCapabilityCreateRepositories::CAPABILITY); // TODO: This has some duplication with (and lacks some of the validation // of) the web workflow; refactor things so they can share more code as this // stabilizes. Specifically, this should move to transactions since they // work properly now. $repository = PhabricatorRepository::initializeNewRepository( $request->getUser()); $repository->setName($request->getValue('name')); $callsign = $request->getValue('callsign'); if (!preg_match('/^[A-Z]+\z/', $callsign)) { throw new ConduitException('ERR-BAD-CALLSIGN'); } $repository->setCallsign($callsign); $vcs = $request->getValue('vcs'); $map = array( 'git' => PhabricatorRepositoryType::REPOSITORY_TYPE_GIT, 'hg' => PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL, 'svn' => PhabricatorRepositoryType::REPOSITORY_TYPE_SVN, ); if (empty($map[$vcs])) { throw new ConduitException('ERR-UNKNOWN-REPOSITORY-VCS'); } $repository->setVersionControlSystem($map[$vcs]); $repository->setCredentialPHID($request->getValue('credentialPHID')); + $remote_uri = $request->getValue('uri'); + PhabricatorRepository::assertValidRemoteURI($remote_uri); + $details = array( 'encoding' => $request->getValue('encoding'), 'description' => $request->getValue('description'), 'tracking-enabled' => (bool)$request->getValue('tracking', true), - 'remote-uri' => $request->getValue('uri'), - 'local-path' => $request->getValue('localPath'), + 'remote-uri' => $remote_uri, 'branch-filter' => array_fill_keys( $request->getValue('branchFilter', array()), true), 'close-commits-filter' => array_fill_keys( $request->getValue('closeCommitsFilter', array()), true), 'pull-frequency' => $request->getValue('pullFrequency'), 'default-branch' => $request->getValue('defaultBranch'), 'herald-disabled' => !$request->getValue('heraldEnabled', true), 'svn-subpath' => $request->getValue('svnSubpath'), 'disable-autoclose' => !$request->getValue('autocloseEnabled', true), ); foreach ($details as $key => $value) { $repository->setDetail($key, $value); } try { $repository->save(); } catch (AphrontQueryDuplicateKeyException $ex) { throw new ConduitException('ERR-DUPLICATE'); } return $repository->toDictionary(); } } diff --git a/src/applications/repository/editor/PhabricatorRepositoryEditor.php b/src/applications/repository/editor/PhabricatorRepositoryEditor.php index 0ab9e90158..700a271c84 100644 --- a/src/applications/repository/editor/PhabricatorRepositoryEditor.php +++ b/src/applications/repository/editor/PhabricatorRepositoryEditor.php @@ -1,342 +1,357 @@ getTransactionType()) { case PhabricatorRepositoryTransaction::TYPE_VCS: return $object->getVersionControlSystem(); case PhabricatorRepositoryTransaction::TYPE_ACTIVATE: return $object->isTracked(); case PhabricatorRepositoryTransaction::TYPE_NAME: return $object->getName(); case PhabricatorRepositoryTransaction::TYPE_DESCRIPTION: return $object->getDetail('description'); case PhabricatorRepositoryTransaction::TYPE_ENCODING: return $object->getDetail('encoding'); case PhabricatorRepositoryTransaction::TYPE_DEFAULT_BRANCH: return $object->getDetail('default-branch'); case PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY: return array_keys($object->getDetail('branch-filter', array())); case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY: return array_keys($object->getDetail('close-commits-filter', array())); case PhabricatorRepositoryTransaction::TYPE_UUID: return $object->getUUID(); case PhabricatorRepositoryTransaction::TYPE_SVN_SUBPATH: return $object->getDetail('svn-subpath'); case PhabricatorRepositoryTransaction::TYPE_NOTIFY: return (int)!$object->getDetail('herald-disabled'); case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE: return (int)!$object->getDetail('disable-autoclose'); case PhabricatorRepositoryTransaction::TYPE_REMOTE_URI: return $object->getDetail('remote-uri'); case PhabricatorRepositoryTransaction::TYPE_LOCAL_PATH: return $object->getDetail('local-path'); case PhabricatorRepositoryTransaction::TYPE_HOSTING: return $object->isHosted(); case PhabricatorRepositoryTransaction::TYPE_PROTOCOL_HTTP: return $object->getServeOverHTTP(); case PhabricatorRepositoryTransaction::TYPE_PROTOCOL_SSH: return $object->getServeOverSSH(); case PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY: return $object->getPushPolicy(); case PhabricatorRepositoryTransaction::TYPE_CREDENTIAL: return $object->getCredentialPHID(); case PhabricatorRepositoryTransaction::TYPE_DANGEROUS: return $object->shouldAllowDangerousChanges(); case PhabricatorRepositoryTransaction::TYPE_CLONE_NAME: return $object->getDetail('clone-name'); } } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorRepositoryTransaction::TYPE_ACTIVATE: case PhabricatorRepositoryTransaction::TYPE_NAME: case PhabricatorRepositoryTransaction::TYPE_DESCRIPTION: case PhabricatorRepositoryTransaction::TYPE_ENCODING: case PhabricatorRepositoryTransaction::TYPE_DEFAULT_BRANCH: case PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY: case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY: case PhabricatorRepositoryTransaction::TYPE_UUID: case PhabricatorRepositoryTransaction::TYPE_SVN_SUBPATH: case PhabricatorRepositoryTransaction::TYPE_REMOTE_URI: case PhabricatorRepositoryTransaction::TYPE_SSH_LOGIN: case PhabricatorRepositoryTransaction::TYPE_SSH_KEY: case PhabricatorRepositoryTransaction::TYPE_SSH_KEYFILE: case PhabricatorRepositoryTransaction::TYPE_HTTP_LOGIN: case PhabricatorRepositoryTransaction::TYPE_HTTP_PASS: case PhabricatorRepositoryTransaction::TYPE_LOCAL_PATH: case PhabricatorRepositoryTransaction::TYPE_VCS: case PhabricatorRepositoryTransaction::TYPE_HOSTING: case PhabricatorRepositoryTransaction::TYPE_PROTOCOL_HTTP: case PhabricatorRepositoryTransaction::TYPE_PROTOCOL_SSH: case PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY: case PhabricatorRepositoryTransaction::TYPE_CREDENTIAL: case PhabricatorRepositoryTransaction::TYPE_DANGEROUS: case PhabricatorRepositoryTransaction::TYPE_CLONE_NAME: return $xaction->getNewValue(); case PhabricatorRepositoryTransaction::TYPE_NOTIFY: case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE: return (int)$xaction->getNewValue(); } } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorRepositoryTransaction::TYPE_VCS: $object->setVersionControlSystem($xaction->getNewValue()); break; case PhabricatorRepositoryTransaction::TYPE_ACTIVATE: $object->setDetail('tracking-enabled', $xaction->getNewValue()); break; case PhabricatorRepositoryTransaction::TYPE_NAME: $object->setName($xaction->getNewValue()); break; case PhabricatorRepositoryTransaction::TYPE_DESCRIPTION: $object->setDetail('description', $xaction->getNewValue()); break; case PhabricatorRepositoryTransaction::TYPE_DEFAULT_BRANCH: $object->setDetail('default-branch', $xaction->getNewValue()); break; case PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY: $object->setDetail( 'branch-filter', array_fill_keys($xaction->getNewValue(), true)); break; case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY: $object->setDetail( 'close-commits-filter', array_fill_keys($xaction->getNewValue(), true)); break; case PhabricatorRepositoryTransaction::TYPE_UUID: $object->setUUID($xaction->getNewValue()); break; case PhabricatorRepositoryTransaction::TYPE_SVN_SUBPATH: $object->setDetail('svn-subpath', $xaction->getNewValue()); break; case PhabricatorRepositoryTransaction::TYPE_NOTIFY: $object->setDetail('herald-disabled', (int)!$xaction->getNewValue()); break; case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE: $object->setDetail('disable-autoclose', (int)!$xaction->getNewValue()); break; case PhabricatorRepositoryTransaction::TYPE_REMOTE_URI: $object->setDetail('remote-uri', $xaction->getNewValue()); break; case PhabricatorRepositoryTransaction::TYPE_LOCAL_PATH: $object->setDetail('local-path', $xaction->getNewValue()); break; case PhabricatorRepositoryTransaction::TYPE_HOSTING: return $object->setHosted($xaction->getNewValue()); case PhabricatorRepositoryTransaction::TYPE_PROTOCOL_HTTP: return $object->setServeOverHTTP($xaction->getNewValue()); case PhabricatorRepositoryTransaction::TYPE_PROTOCOL_SSH: return $object->setServeOverSSH($xaction->getNewValue()); case PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY: return $object->setPushPolicy($xaction->getNewValue()); case PhabricatorRepositoryTransaction::TYPE_CREDENTIAL: return $object->setCredentialPHID($xaction->getNewValue()); case PhabricatorRepositoryTransaction::TYPE_DANGEROUS: $object->setDetail('allow-dangerous-changes', $xaction->getNewValue()); return; case PhabricatorRepositoryTransaction::TYPE_CLONE_NAME: $object->setDetail('clone-name', $xaction->getNewValue()); return; case PhabricatorRepositoryTransaction::TYPE_ENCODING: // Make sure the encoding is valid by converting to UTF-8. This tests // that the user has mbstring installed, and also that they didn't type // a garbage encoding name. Note that we're converting from UTF-8 to // the target encoding, because mbstring is fine with converting from // a nonsense encoding. $encoding = $xaction->getNewValue(); if (strlen($encoding)) { try { phutil_utf8_convert('.', $encoding, 'UTF-8'); } catch (Exception $ex) { throw new PhutilProxyException( pht( "Error setting repository encoding '%s': %s'", $encoding, $ex->getMessage()), $ex); } } $object->setDetail('encoding', $encoding); break; } } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorRepositoryTransaction::TYPE_CREDENTIAL: // Adjust the object <-> credential edge for this repository. $old_phid = $xaction->getOldValue(); $new_phid = $xaction->getNewValue(); $editor = id(new PhabricatorEdgeEditor()) ->setActor($this->requireActor()); $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_USES_CREDENTIAL; $src_phid = $object->getPHID(); if ($old_phid) { $editor->removeEdge($src_phid, $edge_type, $old_phid); } if ($new_phid) { $editor->addEdge($src_phid, $edge_type, $new_phid); } $editor->save(); break; } } protected function mergeTransactions( PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { $type = $u->getTransactionType(); switch ($type) { } return parent::mergeTransactions($u, $v); } protected function transactionHasEffect( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $type = $xaction->getTransactionType(); switch ($type) { } return parent::transactionHasEffect($object, $xaction); } protected function requireCapabilities( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorRepositoryTransaction::TYPE_ACTIVATE: case PhabricatorRepositoryTransaction::TYPE_NAME: case PhabricatorRepositoryTransaction::TYPE_DESCRIPTION: case PhabricatorRepositoryTransaction::TYPE_ENCODING: case PhabricatorRepositoryTransaction::TYPE_DEFAULT_BRANCH: case PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY: case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY: case PhabricatorRepositoryTransaction::TYPE_UUID: case PhabricatorRepositoryTransaction::TYPE_SVN_SUBPATH: case PhabricatorRepositoryTransaction::TYPE_REMOTE_URI: case PhabricatorRepositoryTransaction::TYPE_SSH_LOGIN: case PhabricatorRepositoryTransaction::TYPE_SSH_KEY: case PhabricatorRepositoryTransaction::TYPE_SSH_KEYFILE: case PhabricatorRepositoryTransaction::TYPE_HTTP_LOGIN: case PhabricatorRepositoryTransaction::TYPE_HTTP_PASS: case PhabricatorRepositoryTransaction::TYPE_LOCAL_PATH: case PhabricatorRepositoryTransaction::TYPE_VCS: case PhabricatorRepositoryTransaction::TYPE_NOTIFY: case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE: case PhabricatorRepositoryTransaction::TYPE_HOSTING: case PhabricatorRepositoryTransaction::TYPE_PROTOCOL_HTTP: case PhabricatorRepositoryTransaction::TYPE_PROTOCOL_SSH: case PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY: case PhabricatorRepositoryTransaction::TYPE_CREDENTIAL: case PhabricatorRepositoryTransaction::TYPE_DANGEROUS: case PhabricatorRepositoryTransaction::TYPE_CLONE_NAME: PhabricatorPolicyFilter::requireCapability( $this->requireActor(), $object, PhabricatorPolicyCapability::CAN_EDIT); break; } } protected function validateTransaction( PhabricatorLiskDAO $object, $type, array $xactions) { $errors = parent::validateTransaction($object, $type, $xactions); switch ($type) { + case PhabricatorRepositoryTransaction::TYPE_REMOTE_URI: + foreach ($xactions as $xaction) { + $new_uri = $xaction->getNewValue(); + try { + PhabricatorRepository::assertValidRemoteURI($new_uri); + } catch (Exception $ex) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + $ex->getMessage(), + $xaction); + } + } + break; + case PhabricatorRepositoryTransaction::TYPE_CREDENTIAL: $ok = PassphraseCredentialControl::validateTransactions( $this->getActor(), $xactions); if (!$ok) { foreach ($xactions as $xaction) { $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), pht( 'The selected credential does not exist, or you do not have '. 'permission to use it.'), $xaction); } } break; } return $errors; } } diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index 4ac35afe75..d2a5990e82 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -1,1337 +1,1399 @@ setViewer($actor) ->withClasses(array('PhabricatorApplicationDiffusion')) ->executeOne(); $view_policy = $app->getPolicy(DiffusionCapabilityDefaultView::CAPABILITY); $edit_policy = $app->getPolicy(DiffusionCapabilityDefaultEdit::CAPABILITY); $push_policy = $app->getPolicy(DiffusionCapabilityDefaultPush::CAPABILITY); return id(new PhabricatorRepository()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setPushPolicy($push_policy); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorRepositoryPHIDTypeRepository::TYPECONST); } public function toDictionary() { return array( 'id' => $this->getID(), 'name' => $this->getName(), 'phid' => $this->getPHID(), 'callsign' => $this->getCallsign(), 'monogram' => $this->getMonogram(), 'vcs' => $this->getVersionControlSystem(), 'uri' => PhabricatorEnv::getProductionURI($this->getURI()), 'remoteURI' => (string)$this->getRemoteURI(), 'description' => $this->getDetail('description'), 'isActive' => $this->isTracked(), 'isHosted' => $this->isHosted(), 'isImporting' => $this->isImporting(), ); } public function getMonogram() { return 'r'.$this->getCallsign(); } public function getDetail($key, $default = null) { return idx($this->details, $key, $default); } public function getHumanReadableDetail($key, $default = null) { $value = $this->getDetail($key, $default); switch ($key) { case 'branch-filter': case 'close-commits-filter': $value = array_keys($value); $value = implode(', ', $value); break; } return $value; } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } public function attachCommitCount($count) { $this->commitCount = $count; return $this; } public function getCommitCount() { return $this->assertAttached($this->commitCount); } public function attachMostRecentCommit( PhabricatorRepositoryCommit $commit = null) { $this->mostRecentCommit = $commit; return $this; } public function getMostRecentCommit() { return $this->assertAttached($this->mostRecentCommit); } public function getDiffusionBrowseURIForPath( PhabricatorUser $user, $path, $line = null, $branch = null) { $drequest = DiffusionRequest::newFromDictionary( array( 'user' => $user, 'repository' => $this, 'path' => $path, 'branch' => $branch, )); return $drequest->generateURI( array( 'action' => 'browse', 'line' => $line, )); } public function getLocalPath() { return $this->getDetail('local-path'); } public function getSubversionBaseURI($commit = null) { $subpath = $this->getDetail('svn-subpath'); if (!strlen($subpath)) { $subpath = null; } return $this->getSubversionPathURI($subpath, $commit); } public function getSubversionPathURI($path = null, $commit = null) { $vcs = $this->getVersionControlSystem(); if ($vcs != PhabricatorRepositoryType::REPOSITORY_TYPE_SVN) { throw new Exception('Not a subversion repository!'); } if ($this->isHosted()) { $uri = 'file://'.$this->getLocalPath(); } else { $uri = $this->getDetail('remote-uri'); } $uri = rtrim($uri, '/'); if (strlen($path)) { $path = rawurlencode($path); $path = str_replace('%2F', '/', $path); $uri = $uri.'/'.ltrim($path, '/'); } if ($path !== null || $commit !== null) { $uri .= '@'; } if ($commit !== null) { $uri .= $commit; } return $uri; } public function attachProjectPHIDs(array $project_phids) { $this->projectPHIDs = $project_phids; return $this; } public function getProjectPHIDs() { return $this->assertAttached($this->projectPHIDs); } /** * Get the name of the directory this repository should clone or checkout * into. For example, if the repository name is "Example Repository", a * reasonable name might be "example-repository". This is used to help users * get reasonable results when cloning repositories, since they generally do * not want to clone into directories called "X/" or "Example Repository/". * * @return string */ public function getCloneName() { $name = $this->getDetail('clone-name'); // Make some reasonable effort to produce reasonable default directory // names from repository names. if (!strlen($name)) { $name = $this->getName(); $name = phutil_utf8_strtolower($name); $name = preg_replace('@[/ -:]+@', '-', $name); $name = trim($name, '-'); if (!strlen($name)) { $name = $this->getCallsign(); } } return $name; } /* -( Remote Command Execution )------------------------------------------- */ public function execRemoteCommand($pattern /* , $arg, ... */) { $args = func_get_args(); return $this->newRemoteCommandFuture($args)->resolve(); } public function execxRemoteCommand($pattern /* , $arg, ... */) { $args = func_get_args(); return $this->newRemoteCommandFuture($args)->resolvex(); } public function getRemoteCommandFuture($pattern /* , $arg, ... */) { $args = func_get_args(); return $this->newRemoteCommandFuture($args); } public function passthruRemoteCommand($pattern /* , $arg, ... */) { $args = func_get_args(); return $this->newRemoteCommandPassthru($args)->execute(); } private function newRemoteCommandFuture(array $argv) { $argv = $this->formatRemoteCommand($argv); $future = newv('ExecFuture', $argv); $future->setEnv($this->getRemoteCommandEnvironment()); return $future; } private function newRemoteCommandPassthru(array $argv) { $argv = $this->formatRemoteCommand($argv); $passthru = newv('PhutilExecPassthru', $argv); $passthru->setEnv($this->getRemoteCommandEnvironment()); return $passthru; } /* -( Local Command Execution )-------------------------------------------- */ public function execLocalCommand($pattern /* , $arg, ... */) { $args = func_get_args(); return $this->newLocalCommandFuture($args)->resolve(); } public function execxLocalCommand($pattern /* , $arg, ... */) { $args = func_get_args(); return $this->newLocalCommandFuture($args)->resolvex(); } public function getLocalCommandFuture($pattern /* , $arg, ... */) { $args = func_get_args(); return $this->newLocalCommandFuture($args); } public function passthruLocalCommand($pattern /* , $arg, ... */) { $args = func_get_args(); return $this->newLocalCommandPassthru($args)->execute(); } private function newLocalCommandFuture(array $argv) { $this->assertLocalExists(); $argv = $this->formatLocalCommand($argv); $future = newv('ExecFuture', $argv); $future->setEnv($this->getLocalCommandEnvironment()); if ($this->usesLocalWorkingCopy()) { $future->setCWD($this->getLocalPath()); } return $future; } private function newLocalCommandPassthru(array $argv) { $this->assertLocalExists(); $argv = $this->formatLocalCommand($argv); $future = newv('PhutilExecPassthru', $argv); $future->setEnv($this->getLocalCommandEnvironment()); if ($this->usesLocalWorkingCopy()) { $future->setCWD($this->getLocalPath()); } return $future; } /* -( Command Infrastructure )--------------------------------------------- */ private function getSSHWrapper() { $root = dirname(phutil_get_library_root('phabricator')); return $root.'/bin/ssh-connect'; } private function getCommonCommandEnvironment() { $env = array( // NOTE: Force the language to "en_US.UTF-8", which overrides locale // settings. This makes stuff print in English instead of, e.g., French, // so we can parse the output of some commands, error messages, etc. 'LANG' => 'en_US.UTF-8', // Propagate PHABRICATOR_ENV explicitly. For discussion, see T4155. 'PHABRICATOR_ENV' => PhabricatorEnv::getSelectedEnvironmentName(), ); switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: // NOTE: See T2965. Some time after Git 1.7.5.4, Git started fataling if // it can not read $HOME. For many users, $HOME points at /root (this // seems to be a default result of Apache setup). Instead, explicitly // point $HOME at a readable, empty directory so that Git looks for the // config file it's after, fails to locate it, and moves on. This is // really silly, but seems like the least damaging approach to // mitigating the issue. $root = dirname(phutil_get_library_root('phabricator')); $env['HOME'] = $root.'/support/empty/'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: // NOTE: This overrides certain configuration, extensions, and settings // which make Mercurial commands do random unusual things. $env['HGPLAIN'] = 1; break; default: throw new Exception('Unrecognized version control system.'); } return $env; } private function getLocalCommandEnvironment() { return $this->getCommonCommandEnvironment(); } private function getRemoteCommandEnvironment() { $env = $this->getCommonCommandEnvironment(); if ($this->shouldUseSSH()) { // NOTE: This is read by `bin/ssh-connect`, and tells it which credentials // to use. $env['PHABRICATOR_CREDENTIAL'] = $this->getCredentialPHID(); switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // Force SVN to use `bin/ssh-connect`. $env['SVN_SSH'] = $this->getSSHWrapper(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: // Force Git to use `bin/ssh-connect`. $env['GIT_SSH'] = $this->getSSHWrapper(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: // We force Mercurial through `bin/ssh-connect` too, but it uses a // command-line flag instead of an environmental variable. break; default: throw new Exception('Unrecognized version control system.'); } } return $env; } private function formatRemoteCommand(array $args) { $pattern = $args[0]; $args = array_slice($args, 1); switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: if ($this->shouldUseHTTP() || $this->shouldUseSVNProtocol()) { $flags = array(); $flag_args = array(); $flags[] = '--non-interactive'; $flags[] = '--no-auth-cache'; if ($this->shouldUseHTTP()) { $flags[] = '--trust-server-cert'; } $credential_phid = $this->getCredentialPHID(); if ($credential_phid) { $key = PassphrasePasswordKey::loadFromPHID( $credential_phid, PhabricatorUser::getOmnipotentUser()); $flags[] = '--username %P'; $flags[] = '--password %P'; $flag_args[] = $key->getUsernameEnvelope(); $flag_args[] = $key->getPasswordEnvelope(); } $flags = implode(' ', $flags); $pattern = "svn {$flags} {$pattern}"; $args = array_mergev(array($flag_args, $args)); } else { $pattern = "svn --non-interactive {$pattern}"; } break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $pattern = "git {$pattern}"; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: if ($this->shouldUseSSH()) { $pattern = "hg --config ui.ssh=%s {$pattern}"; array_unshift( $args, $this->getSSHWrapper()); } else { $pattern = "hg {$pattern}"; } break; default: throw new Exception('Unrecognized version control system.'); } array_unshift($args, $pattern); return $args; } private function formatLocalCommand(array $args) { $pattern = $args[0]; $args = array_slice($args, 1); switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $pattern = "svn --non-interactive {$pattern}"; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $pattern = "git {$pattern}"; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $pattern = "hg {$pattern}"; break; default: throw new Exception('Unrecognized version control system.'); } array_unshift($args, $pattern); return $args; } /** * Sanitize output of an `hg` command invoked with the `--debug` flag to make * it usable. * * @param string Output from `hg --debug ...` * @return string Usable output. */ public static function filterMercurialDebugOutput($stdout) { // When hg commands are run with `--debug` and some config file isn't // trusted, Mercurial prints out a warning to stdout, twice, after Feb 2011. // // http://selenic.com/pipermail/mercurial-devel/2011-February/028541.html $lines = preg_split('/(?<=\n)/', $stdout); $regex = '/ignoring untrusted configuration option .*\n$/'; foreach ($lines as $key => $line) { $lines[$key] = preg_replace($regex, '', $line); } return implode('', $lines); } public function getURI() { return '/diffusion/'.$this->getCallsign().'/'; } public function getNormalizedPath() { $uri = (string)$this->getCloneURIObject(); switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $normalized_uri = new PhabricatorRepositoryURINormalizer( PhabricatorRepositoryURINormalizer::TYPE_GIT, $uri); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $normalized_uri = new PhabricatorRepositoryURINormalizer( PhabricatorRepositoryURINormalizer::TYPE_SVN, $uri); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $normalized_uri = new PhabricatorRepositoryURINormalizer( PhabricatorRepositoryURINormalizer::TYPE_MERCURIAL, $uri); break; default: throw new Exception('Unrecognized version control system.'); } return $normalized_uri->getNormalizedPath(); } public function isTracked() { return $this->getDetail('tracking-enabled', false); } public function getDefaultBranch() { $default = $this->getDetail('default-branch'); if (strlen($default)) { return $default; } $default_branches = array( PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'master', PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => 'default', ); return idx($default_branches, $this->getVersionControlSystem()); } public function getDefaultArcanistBranch() { return coalesce($this->getDefaultBranch(), 'svn'); } private function isBranchInFilter($branch, $filter_key) { $vcs = $this->getVersionControlSystem(); $is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT); $use_filter = ($is_git); if ($use_filter) { $filter = $this->getDetail($filter_key, array()); if ($filter && empty($filter[$branch])) { return false; } } // By default, all branches pass. return true; } public function shouldTrackBranch($branch) { return $this->isBranchInFilter($branch, 'branch-filter'); } public function shouldAutocloseBranch($branch) { if ($this->isImporting()) { return false; } if ($this->getDetail('disable-autoclose', false)) { return false; } if (!$this->shouldTrackBranch($branch)) { return false; } return $this->isBranchInFilter($branch, 'close-commits-filter'); } public function shouldAutocloseCommit( PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $data) { if ($this->getDetail('disable-autoclose', false)) { return false; } switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: return true; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: return true; default: throw new Exception('Unrecognized version control system.'); } $closeable_flag = PhabricatorRepositoryCommit::IMPORTED_CLOSEABLE; if ($commit->isPartiallyImported($closeable_flag)) { return true; } // TODO: Remove this eventually, it's no longer written to by the import // pipeline (however, old tasks may still be queued which don't reflect // the new data format). $branches = $data->getCommitDetail('seenOnBranches', array()); foreach ($branches as $branch) { if ($this->shouldAutocloseBranch($branch)) { return true; } } return false; } public function formatCommitName($commit_identifier) { $vcs = $this->getVersionControlSystem(); $type_git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; $type_hg = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL; $is_git = ($vcs == $type_git); $is_hg = ($vcs == $type_hg); if ($is_git || $is_hg) { $short_identifier = substr($commit_identifier, 0, 12); } else { $short_identifier = $commit_identifier; } return 'r'.$this->getCallsign().$short_identifier; } public function isImporting() { return (bool)$this->getDetail('importing', false); } /* -( Repository URI Management )------------------------------------------ */ /** * Get the remote URI for this repository. * * @return string * @task uri */ public function getRemoteURI() { return (string)$this->getRemoteURIObject(); } /** * Get the remote URI for this repository, including credentials if they're * used by this repository. * * @return PhutilOpaqueEnvelope URI, possibly including credentials. * @task uri */ public function getRemoteURIEnvelope() { $uri = $this->getRemoteURIObject(); $remote_protocol = $this->getRemoteProtocol(); if ($remote_protocol == 'http' || $remote_protocol == 'https') { // For SVN, we use `--username` and `--password` flags separately, so // don't add any credentials here. if (!$this->isSVN()) { $credential_phid = $this->getCredentialPHID(); if ($credential_phid) { $key = PassphrasePasswordKey::loadFromPHID( $credential_phid, PhabricatorUser::getOmnipotentUser()); $uri->setUser($key->getUsernameEnvelope()->openEnvelope()); $uri->setPass($key->getPasswordEnvelope()->openEnvelope()); } } } return new PhutilOpaqueEnvelope((string)$uri); } /** * Get the clone (or checkout) URI for this repository, without authentication * information. * * @return string Repository URI. * @task uri */ public function getPublicCloneURI() { $uri = $this->getCloneURIObject(); // Make sure we don't leak anything if this repo is using HTTP Basic Auth // with the credentials in the URI or something zany like that. // If repository is not accessed over SSH we remove both username and // password. if (!$this->isHosted()) { if (!$this->shouldUseSSH()) { $uri->setUser(null); // This might be a Git URI or a normal URI. If it's Git, there's no // password support. if ($uri instanceof PhutilURI) { $uri->setPass(null); } } } return (string)$uri; } /** * Get the protocol for the repository's remote. * * @return string Protocol, like "ssh" or "git". * @task uri */ public function getRemoteProtocol() { $uri = $this->getRemoteURIObject(); if ($uri instanceof PhutilGitURI) { return 'ssh'; } else { return $uri->getProtocol(); } } /** * Get a parsed object representation of the repository's remote URI. This * may be a normal URI (returned as a @{class@libphutil:PhutilURI}) or a git * URI (returned as a @{class@libphutil:PhutilGitURI}). * * @return wild A @{class@libphutil:PhutilURI} or * @{class@libphutil:PhutilGitURI}. * @task uri */ public function getRemoteURIObject() { $raw_uri = $this->getDetail('remote-uri'); if (!$raw_uri) { return new PhutilURI(''); } if (!strncmp($raw_uri, '/', 1)) { return new PhutilURI('file://'.$raw_uri); } $uri = new PhutilURI($raw_uri); if ($uri->getProtocol()) { return $uri; } $uri = new PhutilGitURI($raw_uri); if ($uri->getDomain()) { return $uri; } throw new Exception("Remote URI '{$raw_uri}' could not be parsed!"); } /** * Get the "best" clone/checkout URI for this repository, on any protocol. */ public function getCloneURIObject() { if (!$this->isHosted()) { if ($this->isSVN()) { // Make sure we pick up the "Import Only" path for Subversion, so // the user clones the repository starting at the correct path, not // from the root. $base_uri = $this->getSubversionBaseURI(); $base_uri = new PhutilURI($base_uri); $path = $base_uri->getPath(); if (!$path) { $path = '/'; } // If the trailing "@" is not required to escape the URI, strip it for // readability. if (!preg_match('/@.*@/', $path)) { $path = rtrim($path, '@'); } $base_uri->setPath($path); return $base_uri; } else { return $this->getRemoteURIObject(); } } // Choose the best URI: pick a read/write URI over a URI which is not // read/write, and SSH over HTTP. $serve_ssh = $this->getServeOverSSH(); $serve_http = $this->getServeOverHTTP(); if ($serve_ssh === self::SERVE_READWRITE) { return $this->getSSHCloneURIObject(); } else if ($serve_http === self::SERVE_READWRITE) { return $this->getHTTPCloneURIObject(); } else if ($serve_ssh !== self::SERVE_OFF) { return $this->getSSHCloneURIObject(); } else if ($serve_http !== self::SERVE_OFF) { return $this->getHTTPCloneURIObject(); } else { return null; } } /** * Get the repository's SSH clone/checkout URI, if one exists. */ public function getSSHCloneURIObject() { if (!$this->isHosted()) { if ($this->shouldUseSSH()) { return $this->getRemoteURIObject(); } else { return null; } } $serve_ssh = $this->getServeOverSSH(); if ($serve_ssh === self::SERVE_OFF) { return null; } $uri = new PhutilURI(PhabricatorEnv::getProductionURI($this->getURI())); if ($this->isSVN()) { $uri->setProtocol('svn+ssh'); } else { $uri->setProtocol('ssh'); } if ($this->isGit()) { $uri->setPath($uri->getPath().$this->getCloneName().'.git'); } else if ($this->isHg()) { $uri->setPath($uri->getPath().$this->getCloneName().'/'); } $ssh_user = PhabricatorEnv::getEnvConfig('diffusion.ssh-user'); if ($ssh_user) { $uri->setUser($ssh_user); } $uri->setPort(PhabricatorEnv::getEnvConfig('diffusion.ssh-port')); return $uri; } /** * Get the repository's HTTP clone/checkout URI, if one exists. */ public function getHTTPCloneURIObject() { if (!$this->isHosted()) { if ($this->shouldUseHTTP()) { return $this->getRemoteURIObject(); } else { return null; } } $serve_http = $this->getServeOverHTTP(); if ($serve_http === self::SERVE_OFF) { return null; } $uri = PhabricatorEnv::getProductionURI($this->getURI()); $uri = new PhutilURI($uri); if ($this->isGit()) { $uri->setPath($uri->getPath().$this->getCloneName().'.git'); } else if ($this->isHg()) { $uri->setPath($uri->getPath().$this->getCloneName().'/'); } return $uri; } /** * Determine if we should connect to the remote using SSH flags and * credentials. * * @return bool True to use the SSH protocol. * @task uri */ private function shouldUseSSH() { if ($this->isHosted()) { return false; } $protocol = $this->getRemoteProtocol(); if ($this->isSSHProtocol($protocol)) { return true; } return false; } /** * Determine if we should connect to the remote using HTTP flags and * credentials. * * @return bool True to use the HTTP protocol. * @task uri */ private function shouldUseHTTP() { if ($this->isHosted()) { return false; } $protocol = $this->getRemoteProtocol(); return ($protocol == 'http' || $protocol == 'https'); } /** * Determine if we should connect to the remote using SVN flags and * credentials. * * @return bool True to use the SVN protocol. * @task uri */ private function shouldUseSVNProtocol() { if ($this->isHosted()) { return false; } $protocol = $this->getRemoteProtocol(); return ($protocol == 'svn'); } /** * Determine if a protocol is SSH or SSH-like. * * @param string A protocol string, like "http" or "ssh". * @return bool True if the protocol is SSH-like. * @task uri */ private function isSSHProtocol($protocol) { return ($protocol == 'ssh' || $protocol == 'svn+ssh'); } public function delete() { $this->openTransaction(); $paths = id(new PhabricatorOwnersPath()) ->loadAllWhere('repositoryPHID = %s', $this->getPHID()); foreach ($paths as $path) { $path->delete(); } $projects = id(new PhabricatorRepositoryArcanistProject()) ->loadAllWhere('repositoryID = %d', $this->getID()); foreach ($projects as $project) { // note each project deletes its PhabricatorRepositorySymbols $project->delete(); } $commits = id(new PhabricatorRepositoryCommit()) ->loadAllWhere('repositoryID = %d', $this->getID()); foreach ($commits as $commit) { // note PhabricatorRepositoryAuditRequests and // PhabricatorRepositoryCommitData are deleted here too. $commit->delete(); } $mirrors = id(new PhabricatorRepositoryMirror()) ->loadAllWhere('repositoryPHID = %s', $this->getPHID()); foreach ($mirrors as $mirror) { $mirror->delete(); } $ref_cursors = id(new PhabricatorRepositoryRefCursor()) ->loadAllWhere('repositoryPHID = %s', $this->getPHID()); foreach ($ref_cursors as $cursor) { $cursor->delete(); } $conn_w = $this->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE repositoryID = %d', self::TABLE_FILESYSTEM, $this->getID()); queryfx( $conn_w, 'DELETE FROM %T WHERE repositoryID = %d', self::TABLE_PATHCHANGE, $this->getID()); queryfx( $conn_w, 'DELETE FROM %T WHERE repositoryID = %d', self::TABLE_SUMMARY, $this->getID()); $result = parent::delete(); $this->saveTransaction(); return $result; } public function isGit() { $vcs = $this->getVersionControlSystem(); return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT); } public function isSVN() { $vcs = $this->getVersionControlSystem(); return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_SVN); } public function isHg() { $vcs = $this->getVersionControlSystem(); return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL); } public function isHosted() { return (bool)$this->getDetail('hosting-enabled', false); } public function setHosted($enabled) { return $this->setDetail('hosting-enabled', $enabled); } public function getServeOverHTTP() { if ($this->isSVN()) { return self::SERVE_OFF; } $serve = $this->getDetail('serve-over-http', self::SERVE_OFF); return $this->normalizeServeConfigSetting($serve); } public function setServeOverHTTP($mode) { return $this->setDetail('serve-over-http', $mode); } public function getServeOverSSH() { $serve = $this->getDetail('serve-over-ssh', self::SERVE_OFF); return $this->normalizeServeConfigSetting($serve); } public function setServeOverSSH($mode) { return $this->setDetail('serve-over-ssh', $mode); } public static function getProtocolAvailabilityName($constant) { switch ($constant) { case self::SERVE_OFF: return pht('Off'); case self::SERVE_READONLY: return pht('Read Only'); case self::SERVE_READWRITE: return pht('Read/Write'); default: return pht('Unknown'); } } private function normalizeServeConfigSetting($value) { switch ($value) { case self::SERVE_OFF: case self::SERVE_READONLY: return $value; case self::SERVE_READWRITE: if ($this->isHosted()) { return self::SERVE_READWRITE; } else { return self::SERVE_READONLY; } default: return self::SERVE_OFF; } } /** * Raise more useful errors when there are basic filesystem problems. */ private function assertLocalExists() { if (!$this->usesLocalWorkingCopy()) { return; } $local = $this->getLocalPath(); Filesystem::assertExists($local); Filesystem::assertIsDirectory($local); Filesystem::assertReadable($local); } /** * Determine if the working copy is bare or not. In Git, this corresponds * to `--bare`. In Mercurial, `--noupdate`. */ public function isWorkingCopyBare() { switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: return false; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $local = $this->getLocalPath(); if (Filesystem::pathExists($local.'/.git')) { return false; } else { return true; } } } public function usesLocalWorkingCopy() { switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: return $this->isHosted(); case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: return true; } } public function getHookDirectories() { $directories = array(); if (!$this->isHosted()) { return $directories; } $root = $this->getLocalPath(); switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: if ($this->isWorkingCopyBare()) { $directories[] = $root.'/hooks/pre-receive-phabricator.d/'; } else { $directories[] = $root.'/.git/hooks/pre-receive-phabricator.d/'; } break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $directories[] = $root.'/hooks/pre-commit-phabricator.d/'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: // NOTE: We don't support custom Mercurial hooks for now because they're // messy and we can't easily just drop a `hooks.d/` directory next to // the hooks. break; } return $directories; } public function canDestroyWorkingCopy() { if ($this->isHosted()) { // Never destroy hosted working copies. return false; } $default_path = PhabricatorEnv::getEnvConfig( 'repository.default-local-path'); return Filesystem::isDescendant($this->getLocalPath(), $default_path); } public function canUsePathTree() { return !$this->isSVN(); } public function canMirror() { if ($this->isGit() || $this->isHg()) { return true; } return false; } public function canAllowDangerousChanges() { if (!$this->isHosted()) { return false; } if ($this->isGit() || $this->isHg()) { return true; } return false; } public function shouldAllowDangerousChanges() { return (bool)$this->getDetail('allow-dangerous-changes'); } public function writeStatusMessage( $status_type, $status_code, array $parameters = array()) { $table = new PhabricatorRepositoryStatusMessage(); $conn_w = $table->establishConnection('w'); $table_name = $table->getTableName(); if ($status_code === null) { queryfx( $conn_w, 'DELETE FROM %T WHERE repositoryID = %d AND statusType = %s', $table_name, $this->getID(), $status_type); } else { queryfx( $conn_w, 'INSERT INTO %T (repositoryID, statusType, statusCode, parameters, epoch) VALUES (%d, %s, %s, %s, %d) ON DUPLICATE KEY UPDATE statusCode = VALUES(statusCode), parameters = VALUES(parameters), epoch = VALUES(epoch)', $table_name, $this->getID(), $status_type, $status_code, json_encode($parameters), time()); } return $this; } + public static function getRemoteURIProtocol($raw_uri) { + $uri = new PhutilURI($raw_uri); + if ($uri->getProtocol()) { + return strtolower($uri->getProtocol()); + } + + $git_uri = new PhutilGitURI($raw_uri); + if (strlen($git_uri->getDomain()) && strlen($git_uri->getPath())) { + return 'ssh'; + } + + return null; + } + + public static function assertValidRemoteURI($uri) { + if (trim($uri) != $uri) { + throw new Exception( + pht( + 'The remote URI has leading or trailing whitespace.')); + } + + $protocol = self::getRemoteURIProtocol($uri); + + // Catch confusion between Git/SCP-style URIs and normal URIs. See T3619 + // for discussion. This is usually a user adding "ssh://" to an implicit + // SSH Git URI. + if ($protocol == 'ssh') { + if (preg_match('(^[^:@]+://[^/:]+:[^\d])', $uri)) { + throw new Exception( + pht( + "The remote URI is not formatted correctly. Remote URIs ". + "with an explicit protocol should be in the form ". + "'proto://domain/path', not 'proto://domain:/path'. ". + "The ':/path' syntax is only valid in SCP-style URIs.")); + } + } + + switch ($protocol) { + case 'ssh': + case 'http': + case 'https': + case 'git': + case 'svn': + case 'svn+ssh': + break; + default: + // NOTE: We're explicitly rejecting 'file://' because it can be + // used to clone from the working copy of another repository on disk + // that you don't normally have permission to access. + + throw new Exception( + pht( + "The URI protocol is unrecognized. It should begin ". + "'ssh://', 'http://', 'https://', 'git://', 'svn://', ". + "'svn+ssh://', or be in the form 'git@domain.com:path'.")); + } + + return true; + } + + + /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, DiffusionCapabilityPush::CAPABILITY, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); case DiffusionCapabilityPush::CAPABILITY: return $this->getPushPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { return false; } public function describeAutomaticCapability($capability) { return null; } /* -( PhabricatorMarkupInterface )----------------------------------------- */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digestForIndex($this->getMarkupText($field)); return "repo:{$hash}"; } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newMarkupEngine(array()); } public function getMarkupText($field) { return $this->getDetail('description'); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { require_celerity_resource('phabricator-remarkup-css'); return phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $output); } public function shouldUseMarkupCache($field) { return true; } /* -( PhabricatorDestructableInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } }