diff --git a/src/applications/diffusion/query/branch/git/DiffusionGitBranchQuery.php b/src/applications/diffusion/query/branch/git/DiffusionGitBranchQuery.php index a420cbd8b..dc58784eb 100644 --- a/src/applications/diffusion/query/branch/git/DiffusionGitBranchQuery.php +++ b/src/applications/diffusion/query/branch/git/DiffusionGitBranchQuery.php @@ -1,110 +1,114 @@ getRequest(); $repository = $drequest->getRepository(); $local_path = $repository->getDetail('local-path'); list($stdout) = $repository->execxLocalCommand( 'branch -r --verbose --no-abbrev'); $branch_list = self::parseGitRemoteBranchOutput( $stdout, $only_this_remote = DiffusionBranchInformation::DEFAULT_GIT_REMOTE); $branches = array(); foreach ($branch_list as $name => $head) { + if (!$repository->shouldTrackBranch($name)) { + continue; + } + $branch = new DiffusionBranchInformation(); $branch->setName($name); $branch->setHeadCommitIdentifier($head); $branches[] = $branch; } return $branches; } /** * Parse the output of 'git branch -r --verbose --no-abbrev' or similar into * a map. For instance: * * array( * 'origin/master' => '99a9c082f9a1b68c7264e26b9e552484a5ae5f25', * ); * * If you specify $only_this_remote, branches will be filtered to only those * on the given remote, **and the remote name will be stripped**. For example: * * array( * 'master' => '99a9c082f9a1b68c7264e26b9e552484a5ae5f25', * ); * * @param string stdout of git branch command. * @param string Filter branches to those on a specific remote. * @return map Map of 'branch' or 'remote/branch' to hash at HEAD. */ public static function parseGitRemoteBranchOutput( $stdout, $only_this_remote = null) { $map = array(); $lines = array_filter(explode("\n", $stdout)); foreach ($lines as $line) { $matches = null; if (preg_match('/^ (\S+)\s+-> (\S+)$/', $line, $matches)) { // This is a line like: // // origin/HEAD -> origin/master // // ...which we don't currently do anything interesting with, although // in theory we could use it to automatically choose the default // branch. continue; } if (!preg_match('/^[ *] (\S+)\s+([a-z0-9]{40}) /', $line, $matches)) { throw new Exception("Failed to parse {$line}!"); } $remote_branch = $matches[1]; $branch_head = $matches[2]; if ($only_this_remote) { $matches = null; if (!preg_match('#^([^/]+)/(.*)$#', $remote_branch, $matches)) { throw new Exception( "Failed to parse remote branch '{$remote_branch}'!"); } $remote_name = $matches[1]; $branch_name = $matches[2]; if ($remote_name != $only_this_remote) { continue; } $map[$branch_name] = $branch_head; } else { $map[$remote_branch] = $branch_head; } } return $map; } } diff --git a/src/applications/repository/controller/edit/PhabricatorRepositoryEditController.php b/src/applications/repository/controller/edit/PhabricatorRepositoryEditController.php index 25c95ee1a..b8b34f07c 100644 --- a/src/applications/repository/controller/edit/PhabricatorRepositoryEditController.php +++ b/src/applications/repository/controller/edit/PhabricatorRepositoryEditController.php @@ -1,671 +1,696 @@ id = $data['id']; $this->view = idx($data, 'view'); } public function processRequest() { $request = $this->getRequest(); $repository = id(new PhabricatorRepository())->load($this->id); if (!$repository) { return new Aphront404Response(); } $views = array( 'basic' => 'Basics', 'tracking' => 'Tracking', ); $this->repository = $repository; if (!isset($views[$this->view])) { reset($views); $this->view = key($views); } $nav = new AphrontSideNavView(); foreach ($views as $view => $name) { $nav->addNavItem( phutil_render_tag( 'a', array( 'class' => ($view == $this->view ? 'aphront-side-nav-selected' : null), 'href' => '/repository/edit/'.$repository->getID().'/'.$view.'/', ), phutil_escape_html($name))); } $this->sideNav = $nav; switch ($this->view) { case 'basic': return $this->processBasicRequest(); case 'tracking': return $this->processTrackingRequest(); default: throw new Exception("Unknown view."); } } protected function processBasicRequest() { $request = $this->getRequest(); $user = $request->getUser(); $repository = $this->repository; $repository_id = $repository->getID(); $errors = array(); $e_name = true; if ($request->isFormPost()) { $repository->setName($request->getStr('name')); if (!strlen($repository->getName())) { $e_name = 'Required'; $errors[] = 'Repository name is required.'; } else { $e_name = null; } $repository->setDetail('description', $request->getStr('description')); $repository->setDetail('encoding', $request->getStr('encoding')); if (!$errors) { $repository->save(); return id(new AphrontRedirectResponse()) ->setURI('/repository/edit/'.$repository_id.'/basic/?saved=true'); } } $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); $error_view->setErrors($errors); $error_view->setTitle('Form Errors'); } else if ($request->getStr('saved')) { $error_view = new AphrontErrorView(); $error_view->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $error_view->setTitle('Changes Saved'); $error_view->appendChild( 'Repository changes were saved.'); } $encoding_doc_link = PhabricatorEnv::getDoclink( 'article/User_Guide:_UTF-8_and_Character_Encoding.html'); $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 AphrontFormTextAreaControl()) ->setLabel('Description') ->setName('description') ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) ->setValue($repository->getDetail('description'))) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('Callsign') ->setName('callsign') ->setValue($repository->getCallsign())) ->appendChild('

'. 'If source code in this repository uses a character '. 'encoding other than UTF-8 (for example, ISO-8859-1), '. 'specify it here. You can usually leave this field blank. '. 'See User Guide: '. ''. 'UTF-8 and Character Encoding'. ' for more information.'. '

') ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Encoding') ->setName('encoding') ->setValue($repository->getDetail('encoding'))) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('Type') ->setName('type') ->setValue($repository->getVersionControlSystem())) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('ID') ->setValue($repository->getID())) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('PHID') ->setValue($repository->getPHID())) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue('Save')); $panel = new AphrontPanelView(); $panel->setHeader('Edit Repository'); $panel->appendChild($form); $panel->setWidth(AphrontPanelView::WIDTH_FORM); $nav = $this->sideNav; $nav->appendChild($error_view); $nav->appendChild($panel); return $this->buildStandardPageResponse( $nav, array( 'title' => 'Edit Repository', )); } private function processTrackingRequest() { $request = $this->getRequest(); $user = $request->getUser(); $repository = $this->repository; $repository_id = $repository->getID(); $errors = array(); $e_uri = null; $e_path = null; $is_git = false; $is_svn = false; $is_mercurial = false; $e_ssh_key = null; $e_ssh_keyfile = null; $e_branch = null; switch ($repository->getVersionControlSystem()) { 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_branches = ($is_git || $is_mercurial); $has_local = ($is_git || $is_mercurial); + $has_branch_filter = ($is_git); $has_http_support = $is_svn; if ($request->isFormPost()) { $tracking = ($request->getStr('tracking') == 'enabled' ? true : false); $repository->setDetail('tracking-enabled', $tracking); $repository->setDetail('remote-uri', $request->getStr('uri')); if ($has_local) { $repository->setDetail('local-path', $request->getStr('path')); } + + if ($has_branch_filter) { + $branch_filter = $request->getStrList('branch-filter'); + $branch_filter = array_fill_keys($branch_filter, true); + + $repository->setDetail('branch-filter', $branch_filter); + } + $repository->setDetail( 'pull-frequency', max(1, $request->getInt('frequency'))); if ($has_branches) { $repository->setDetail( 'default-branch', $request->getStr('default-branch')); if ($is_git) { $branch_name = $repository->getDetail('default-branch'); if (strpos($branch_name, '/') !== false) { $e_branch = 'Invalid'; $errors[] = "Your branch name should not specify an explicit ". "remote. For instance, use 'master', not ". "'origin/master'."; } } } $repository->setDetail( 'default-owners-path', $request->getStr( 'default-owners-path', '/')); $repository->setDetail('ssh-login', $request->getStr('ssh-login')); $repository->setDetail('ssh-key', $request->getStr('ssh-key')); $repository->setDetail('ssh-keyfile', $request->getStr('ssh-keyfile')); $repository->setDetail('http-login', $request->getStr('http-login')); $repository->setDetail('http-pass', $request->getStr('http-pass')); if ($repository->getDetail('ssh-key') && $repository->getDetail('ssh-keyfile')) { $errors[] = "Specify only one of 'SSH Private Key' and 'SSH Private Key File', ". "not both."; $e_ssh_key = 'Choose Only One'; $e_ssh_keyfile = 'Choose Only One'; } $repository->setDetail( 'herald-disabled', $request->getInt('herald-disabled', 0)); if ($is_svn) { $repository->setUUID($request->getStr('uuid')); $subpath = ltrim($request->getStr('svn-subpath'), '/'); if ($subpath) { $subpath = rtrim($subpath, '/').'/'; } $repository->setDetail('svn-subpath', $subpath); } $repository->setDetail( 'detail-parser', $request->getStr( 'detail-parser', 'PhabricatorRepositoryDefaultCommitMessageDetailParser')); if ($tracking) { if (!$repository->getDetail('remote-uri')) { $e_uri = 'Required'; $errors[] = "Repository URI is required."; } else if ($is_svn && !preg_match('@/$@', $repository->getDetail('remote-uri'))) { $e_uri = 'Invalid'; $errors[] = 'Subversion Repository Root must end in a slash ("/").'; } else { $e_uri = null; } if ($has_local) { if (!$repository->getDetail('local-path')) { $e_path = 'Required'; $errors[] = "Local path is required."; } else { $e_path = null; } } } if (!$errors) { $repository->save(); return id(new AphrontRedirectResponse()) ->setURI('/repository/edit/'.$repository_id.'/tracking/?saved=true'); } } $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); $error_view->setErrors($errors); $error_view->setTitle('Form Errors'); } else if ($request->getStr('saved')) { $error_view = new AphrontErrorView(); $error_view->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $error_view->setTitle('Changes Saved'); $error_view->appendChild( 'Tracking changes were saved. You may need to restart the daemon '. 'before changes will take effect.'); } else if (!$repository->isTracked()) { $error_view = new AphrontErrorView(); $error_view->setSeverity(AphrontErrorView::SEVERITY_WARNING); $error_view->setTitle('Repository Not Tracked'); $error_view->appendChild( 'Tracking is currently "Disabled" for this repository, so it will '. 'not be imported into Phabricator. You can enable it below.'); } switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $is_git = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $is_svn = true; break; } $doc_href = PhabricatorEnv::getDoclink('article/Diffusion_User_Guide.html'); $user_guide_link = phutil_render_tag( 'a', array( 'href' => $doc_href, ), 'Diffusion User Guide'); $form = new AphrontFormView(); $form ->setUser($user) ->setAction('/repository/edit/'.$repository->getID().'/tracking/') ->appendChild( '

Phabricator can track '. 'repositories, importing commits as they happen and notifying '. 'Differential, Diffusion, Herald, and other services. To enable '. 'tracking for a repository, configure it here and then start (or '. 'restart) the daemons. More information is available in the '. ''.$user_guide_link.'.

'); $form ->appendChild( '

Basics

') ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('Repository Name') ->setValue($repository->getName())) ->appendChild( id(new AphrontFormSelectControl()) ->setName('tracking') ->setLabel('Tracking') ->setOptions(array( 'disabled' => 'Disabled', 'enabled' => 'Enabled', )) ->setValue( $repository->isTracked() ? 'enabled' : 'disabled')) ->appendChild('
'); $form->appendChild( '

Remote URI

'. '
'); $clone_command = null; $fetch_command = null; if ($is_git) { $clone_command = 'git clone'; $fetch_command = 'git fetch'; } else if ($is_mercurial) { $clone_command = 'hg clone'; $fetch_command = 'hg pull'; } $uri_label = 'Repository URI'; if ($has_local) { if ($is_git) { $instructions = 'Enter the URI to clone this repository from. It should look like '. 'git@github.com:example/example.git, '. 'ssh://user@host.com/git/example.git, or '. 'file:///local/path/to/repo'; } else if ($is_mercurial) { $instructions = 'Enter the URI to clone this repository from. It should look '. 'something like ssh://user@host.com/hg/example'; } $form->appendChild( '

'.$instructions.'

'); } else if ($is_svn) { $instructions = 'Enter the Repository Root for this SVN repository. '. 'You can figure this out by running svn info and looking at '. 'the value in the Repository Root field. It should be a URI '. 'and look like http://svn.example.org/svn/ or '. 'svn+ssh://svn.example.com/svnroot/'; $form->appendChild( '

'.$instructions.'

'); $uri_label = 'Repository Root'; } $form ->appendChild( id(new AphrontFormTextControl()) ->setName('uri') ->setLabel($uri_label) ->setID('remote-uri') ->setValue($repository->getDetail('remote-uri')) ->setError($e_uri)); $form->appendChild( '
'. 'If you want to connect to this repository over SSH, enter the '. 'username and private key to use. You can leave these fields blank if '. 'the repository does not use SSH.'. '
'); $form ->appendChild( id(new AphrontFormTextControl()) ->setName('ssh-login') ->setLabel('SSH User') ->setValue($repository->getDetail('ssh-login'))) ->appendChild( id(new AphrontFormTextAreaControl()) ->setName('ssh-key') ->setLabel('SSH Private Key') ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) ->setValue($repository->getDetail('ssh-key')) ->setError($e_ssh_key) ->setCaption('Specify the entire private key, or...')) ->appendChild( id(new AphrontFormTextControl()) ->setName('ssh-keyfile') ->setLabel('SSH Private Key File') ->setValue($repository->getDetail('ssh-keyfile')) ->setError($e_ssh_keyfile) ->setCaption( '...specify a path on disk where the daemon should '. 'look for a private key.')); if ($has_http_support) { $form ->appendChild( '
'. 'If you want to connect to this repository over HTTP Basic Auth, '. 'enter the username and password to use. You can leave these '. 'fields blank if the repository does not use HTTP Basic Auth.'. '
') ->appendChild( id(new AphrontFormTextControl()) ->setName('http-login') ->setLabel('HTTP Basic Login') ->setValue($repository->getDetail('http-login'))) ->appendChild( id(new AphrontFormPasswordControl()) ->setName('http-pass') ->setLabel('HTTP Basic Password') ->setValue($repository->getDetail('http-pass'))); } $form ->appendChild( '
'. 'To test your authentication configuration, save this '. 'form and then run this script:'. ''. 'phabricator/ $ ./scripts/repository/test_connection.php '. phutil_escape_html($repository->getCallsign()). ''. 'This will verify that your configuration is correct and the '. 'daemons can connect to the remote repository and pull changes '. 'from it.'. '
'); $form->appendChild('
'); $form->appendChild( '

Importing Repository Information

'. '
'); if ($has_local) { $form->appendChild( '

Select a path on local disk '. 'which the daemons should '.$clone_command.' the repository '. 'into. This must be readable and writable by the daemons, and '. 'readable by the webserver. The daemons will '.$fetch_command. ' and keep this repository up to date.

'); $form->appendChild( id(new AphrontFormTextControl()) ->setName('path') ->setLabel('Local Path') ->setValue($repository->getDetail('local-path')) ->setError($e_path)); } else if ($is_svn) { $form->appendChild( '

If you only want to parse one '. 'subpath of the repository, specify it here, relative to the '. 'repository root (e.g., trunk/ or projects/wheel/). '. 'If you want to parse multiple subdirectories, create a separate '. 'Phabricator repository for each one.

'); $form->appendChild( id(new AphrontFormTextControl()) ->setName('svn-subpath') ->setLabel('Subpath') ->setValue($repository->getDetail('svn-subpath')) ->setError($e_path)); } + if ($has_branch_filter) { + $branch_filter_str = implode( + ', ', + array_keys($repository->getDetail('branch-filter', array()))); + $form + ->appendChild( + id(new AphrontFormTextControl()) + ->setName('branch-filter') + ->setLabel('Track Only') + ->setValue($branch_filter_str) + ->setCaption( + 'Optional list of branches to track. Other branches will be '. + 'completely ignored. If left empty, all branches are tracked. '. + 'Example: master, release')); + } + $form ->appendChild( id(new AphrontFormTextControl()) ->setName('frequency') ->setLabel('Pull Frequency') ->setValue($repository->getDetail('pull-frequency', 15)) ->setCaption( 'Number of seconds daemon should sleep between requests. Larger '. 'numbers reduce load but also decrease responsiveness.')); $form->appendChild('
'); $form->appendChild( '

Application Configuration

'. '
'); if ($has_branches) { $default_branch_name = null; if ($is_mercurial) { $default_branch_name = 'default'; } else if ($is_git) { $default_branch_name = 'master'; } $form ->appendChild( id(new AphrontFormTextControl()) ->setName('default-branch') ->setLabel('Default Branch') ->setValue( $repository->getDetail( 'default-branch', $default_branch_name)) ->setError($e_branch) ->setCaption( - 'Default remote branch to show in Diffusion.')); + 'Default branch to show in Diffusion.')); } $form ->appendChild( id(new AphrontFormTextControl()) ->setName('default-owners-path') ->setLabel('Default Owners Path') ->setValue( $repository->getDetail( 'default-owners-path', '/')) ->setCaption('Default path in Owners tool.')); $form ->appendChild( id(new AphrontFormSelectControl()) ->setName('herald-disabled') ->setLabel('Herald Enabled') ->setValue($repository->getDetail('herald-disabled', 0)) ->setOptions( array( 0 => 'Enabled - Send Email', 1 => 'Disabled - Do Not Send Email', )) ->setCaption( 'You can temporarily disable Herald commit notifications when '. 'reparsing a repository or importing a new repository.')); $parsers = id(new PhutilSymbolLoader()) ->setAncestorClass('PhabricatorRepositoryCommitMessageDetailParser') ->selectSymbolsWithoutLoading(); $parsers = ipull($parsers, 'name', 'name'); $form ->appendChild( '

If you extend the commit '. 'message format, you can provide a new parser which will extract '. 'extra information from it when commits are imported. This is an '. 'advanced feature, and using the default parser will be suitable '. 'in most cases.

') ->appendChild( id(new AphrontFormSelectControl()) ->setName('detail-parser') ->setLabel('Detail Parser') ->setOptions($parsers) ->setValue( $repository->getDetail( 'detail-parser', 'PhabricatorRepositoryDefaultCommitMessageDetailParser'))); if ($is_svn) { $form ->appendChild( id(new AphrontFormTextControl()) ->setName('uuid') ->setLabel('UUID') ->setValue($repository->getUUID()) ->setCaption('Repository UUID from svn info.')); } $form->appendChild('
'); $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue('Save Configuration')); $panel = new AphrontPanelView(); $panel->setHeader('Repository Tracking'); $panel->appendChild($form); $panel->setWidth(AphrontPanelView::WIDTH_WIDE); $nav = $this->sideNav; $nav->appendChild($error_view); $nav->appendChild($panel); return $this->buildStandardPageResponse( $nav, array( 'title' => 'Edit Repository Tracking', )); } } diff --git a/src/applications/repository/daemon/commitdiscovery/base/PhabricatorRepositoryCommitDiscoveryDaemon.php b/src/applications/repository/daemon/commitdiscovery/base/PhabricatorRepositoryCommitDiscoveryDaemon.php index bbc542271..c4d60a6d9 100644 --- a/src/applications/repository/daemon/commitdiscovery/base/PhabricatorRepositoryCommitDiscoveryDaemon.php +++ b/src/applications/repository/daemon/commitdiscovery/base/PhabricatorRepositoryCommitDiscoveryDaemon.php @@ -1,111 +1,113 @@ repository; } final public function run() { - $this->repository = $this->loadRepository(); - - $sleep = $this->repository->getDetail('pull-frequency'); while (true) { + // Reload the repository every time to pick up changes from the web + // console. + $this->repository = $this->loadRepository(); $this->discoverCommits(); - $this->sleep(max(2, $sleep)); + + $sleep = max(2, $this->getRepository()->getDetail('pull-frequency')); + $this->sleep($sleep); } } final public function runOnce() { $this->repository = $this->loadRepository(); $this->discoverCommits(); } protected function isKnownCommit($target) { if (isset($this->commitCache[$target])) { return true; } $commit = id(new PhabricatorRepositoryCommit())->loadOneWhere( 'repositoryID = %s AND commitIdentifier = %s', $this->getRepository()->getID(), $target); if (!$commit) { return false; } $this->commitCache[$target] = true; while (count($this->commitCache) > 64) { array_shift($this->commitCache); } return true; } protected function recordCommit($commit_identifier, $epoch) { $repository = $this->getRepository(); $commit = new PhabricatorRepositoryCommit(); $commit->setRepositoryID($repository->getID()); $commit->setCommitIdentifier($commit_identifier); $commit->setEpoch($epoch); try { $commit->save(); $event = new PhabricatorTimelineEvent( 'cmit', array( 'id' => $commit->getID(), )); $event->recordEvent(); queryfx( $repository->establishConnection('w'), 'INSERT INTO %T (repositoryID, size, lastCommitID, epoch) VALUES (%d, 1, %d, %d) ON DUPLICATE KEY UPDATE size = size + 1, lastCommitID = IF(VALUES(epoch) > epoch, VALUES(lastCommitID), lastCommitID), epoch = IF(VALUES(epoch) > epoch, VALUES(epoch), epoch)', PhabricatorRepository::TABLE_SUMMARY, $repository->getID(), $commit->getID(), $epoch); $this->commitCache[$commit_identifier] = true; } catch (AphrontQueryDuplicateKeyException $ex) { // Ignore. This can happen because we discover the same new commit // more than once when looking at history, or because of races or // data inconsistency or cosmic radiation; in any case, we're still // in a good state if we ignore the failure. $this->commitCache[$commit_identifier] = true; } $this->stillWorking(); } abstract protected function discoverCommits(); } diff --git a/src/applications/repository/daemon/commitdiscovery/git/PhabricatorRepositoryGitCommitDiscoveryDaemon.php b/src/applications/repository/daemon/commitdiscovery/git/PhabricatorRepositoryGitCommitDiscoveryDaemon.php index 9f59c2550..275d02eac 100644 --- a/src/applications/repository/daemon/commitdiscovery/git/PhabricatorRepositoryGitCommitDiscoveryDaemon.php +++ b/src/applications/repository/daemon/commitdiscovery/git/PhabricatorRepositoryGitCommitDiscoveryDaemon.php @@ -1,147 +1,162 @@ getRepository(); $vcs = $repository->getVersionControlSystem(); if ($vcs != PhabricatorRepositoryType::REPOSITORY_TYPE_GIT) { throw new Exception("Repository is not a git repository."); } list($remotes) = $repository->execxLocalCommand( 'remote show -n origin'); $matches = null; if (!preg_match('/^\s*Fetch URL:\s*(.*?)\s*$/m', $remotes, $matches)) { throw new Exception( "Expected 'Fetch URL' in 'git remote show -n origin'."); } self::verifySameGitOrigin( $matches[1], $repository->getRemoteURI(), $repository->getLocalPath()); list($stdout) = $repository->execxLocalCommand( 'branch -r --verbose --no-abbrev'); $branches = DiffusionGitBranchQuery::parseGitRemoteBranchOutput( $stdout, $only_this_remote = DiffusionBranchInformation::DEFAULT_GIT_REMOTE); $got_something = false; + $tracked_something = false; foreach ($branches as $name => $commit) { + if (!$repository->shouldTrackBranch($name)) { + continue; + } + + $tracked_something = true; + if ($this->isKnownCommit($commit)) { continue; } else { $this->discoverCommit($commit); $got_something = true; } } + if (!$tracked_something) { + $repo_name = $repository->getName(); + $repo_callsign = $repository->getCallsign(); + throw new Exception( + "Repository r{$repo_callsign} '{$repo_name}' has no tracked branches! ". + "Verify that your branch filtering settings are correct."); + } + return $got_something; } private function discoverCommit($commit) { $discover = array(); $insert = array(); $repository = $this->getRepository(); $discover[] = $commit; $insert[] = $commit; $seen_parent = array(); while (true) { $target = array_pop($discover); list($parents) = $repository->execxLocalCommand( 'log -n1 --pretty="%%P" %s', $target); $parents = array_filter(explode(' ', trim($parents))); foreach ($parents as $parent) { if (isset($seen_parent[$parent])) { // We end up in a loop here somehow when we parse Arcanist if we // don't do this. TODO: Figure out why and draw a pretty diagram // since it's not evident how parsing a DAG with this causes the // loop to stop terminating. continue; } $seen_parent[$parent] = true; if (!$this->isKnownCommit($parent)) { $discover[] = $parent; $insert[] = $parent; } } if (empty($discover)) { break; } $this->stillWorking(); } while (true) { $target = array_pop($insert); list($epoch) = $repository->execxLocalCommand( 'log -n1 --pretty="%%at" %s', $target); $epoch = trim($epoch); $this->recordCommit($target, $epoch); if (empty($insert)) { break; } } } public static function verifySameGitOrigin($remote, $expect, $where) { $remote_uri = PhabricatorRepository::newPhutilURIFromGitURI($remote); $expect_uri = PhabricatorRepository::newPhutilURIFromGitURI($expect); $remote_path = $remote_uri->getPath(); $expect_path = $expect_uri->getPath(); $remote_match = self::normalizeGitPath($remote_path); $expect_match = self::normalizeGitPath($expect_path); if ($remote_match != $expect_match) { throw new Exception( "Working copy at '{$where}' has a mismatched origin URL. It has ". "origin URL '{$remote}' (with remote path '{$remote_path}'), but the ". "configured URL '{$expect}' (with remote path '{$expect_path}') is ". "expected. Refusing to proceed because this may indicate that the ". "working copy is actually some other repository."); } } private static function normalizeGitPath($path) { // Strip away trailing "/" and ".git", so similar paths correctly match. $path = rtrim($path, '/'); $path = preg_replace('/\.git$/', '', $path); return $path; } } diff --git a/src/applications/repository/storage/repository/PhabricatorRepository.php b/src/applications/repository/storage/repository/PhabricatorRepository.php index 87647e028..1bcf27e3d 100644 --- a/src/applications/repository/storage/repository/PhabricatorRepository.php +++ b/src/applications/repository/storage/repository/PhabricatorRepository.php @@ -1,311 +1,329 @@ true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPHIDConstants::PHID_TYPE_REPO); } public function getDetail($key, $default = null) { return idx($this->details, $key, $default); } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } public static function newPhutilURIFromGitURI($raw_uri) { // If there's no protocol (git implicit SSH) reformat the URI to be a // normal URI. These git URIs look like "user@domain.com:path" instead of // "ssh://user@domain/path". $uri = new PhutilURI($raw_uri); if (!$uri->getProtocol()) { list($domain, $path) = explode(':', $raw_uri, 2); $uri = new PhutilURI('ssh://'.$domain.'/'.$path); } return $uri; } public function getRemoteURI() { $raw_uri = $this->getDetail('remote-uri'); $vcs = $this->getVersionControlSystem(); $is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT); if ($is_git) { $uri = self::newPhutilURIFromGitURI($raw_uri); } else { $uri = new PhutilURI($raw_uri); } if ($this->isSSHProtocol($uri->getProtocol())) { if ($this->getSSHLogin()) { $uri->setUser($this->getSSHLogin()); } } return (string)$uri; } public function getLocalPath() { return $this->getDetail('local-path'); } public function execRemoteCommand($pattern /*, $arg, ... */) { $args = func_get_args(); $args = $this->formatRemoteCommand($args); return call_user_func_array('exec_manual', $args); } public function execxRemoteCommand($pattern /*, $arg, ... */) { $args = func_get_args(); $args = $this->formatRemoteCommand($args); return call_user_func_array('execx', $args); } public function getRemoteCommandFuture($pattern /*, $arg, ... */) { $args = func_get_args(); $args = $this->formatRemoteCommand($args); return newv('ExecFuture', $args); } public function passthruRemoteCommand($pattern /*, $arg, ... */) { $args = func_get_args(); $args = $this->formatRemoteCommand($args); return call_user_func_array('phutil_passthru', $args); } public function execLocalCommand($pattern /*, $arg, ... */) { $args = func_get_args(); $args = $this->formatLocalCommand($args); return call_user_func_array('exec_manual', $args); } public function execxLocalCommand($pattern /*, $arg, ... */) { $args = func_get_args(); $args = $this->formatLocalCommand($args); return call_user_func_array('execx', $args); } public function getLocalCommandFuture($pattern /*, $arg, ... */) { $args = func_get_args(); $args = $this->formatLocalCommand($args); return newv('ExecFuture', $args); } public function passthruLocalCommand($pattern /*, $arg, ... */) { $args = func_get_args(); $args = $this->formatLocalCommand($args); return call_user_func_array('phutil_passthru', $args); } private function formatRemoteCommand(array $args) { $pattern = $args[0]; $args = array_slice($args, 1); if ($this->shouldUseSSH()) { switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $pattern = "SVN_SSH=%s svn --non-interactive {$pattern}"; array_unshift( $args, csprintf( 'ssh -l %s -i %s', $this->getSSHLogin(), $this->getSSHKeyfile())); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $command = call_user_func_array( 'csprintf', array_merge( array( "(ssh-add %s && git {$pattern})", $this->getSSHKeyfile(), ), $args)); $pattern = "ssh-agent sh -c %s"; $args = array($command); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $pattern = "hg --config ui.ssh=%s {$pattern}"; array_unshift( $args, csprintf( 'ssh -l %s -i %s', $this->getSSHLogin(), $this->getSSHKeyfile())); break; default: throw new Exception("Unrecognized version control system."); } } else if ($this->shouldUseHTTP()) { switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $pattern = "svn ". "--non-interactive ". "--no-auth-cache ". "--trust-server-cert ". "--username %s ". "--password %s ". $pattern; array_unshift( $args, $this->getDetail('http-login'), $this->getDetail('http-pass')); break; default: throw new Exception( "No support for HTTP Basic Auth in this version control system."); } } else { 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; } private function formatLocalCommand(array $args) { $pattern = $args[0]; $args = array_slice($args, 1); switch ($this->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $pattern = "(cd %s && svn --non-interactive {$pattern})"; array_unshift($args, $this->getLocalPath()); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $pattern = "(cd %s && git {$pattern})"; array_unshift($args, $this->getLocalPath()); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $pattern = "(cd %s && hg {$pattern})"; array_unshift($args, $this->getLocalPath()); break; default: throw new Exception("Unrecognized version control system."); } array_unshift($args, $pattern); return $args; } private function getSSHLogin() { return $this->getDetail('ssh-login'); } private function getSSHKeyfile() { if ($this->sshKeyfile === null) { $key = $this->getDetail('ssh-key'); $keyfile = $this->getDetail('ssh-keyfile'); if ($keyfile) { // Make sure we can read the file, that it exists, etc. Filesystem::readFile($keyfile); $this->sshKeyfile = $keyfile; } else if ($key) { $keyfile = new TempFile('phabricator-repository-ssh-key'); chmod($keyfile, 0600); Filesystem::writeFile($keyfile, $key); $this->sshKeyfile = $keyfile; } else { $this->sshKeyfile = ''; } } return (string)$this->sshKeyfile; } public function shouldUseSSH() { $uri = new PhutilURI($this->getRemoteURI()); $protocol = $uri->getProtocol(); if ($this->isSSHProtocol($protocol)) { return (bool)$this->getSSHKeyfile(); } else { return false; } } public function shouldUseHTTP() { $uri = new PhutilURI($this->getRemoteURI()); $protocol = $uri->getProtocol(); if ($this->isHTTPProtocol($protocol)) { return (bool)$this->getDetail('http-login'); } else { return false; } } private function isSSHProtocol($protocol) { return ($protocol == 'ssh' || $protocol == 'svn+ssh'); } private function isHTTPProtocol($protocol) { return ($protocol == 'http' || $protocol == 'https'); } public function isTracked() { return $this->getDetail('tracking-enabled', false); } + public function shouldTrackBranch($branch) { + $vcs = $this->getVersionControlSystem(); + + $is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT); + + $use_filter = ($is_git); + + if ($use_filter) { + $filter = $this->getDetail('branch-filter', array()); + if ($filter && !isset($filter[$branch])) { + return false; + } + } + + // By default, track all branches. + return true; + } + } diff --git a/src/applications/repository/storage/repository/__tests__/PhabricatorRepositoryTestCase.php b/src/applications/repository/storage/repository/__tests__/PhabricatorRepositoryTestCase.php index 0bc70a892..7af624642 100644 --- a/src/applications/repository/storage/repository/__tests__/PhabricatorRepositoryTestCase.php +++ b/src/applications/repository/storage/repository/__tests__/PhabricatorRepositoryTestCase.php @@ -1,37 +1,65 @@ 'ssh://user@domain.com/path.git', 'user@domain.com:path.git' => 'ssh://user@domain.com/path.git', ); foreach ($map as $raw => $expect) { $uri = PhabricatorRepository::newPhutilURIFromGitURI($raw); $this->assertEqual( $expect, (string)$uri, "Normalized Git URI '{$raw}'"); } } + public function testBranchFilter() { + $git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; + + $repo = new PhabricatorRepository(); + $repo->setVersionControlSystem($git); + + $this->assertEqual( + true, + $repo->shouldTrackBranch('imaginary'), + 'Track all branches by default.'); + + $repo->setDetail( + 'branch-filter', + array( + 'master' => true, + )); + + $this->assertEqual( + true, + $repo->shouldTrackBranch('master'), + 'Track listed branches.'); + + $this->assertEqual( + false, + $repo->shouldTrackBranch('imaginary'), + 'Do not track unlisted branches.'); + } + } diff --git a/src/applications/repository/storage/repository/__tests__/__init__.php b/src/applications/repository/storage/repository/__tests__/__init__.php index 8676e844d..60c3a605b 100644 --- a/src/applications/repository/storage/repository/__tests__/__init__.php +++ b/src/applications/repository/storage/repository/__tests__/__init__.php @@ -1,13 +1,14 @@