Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F15420352
D21372.id50874.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
8 KB
Referenced Files
None
Subscribers
None
D21372.id50874.diff
View Options
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -427,6 +427,8 @@
'ArcanistRepositoryQuery' => 'repository/query/ArcanistRepositoryQuery.php',
'ArcanistRepositoryRef' => 'ref/ArcanistRepositoryRef.php',
'ArcanistRepositoryRemoteQuery' => 'repository/remote/ArcanistRepositoryRemoteQuery.php',
+ 'ArcanistRepositoryURINormalizer' => 'repository/remote/ArcanistRepositoryURINormalizer.php',
+ 'ArcanistRepositoryURINormalizerTestCase' => 'repository/remote/__tests__/ArcanistRepositoryURINormalizerTestCase.php',
'ArcanistReusedAsIteratorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedAsIteratorXHPASTLinterRule.php',
'ArcanistReusedAsIteratorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistReusedAsIteratorXHPASTLinterRuleTestCase.php',
'ArcanistReusedIteratorReferenceXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedIteratorReferenceXHPASTLinterRule.php',
@@ -1455,6 +1457,8 @@
'ArcanistRepositoryQuery' => 'Phobject',
'ArcanistRepositoryRef' => 'ArcanistRef',
'ArcanistRepositoryRemoteQuery' => 'ArcanistRepositoryQuery',
+ 'ArcanistRepositoryURINormalizer' => 'Phobject',
+ 'ArcanistRepositoryURINormalizerTestCase' => 'PhutilTestCase',
'ArcanistReusedAsIteratorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistReusedAsIteratorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistReusedIteratorReferenceXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
diff --git a/src/repository/remote/ArcanistRepositoryURINormalizer.php b/src/repository/remote/ArcanistRepositoryURINormalizer.php
new file mode 100644
--- /dev/null
+++ b/src/repository/remote/ArcanistRepositoryURINormalizer.php
@@ -0,0 +1,159 @@
+<?php
+
+/**
+ * Normalize repository URIs. For example, these URIs are generally equivalent
+ * and all point at the same repository:
+ *
+ * ssh://user@host/repo
+ * ssh://user@host/repo/
+ * ssh://user@host:22/repo
+ * user@host:/repo
+ * ssh://user@host/repo.git
+ *
+ * This class can be used to normalize URIs like this, in order to detect
+ * alternate spellings of the same repository URI. In particular, the
+ * @{method:getNormalizedPath} method will return:
+ *
+ * repo
+ *
+ * ...for all of these URIs. Generally, usage looks like this:
+ *
+ * $norm_a = new ArcanistRepositoryURINormalizer($type, $uri_a);
+ * $norm_b = new ArcanistRepositoryURINormalizer($type, $uri_b);
+ *
+ * if ($norm_a->getNormalizedPath() === $norm_b->getNormalizedPath()) {
+ * // URIs appear to point at the same repository.
+ * } else {
+ * // URIs are very unlikely to be the same repository.
+ * }
+ *
+ * Because a repository can be hosted at arbitrarily many arbitrary URIs, there
+ * is no way to completely prevent false negatives by only examining URIs
+ * (that is, repositories with totally different URIs could really be the same).
+ * However, normalization is relatively aggressive and false negatives should
+ * be rare: if normalization says two URIs are different repositories, they
+ * probably are.
+ *
+ * @task normal Normalizing URIs
+ */
+final class ArcanistRepositoryURINormalizer
+ extends Phobject {
+
+ const TYPE_GIT = 'git';
+ const TYPE_SVN = 'svn';
+ const TYPE_MERCURIAL = 'hg';
+
+ private $type;
+ private $uri;
+ private $domainMap = array();
+
+ public function __construct($type, $uri) {
+ switch ($type) {
+ case self::TYPE_GIT:
+ case self::TYPE_SVN:
+ case self::TYPE_MERCURIAL:
+ break;
+ default:
+ throw new Exception(pht('Unknown URI type "%s"!', $type));
+ }
+
+ $this->type = $type;
+ $this->uri = $uri;
+ }
+
+ public static function getAllURITypes() {
+ return array(
+ self::TYPE_GIT,
+ self::TYPE_SVN,
+ self::TYPE_MERCURIAL,
+ );
+ }
+
+ public function setDomainMap(array $domain_map) {
+ foreach ($domain_map as $key => $domain) {
+ $domain_map[$key] = phutil_utf8_strtolower($domain);
+ }
+
+ $this->domainMap = $domain_map;
+ return $this;
+ }
+
+
+/* -( Normalizing URIs )--------------------------------------------------- */
+
+
+ /**
+ * @task normal
+ */
+ public function getPath() {
+ switch ($this->type) {
+ case self::TYPE_GIT:
+ $uri = new PhutilURI($this->uri);
+ return $uri->getPath();
+ case self::TYPE_SVN:
+ case self::TYPE_MERCURIAL:
+ $uri = new PhutilURI($this->uri);
+ if ($uri->getProtocol()) {
+ return $uri->getPath();
+ }
+
+ return $this->uri;
+ }
+ }
+
+ public function getNormalizedURI() {
+ return $this->getNormalizedDomain().'/'.$this->getNormalizedPath();
+ }
+
+
+ /**
+ * @task normal
+ */
+ public function getNormalizedPath() {
+ $path = $this->getPath();
+ $path = trim($path, '/');
+
+ switch ($this->type) {
+ case self::TYPE_GIT:
+ $path = preg_replace('/\.git$/', '', $path);
+ break;
+ case self::TYPE_SVN:
+ case self::TYPE_MERCURIAL:
+ break;
+ }
+
+ // If this is a Phabricator URI, strip it down to the callsign. We mutably
+ // allow you to clone repositories as "/diffusion/X/anything.git", for
+ // example.
+
+ $matches = null;
+ if (preg_match('@^(diffusion/(?:[A-Z]+|\d+))@', $path, $matches)) {
+ $path = $matches[1];
+ }
+
+ return $path;
+ }
+
+ public function getNormalizedDomain() {
+ $domain = null;
+
+ $uri = new PhutilURI($this->uri);
+ $domain = $uri->getDomain();
+
+ if (!strlen($domain)) {
+ return '<void>';
+ }
+
+ $domain = phutil_utf8_strtolower($domain);
+
+ foreach ($this->domainMap as $domain_key => $domain_value) {
+ if ($domain === $domain_value) {
+ $domain = $domain_key;
+ break;
+ }
+ }
+
+ return $domain;
+ }
+
+}
diff --git a/src/repository/remote/__tests__/ArcanistRepositoryURINormalizerTestCase.php b/src/repository/remote/__tests__/ArcanistRepositoryURINormalizerTestCase.php
new file mode 100644
--- /dev/null
+++ b/src/repository/remote/__tests__/ArcanistRepositoryURINormalizerTestCase.php
@@ -0,0 +1,84 @@
+<?php
+
+final class ArcanistRepositoryURINormalizerTestCase
+ extends PhutilTestCase {
+
+ public function testGitURINormalizer() {
+ $cases = array(
+ 'ssh://user@domain.com/path.git' => 'path',
+ 'https://user@domain.com/path.git' => 'path',
+ 'git@domain.com:path.git' => 'path',
+ 'ssh://user@gitserv002.com/path.git' => 'path',
+ 'ssh://htaft@domain.com/path.git' => 'path',
+ 'ssh://user@domain.com/bananas.git' => 'bananas',
+ 'git@domain.com:bananas.git' => 'bananas',
+ 'user@domain.com:path/repo' => 'path/repo',
+ 'user@domain.com:path/repo/' => 'path/repo',
+ 'file:///path/to/local/repo.git' => 'path/to/local/repo',
+ '/path/to/local/repo.git' => 'path/to/local/repo',
+ 'ssh://something.com/diffusion/X/anything.git' => 'diffusion/X',
+ 'ssh://something.com/diffusion/X/' => 'diffusion/X',
+ );
+
+ $type_git = ArcanistRepositoryURINormalizer::TYPE_GIT;
+
+ foreach ($cases as $input => $expect) {
+ $normal = new ArcanistRepositoryURINormalizer($type_git, $input);
+ $this->assertEqual(
+ $expect,
+ $normal->getNormalizedPath(),
+ pht('Normalized Git path for "%s".', $input));
+ }
+ }
+
+ public function testDomainURINormalizer() {
+ $base_domain = 'base.phabricator.example.com';
+ $ssh_domain = 'ssh.phabricator.example.com';
+
+ $domain_map = array(
+ '<base-uri>' => $base_domain,
+ '<ssh-host>' => $ssh_domain,
+ );
+
+ $cases = array(
+ '/' => '<void>',
+ '/path/to/local/repo.git' => '<void>',
+ 'ssh://user@domain.com/path.git' => 'domain.com',
+ 'ssh://user@DOMAIN.COM/path.git' => 'domain.com',
+ 'http://'.$base_domain.'/diffusion/X/' => '<base-uri>',
+ 'ssh://'.$ssh_domain.'/diffusion/X/' => '<ssh-host>',
+ 'git@'.$ssh_domain.':bananas.git' => '<ssh-host>',
+ );
+
+ $type_git = ArcanistRepositoryURINormalizer::TYPE_GIT;
+
+ foreach ($cases as $input => $expect) {
+ $normalizer = new ArcanistRepositoryURINormalizer($type_git, $input);
+
+ $normalizer->setDomainMap($domain_map);
+
+ $this->assertEqual(
+ $expect,
+ $normalizer->getNormalizedDomain(),
+ pht('Normalized domain for "%s".', $input));
+ }
+ }
+
+ public function testSVNURINormalizer() {
+ $cases = array(
+ 'file:///path/to/repo' => 'path/to/repo',
+ 'file:///path/to/repo/' => 'path/to/repo',
+ );
+
+ $type_svn = ArcanistRepositoryURINormalizer::TYPE_SVN;
+
+ foreach ($cases as $input => $expect) {
+ $normal = new ArcanistRepositoryURINormalizer($type_svn, $input);
+ $this->assertEqual(
+ $expect,
+ $normal->getNormalizedPath(),
+ pht('Normalized SVN path for "%s".', $input));
+ }
+ }
+
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Mar 22, 1:18 PM (2 h, 52 m ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7382361
Default Alt Text
D21372.id50874.diff (8 KB)
Attached To
Mode
D21372: Copy repository URI normalization code from Phabricator to Arcanist
Attached
Detach File
Event Timeline
Log In to Comment