diff --git a/src/applications/repository/controller/edit/PhabricatorRepositoryEditController.php b/src/applications/repository/controller/edit/PhabricatorRepositoryEditController.php index 2794cb2c02..02b7623475 100644 --- a/src/applications/repository/controller/edit/PhabricatorRepositoryEditController.php +++ b/src/applications/repository/controller/edit/PhabricatorRepositoryEditController.php @@ -32,7 +32,6 @@ class PhabricatorRepositoryEditController public function processRequest() { $request = $this->getRequest(); - $user = $request->getUser(); $repository = id(new PhabricatorRepository())->load($this->id); if (!$repository) { @@ -52,7 +51,7 @@ class PhabricatorRepositoryEditController $repository->save(); } - $views['github'] = 'Github'; + $views['github'] = 'GitHub'; } $this->repository = $repository; @@ -96,11 +95,6 @@ class PhabricatorRepositoryEditController $repository = $this->repository; $repository_id = $repository->getID(); - $type_map = array( - 'svn' => 'Subversion', - 'git' => 'Git', - ); - $errors = array(); $e_name = true; @@ -223,7 +217,9 @@ class PhabricatorRepositoryEditController $tracking = ($request->getStr('tracking') == 'enabled' ? true : false); $repository->setDetail('tracking-enabled', $tracking); $repository->setDetail('remote-uri', $request->getStr('uri')); - $repository->setDetail('local-path', $request->getStr('path')); + if ($is_git) { + $repository->setDetail('local-path', $request->getStr('path')); + } $repository->setDetail( 'pull-frequency', max(1, $request->getInt('frequency'))); @@ -246,6 +242,11 @@ class PhabricatorRepositoryEditController 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( @@ -262,16 +263,18 @@ class PhabricatorRepositoryEditController !preg_match('@/$@', $repository->getDetail('remote-uri'))) { $e_uri = 'Invalid'; - $errors[] = 'Subversion Repository URI must end in a slash ("/").'; + $errors[] = 'Subversion Repository Root must end in a slash ("/").'; } else { $e_uri = null; } - if (!$repository->getDetail('local-path')) { - $e_path = 'Required'; - $errors[] = "Local path is required."; - } else { - $e_path = null; + if ($is_git) { + if (!$repository->getDetail('local-path')) { + $e_path = 'Required'; + $errors[] = "Local path is required."; + } else { + $e_path = null; + } } } @@ -296,29 +299,23 @@ class PhabricatorRepositoryEditController 'before changes will take effect.'); } - $uri_caption = null; - $path_caption = null; - - switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $is_git = true; - $uri_caption = - 'The user the tracking daemon runs as must have permission to '. - 'git clone from this URI.'; - $path_caption = - 'Directory where the daemon should look to find a copy of the '. - 'repository (or create one if it does not yet exist). The daemon '. - 'will regularly pull remote changes into this working copy.'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $is_svn = true; - $uri_caption = - 'The user the tracking daemon runs as must have permission to '. - 'svn log from this URI.'; 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) @@ -328,7 +325,8 @@ class PhabricatorRepositoryEditController '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 (TODO: explain this).

') + 'restart) the daemons. More information is available in the '. + ''.$user_guide_link.'.

') ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('Repository') @@ -344,21 +342,67 @@ class PhabricatorRepositoryEditController ->setValue( $repository->getDetail('tracking-enabled') ? 'enabled' - : 'disabled')) + : 'disabled')); + + $uri_label = 'Repository URI'; + if ($is_git) { + $instructions = + 'NOTE: The user the tracking daemon runs as must have permission to '. + 'git clone from this URI.'; + $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/.'. + '

'. + 'NOTE: The user the daemons run as must be able to execute '. + 'svn log against this URI.'; + $form->appendChild( + '

'.$instructions.'

'); + $uri_label = 'Repository Root'; + } + + $form ->appendChild( id(new AphrontFormTextControl()) ->setName('uri') - ->setLabel('URI') + ->setLabel($uri_label) ->setValue($repository->getDetail('remote-uri')) - ->setError($e_uri) - ->setCaption($uri_caption)) - ->appendChild( + ->setError($e_uri)); + + if ($is_git) { + $form->appendChild( + '

