diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 9965c7b12e..f7ad64232e 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -222,6 +222,14 @@ phutil_register_library_map(array( 'PhabricatorPeopleProfileController' => 'applications/people/controller/profile', 'PhabricatorRemarkupRuleDifferential' => 'infrastructure/markup/remarkup/markuprule/differential', 'PhabricatorRemarkupRuleManiphest' => 'infrastructure/markup/remarkup/markuprule/maniphest', + 'PhabricatorRepository' => 'applications/repository/storage/repository', + 'PhabricatorRepositoryController' => 'applications/repository/controller/base', + 'PhabricatorRepositoryCreateController' => 'applications/repository/controller/create', + 'PhabricatorRepositoryDAO' => 'applications/repository/storage/base', + 'PhabricatorRepositoryEditController' => 'applications/repository/controller/edit', + 'PhabricatorRepositoryGitHubNotification' => 'applications/repository/storage/githubnotification', + 'PhabricatorRepositoryGitHubPostReceiveController' => 'applications/repository/controller/github-post-receive', + 'PhabricatorRepositoryListController' => 'applications/repository/controller/list', 'PhabricatorStandardPageView' => 'view/page/standard', 'PhabricatorTypeaheadCommonDatasourceController' => 'applications/typeahead/controller/common', 'PhabricatorTypeaheadDatasourceController' => 'applications/typeahead/controller/base', @@ -421,6 +429,14 @@ phutil_register_library_map(array( 'PhabricatorPeopleProfileController' => 'PhabricatorPeopleController', 'PhabricatorRemarkupRuleDifferential' => 'PhutilRemarkupRule', 'PhabricatorRemarkupRuleManiphest' => 'PhutilRemarkupRule', + 'PhabricatorRepository' => 'PhabricatorRepositoryDAO', + 'PhabricatorRepositoryController' => 'PhabricatorController', + 'PhabricatorRepositoryCreateController' => 'PhabricatorController', + 'PhabricatorRepositoryDAO' => 'PhabricatorLiskDAO', + 'PhabricatorRepositoryEditController' => 'PhabricatorController', + 'PhabricatorRepositoryGitHubNotification' => 'PhabricatorRepositoryDAO', + 'PhabricatorRepositoryGitHubPostReceiveController' => 'PhabricatorRepositoryController', + 'PhabricatorRepositoryListController' => 'PhabricatorController', 'PhabricatorStandardPageView' => 'AphrontPageView', 'PhabricatorTypeaheadCommonDatasourceController' => 'PhabricatorTypeaheadDatasourceController', 'PhabricatorTypeaheadDatasourceController' => 'PhabricatorController', diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index 1d9a30a173..8db208d90e 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -28,12 +28,6 @@ class AphrontDefaultApplicationConfiguration public function getURIMap() { return array( - '/repository/' => array( - '$' => 'RepositoryListController', - 'new/$' => 'RepositoryEditController', - 'edit/(?P\d+)/$' => 'RepositoryEditController', - 'delete/(?P\d+)/$' => 'RepositoryDeleteController', - ), '/' => array( '$' => 'PhabricatorDirectoryMainController', ), @@ -152,6 +146,15 @@ class AphrontDefaultApplicationConfiguration ), '/T(?P\d+)$' => 'ManiphestTaskDetailController', + + '/github-post-receive/(?P\d+)/(?P[^/]+)/$' + => 'PhabricatorRepositoryGitHubPostReceiveController', + '/repository/' => array( + '$' => 'PhabricatorRepositoryListController', + 'create/$' => 'PhabricatorRepositoryCreateController', + 'edit/(?P\d+)/$' => 'PhabricatorRepositoryEditController', + 'delete/(?P\d+)/$' => 'PhabricatorRepositoryDeleteController', + ), ); } diff --git a/src/applications/repository/controller/base/PhabricatorRepositoryController.php b/src/applications/repository/controller/base/PhabricatorRepositoryController.php new file mode 100644 index 0000000000..bac6234ae2 --- /dev/null +++ b/src/applications/repository/controller/base/PhabricatorRepositoryController.php @@ -0,0 +1,34 @@ +buildStandardPageView(); + + $page->setApplicationName('Repositories'); + $page->setBaseURI('/repository/'); + $page->setTitle(idx($data, 'title')); + $page->setGlyph("rX"); + $page->appendChild($view); + + $response = new AphrontWebpageResponse(); + return $response->setContent($page->render()); + } + +} diff --git a/src/applications/repository/controller/base/__init__.php b/src/applications/repository/controller/base/__init__.php new file mode 100644 index 0000000000..4f185531d2 --- /dev/null +++ b/src/applications/repository/controller/base/__init__.php @@ -0,0 +1,15 @@ +getRequest(); + $user = $request->getUser(); + + $e_name = true; + $e_callsign = true; + + $repository = new PhabricatorRepository(); + + $type_map = array( + 'svn' => 'Subversion', + 'git' => 'Git', + ); + $errors = array(); + + if ($request->isFormPost()) { + + $repository->setName($request->getStr('name')); + $repository->setCallsign($request->getStr('callsign')); + $repository->setVersionControlSystem($request->getStr('type')); + + if (!strlen($repository->getName())) { + $e_name = 'Required'; + $errors[] = 'Repository name is required.'; + } else { + $e_name = null; + } + + if (!strlen($repository->getCallsign())) { + $e_callsign = 'Required'; + $errors[] = 'Callsign is required.'; + } else if (!preg_match('/^[A-Z]+$/', $repository->getCallsign())) { + $e_callsign = 'Invalid'; + $errors[] = 'Callsign must be ALL UPPERCASE LETTERS.'; + } else { + $e_callsign = null; + } + + if (empty($type_map[$repository->getVersionControlSystem()])) { + $errors[] = 'Invalid version control system.'; + } + + if (!$errors) { + try { + $repository->save(); + + return id(new AphrontRedirectResponse()) + ->setURI('/repository/edit/'.$repository->getID().'/'); + + } catch (PhabricatorQueryDuplicateKeyException $ex) { + if ($ex->getDuplicateKey() == 'callsign') { + $e_callsign = 'Duplicate'; + $errors[] = 'Callsign must be unique. Another repository already '. + 'uses that callsign.'; + } else { + throw $ex; + } + } + } + } + + $error_view = null; + if ($errors) { + $error_view = new AphrontErrorView(); + $error_view->setErrors($errors); + $error_view->setTitle('Form Errors'); + } + + + $form = new AphrontFormView(); + $form + ->setUser($user) + ->setAction('/repository/create/') + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Name') + ->setName('name') + ->setValue($repository->getName()) + ->setError($e_name) + ->setCaption('Human-readable repository name.')) + ->appendChild( + '

Select a "Callsign" — a '. + 'short, uppercase string to identify revisions in this repository. If '. + 'you choose "EX", revisions in this repository will be identified '. + 'with the prefix "rEX".

') + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Callsign') + ->setName('callsign') + ->setValue($repository->getCallsign()) + ->setError($e_callsign) + ->setCaption( + 'Short, UPPERCASE identifier. Once set, it can not be changed.')) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel('Type') + ->setName('type') + ->setOptions($type_map) + ->setValue($repository->getVersionControlSystem())) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Create Repository') + ->addCancelButton('/repository/')); + + $panel = new AphrontPanelView(); + $panel->setHeader('Create Repository'); + $panel->appendChild($form); + $panel->setWidth(AphrontPanelView::WIDTH_FORM); + + return $this->buildStandardPageResponse( + array( + $error_view, + $panel, + ), + array( + 'title' => 'Create Repository', + )); + } + +} diff --git a/src/applications/repository/controller/create/__init__.php b/src/applications/repository/controller/create/__init__.php new file mode 100644 index 0000000000..b30ea67a72 --- /dev/null +++ b/src/applications/repository/controller/create/__init__.php @@ -0,0 +1,20 @@ +id = idx($data, 'id'); + } + + public function processRequest() { + + $request = $this->getRequest(); + $user = $request->getUser(); + + $repository = id(new PhabricatorRepository())->load($this->id); + if (!$repository) { + return new Aphront404Response(); + } + + $vcs = $repository->getVersionControlSystem(); + if ($vcs == DifferentialRevisionControlSystem::GIT) { + if (!$repository->getDetail('github-token')) { + $token = substr(base64_encode(Filesystem::readRandomBytes(8)), 0, 8); + $repository->setDetail('github-token', $token); + $repository->save(); + } + } + + $e_name = true; + + $type_map = array( + 'svn' => 'Subversion', + 'git' => 'Git', + ); + $errors = array(); + + if ($request->isFormPost()) { + $repository->setName($request->getStr('name')); + + if (!strlen($repository->getName())) { + $e_name = 'Required'; + $errors[] = 'Repository name is required.'; + } else { + $e_name = null; + } + + if (!$errors) { + $repository->save(); + return id(new AphrontRedirectResponse()) + ->setURI('/repository/'); + } + + } + + $error_view = null; + if ($errors) { + $error_view = new AphrontErrorView(); + $error_view->setErrors($errors); + $error_view->setTitle('Form Errors'); + } + + + $form = new AphrontFormView(); + $form + ->setUser($user) + ->setAction('/repository/edit/'.$repository->getID().'/') + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Name') + ->setName('name') + ->setValue($repository->getName()) + ->setError($e_name) + ->setCaption('Human-readable repository name.')) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel('Callsign') + ->setName('callsign') + ->setValue($repository->getCallsign())) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel('Type') + ->setName('type') + ->setValue($repository->getVersionControlSystem())); + + + $form + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Save') + ->addCancelButton('/repository/')); + + $panel = new AphrontPanelView(); + $panel->setHeader('Edit Repository'); + $panel->appendChild($form); + $panel->setWidth(AphrontPanelView::WIDTH_FORM); + + $phid = $repository->getID(); + $token = $repository->getDetail('github-token'); + $path = '/github-post-receive/'.$phid.'/'.$token.'/'; + $post_uri = PhabricatorEnv::getURI($path); + + $gitform = new AphrontFormView(); + $gitform + ->setUser($user) + ->setAction('/repository/edit/'.$repository->getID().'/') + ->appendChild( + '