Select a path on local disk '. + 'which the daemons should git clone the repository into. '. + 'This must be readable and writable by the daemons, and readable by '. + 'the webserver. The daemons will git fetch 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) - ->setCaption($path_caption)) + ->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)); + } + + $form ->appendChild( id(new AphrontFormTextControl()) ->setName('frequency') diff --git a/src/applications/repository/daemon/commitdiscovery/svn/PhabricatorRepositorySvnCommitDiscoveryDaemon.php b/src/applications/repository/daemon/commitdiscovery/svn/PhabricatorRepositorySvnCommitDiscoveryDaemon.php index 44943d6fd3..e8bc3a8455 100644 --- a/src/applications/repository/daemon/commitdiscovery/svn/PhabricatorRepositorySvnCommitDiscoveryDaemon.php +++ b/src/applications/repository/daemon/commitdiscovery/svn/PhabricatorRepositorySvnCommitDiscoveryDaemon.php @@ -27,51 +27,96 @@ class PhabricatorRepositorySvnCommitDiscoveryDaemon throw new Exception("Repository is not a svn repository."); } - $repository_phid = $repository->getPHID(); - - $uri = $repository->getDetail('remote-uri'); + $uri = $this->getBaseSVNLogURI(); list($xml) = execx( 'svn log --xml --non-interactive --quiet --limit 1 %s@HEAD', $uri); - // TODO: We need to slam the XML output into valid UTF-8. - - $log = new SimpleXMLElement($xml); - $entry = $log->logentry[0]; - $commit = (int)$entry['revision']; + $results = $this->parseSVNLogXML($xml); + $commit = key($results); + $epoch = reset($results); if ($this->isKnownCommit($commit)) { return false; } - $this->discoverCommit($commit); + $this->discoverCommit($commit, $epoch); return true; } - private function discoverCommit($commit) { - $discover = array(); - $largest_known = $commit - 1; - while ($largest_known > 0 && !$this->isKnownCommit($largest_known)) { - $largest_known--; + private function discoverCommit($commit, $epoch) { + $uri = $this->getBaseSVNLogURI(); + + $discover = array( + $commit => $epoch, + ); + $upper_bound = $commit; + + $limit = 1; + while ($upper_bound > 1 && !$this->isKnownCommit($upper_bound)) { + // Find all the unknown commits on this path. Note that we permit + // importing an SVN subdirectory rather than the entire repository, so + // commits may be nonsequential. + list($err, $xml, $stderr) = exec_manual( + 'svn log --xml --non-interactive --quiet --limit %d %s@%d', + $limit, + $uri, + $upper_bound - 1); + if ($err) { + if (preg_match('/path not found/', $stderr)) { + // We've gone all the way back through history and this path was not + // affected by earlier commits. + break; + } else { + throw new Exception("svn log error #{$err}: {$stderr}"); + } + } + $discover += $this->parseSVNLogXML($xml); + + $upper_bound = min(array_keys($discover)); + + // Discover 2, 4, 8, ... 256 logs at a time. This allows us to initially + // import large repositories fairly quickly, while pulling only as much + // data as we need in the common case (when we've already imported the + // repository and are just grabbing one commit at a time). + $limit = min($limit * 2, 256); } - $repository = $this->getRepository(); - $uri = $repository->getDetail('remote-uri'); + // NOTE: We do writes only after discovering all the commits so that we're + // never left in a state where we've missed commits -- if the discovery + // script terminates it can always resume and restore the import to a good + // state. This is also why we sort the discovered commits so we can do + // writes forward from the smallest one. - for ($ii = $largest_known + 1; $ii <= $commit; $ii++) { - list($xml) = execx( - 'svn log --xml --non-interactive --quiet --limit 1 %s@%d', - $uri, - $ii); - $log = new SimpleXMLElement($xml); - $entry = $log->logentry[0]; - - $identifier = (int)$entry['revision']; - $epoch = (int)strtotime((string)$entry->date[0]); - - $this->recordCommit($identifier, $epoch); + ksort($discover); + foreach ($discover as $commit => $epoch) { + $this->recordCommit($commit, $epoch); } } + private function parseSVNLogXML($xml) { + $xml = phutil_utf8ize($xml); + + $result = array(); + + $log = new SimpleXMLElement($xml); + foreach ($log->logentry as $entry) { + $commit = (int)$entry['revision']; + $epoch = (int)strtotime((string)$entry->date[0]); + $result[$commit] = $epoch; + } + + return $result; + } + + + private function getBaseSVNLogURI() { + $repository = $this->getRepository(); + + $uri = $repository->getDetail('remote-uri'); + $subpath = $repository->getDetail('svn-subpath'); + + return $uri.$subpath; + } } diff --git a/src/applications/repository/daemon/commitdiscovery/svn/__init__.php b/src/applications/repository/daemon/commitdiscovery/svn/__init__.php index 61d4b5a2af..df2258faf8 100644 --- a/src/applications/repository/daemon/commitdiscovery/svn/__init__.php +++ b/src/applications/repository/daemon/commitdiscovery/svn/__init__.php @@ -10,6 +10,7 @@ phutil_require_module('phabricator', 'applications/repository/constants/reposito phutil_require_module('phabricator', 'applications/repository/daemon/commitdiscovery/base'); phutil_require_module('phutil', 'future/exec'); +phutil_require_module('phutil', 'utils'); phutil_require_source('PhabricatorRepositorySvnCommitDiscoveryDaemon.php');