You can configure GitHub to '. + 'notify Phabricator after changes are pushed. Log into GitHub, go '. + 'to "Admin" → "Service Hooks" → "Post-Receive URLs", and '. + 'add this URL to the list. Obviously, this will only work if your '. + 'Phabricator installation is accessible from the internet.

') + ->appendChild( + '

If things are working '. + 'properly, push notifications should appear below once you make some '. + 'commits.

') + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('URL') + ->setCaption('Set this as a GitHub "Post-Receive URL".') + ->setValue($post_uri)) + ->appendChild('

') + ->appendChild('

Recent Commit Notifications

'); + + $notifications = id(new PhabricatorRepositoryGitHubNotification()) + ->loadAllWhere( + 'repositoryPHID = %s ORDER BY id DESC limit 10', + $repository->getPHID()); + + $rows = array(); + foreach ($notifications as $notification) { + $rows[] = array( + phutil_escape_html($notification->getRemoteAddress()), + phabricator_format_timestamp($notification->getDateCreated()), + $notification->getPayload() + ? phutil_escape_html(substr($notification->getPayload(), 0, 32).'...') + : 'Empty', + ); + } + + $notification_table = new AphrontTableView($rows); + $notification_table->setHeaders( + array( + 'Remote Address', + 'Received', + 'Payload', + )); + $notification_table->setColumnClasses( + array( + null, + null, + 'wide', + )); + $notification_table->setNoDataString( + 'Phabricator has not yet received any commit notifications for this '. + 'repository from GitHub.'); + + $gitform->appendChild($notification_table); + + $github = new AphrontPanelView(); + $github->setHeader('GitHub Integration'); + $github->appendChild($gitform); + $github->setWidth(AphrontPanelView::WIDTH_FORM); + + return $this->buildStandardPageResponse( + array( + $error_view, + $panel, + $github, + ), + array( + 'title' => 'Edit Repository', + )); + } + +} diff --git a/src/applications/repository/controller/edit/__init__.php b/src/applications/repository/controller/edit/__init__.php new file mode 100644 index 0000000000..96c6efbc23 --- /dev/null +++ b/src/applications/repository/controller/edit/__init__.php @@ -0,0 +1,29 @@ +id = $data['id']; + $this->token = $data['token']; + } + + public function processRequest() { + $request = $this->getRequest(); + + $repo = id(new PhabricatorRepository())->load($this->id); + if (!$repo) { + return new Aphront404Response(); + } + + if ($repo->getDetail('github-token') != $this->token) { + return new Aphront400Response(); + } + + if (!$request->isHTTPPost()) { + return id(new AphrontFileResponse()) + ->setMimeType('text/plain') + ->setContent( + "Put this URL in your GitHub configuration. Accessing it directly ". + "won't do anything!"); + } + + $notification = new PhabricatorRepositoryGitHubNotification(); + $notification->setRepositoryPHID($repo->getPHID()); + $notification->setRemoteAddress($_SERVER['REMOTE_ADDR']); + $notification->setPayload($request->getStr('payload', '')); + $notification->save(); + + return id(new AphrontFileResponse()) + ->setMimeType('text/plain') + ->setContent('OK'); + } + +} diff --git a/src/applications/repository/controller/github-post-receive/__init__.php b/src/applications/repository/controller/github-post-receive/__init__.php new file mode 100644 index 0000000000..55d0ead44b --- /dev/null +++ b/src/applications/repository/controller/github-post-receive/__init__.php @@ -0,0 +1,19 @@ +loadAll(); + + $rows = array(); + foreach ($repos as $repo) { + $rows[] = array( + phutil_escape_html($repo->getCallsign()), + phutil_escape_html($repo->getName()), + $repo->getVersionControlSystem(), + phutil_render_tag( + 'a', + array( + 'class' => 'button small grey', + 'href' => '/repository/edit/'.$repo->getID().'/', + ), + 'Edit'), + phutil_render_tag( + 'a', + array( + 'class' => 'button small grey', + 'href' => '/repository/delete/'.$repo->getID().'/', + ), + 'Delete'), + ); + } + + $table = new AphrontTableView($rows); + $table->setHeaders( + array( + 'Callsign', + 'Repository', + 'Type', + '', + '' + )); + $table->setColumnClasses( + array( + null, + 'wide', + null, + 'action', + 'action', + )); + + $panel = new AphrontPanelView(); + $panel->setHeader('Repositories'); + $panel->setCreateButton('Create New Repository', '/repository/create/'); + $panel->appendChild($table); + + return $this->buildStandardPageResponse( + $panel, + array( + 'title' => 'Repository List', + )); + } + +} diff --git a/src/applications/repository/controller/list/__init__.php b/src/applications/repository/controller/list/__init__.php new file mode 100644 index 0000000000..918dbcc426 --- /dev/null +++ b/src/applications/repository/controller/list/__init__.php @@ -0,0 +1,18 @@ + true, + self::CONFIG_SERIALIZATION => array( + 'details' => self::SERIALIZATION_JSON, + ), + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID('REPO'); + } + + public function getDetail($key, $default = null) { + return idx($this->details, $key, $default); + } + + public function setDetail($key, $value) { + $this->details[$key] = $value; + return $this; + } + +} diff --git a/src/applications/repository/storage/repository/__init__.php b/src/applications/repository/storage/repository/__init__.php new file mode 100644 index 0000000000..1fea420fa4 --- /dev/null +++ b/src/applications/repository/storage/repository/__init__.php @@ -0,0 +1,15 @@ +