Edgewall Software

Ticket #7687: svn-externals-7687.patch

File svn-externals-7687.patch, 67.4 KB (added by itamaro, 2 years ago)

some more refactoring, some bug fixes, some more tests

  • tests/__init__.py

     
    11import unittest 
    22 
    3 from trac.versioncontrol.tests import cache, diff, svn_authz, svn_fs, api 
     3from trac.versioncontrol.tests import cache, diff, svn_authz,   \ 
     4                                      svn_fs, svn_prop, api 
    45from trac.versioncontrol.tests.functional import functionalSuite 
    56 
    67def suite(): 
     
    1011    suite.addTest(diff.suite()) 
    1112    suite.addTest(svn_authz.suite()) 
    1213    suite.addTest(svn_fs.suite()) 
     14    suite.addTest(svn_prop.suite()) 
    1315    suite.addTest(api.suite()) 
    1416    return suite 
    1517 
  • tests/svn_prop.py

     
     1# -*- coding: utf-8 -*- 
     2# 
     3# Copyright (C)2005-2010 Edgewall Software 
     4# Copyright (C) 2010 Itamar Ostricher <itamarost@gmail.com> 
     5# All rights reserved. 
     6# 
     7# This software is licensed as described in the file COPYING, which 
     8# you should have received as part of this distribution. The terms 
     9# are also available at http://trac.edgewall.org/wiki/TracLicense. 
     10# 
     11# This software consists of voluntary contributions made by many 
     12# individuals. For the exact contribution history, see the revision 
     13# history and logs, available at http://trac.edgewall.org/log/. 
     14# 
     15# Author: Itamar Ostricher <itamarost@gmail.com> 
     16 
     17import unittest 
     18import random 
     19from urllib import splittype 
     20 
     21from trac.test import Mock 
     22from trac.core import TracError 
     23from trac.versioncontrol import svn_prop 
     24 
     25class SvnExternalsParserTests(unittest.TestCase): 
     26 
     27    def setUp(self): 
     28        self.myrep = Mock(reponame='myrep') 
     29 
     30    def test_no_externals(self): 
     31        externals_prop = '' 
     32        externals_list = svn_prop.parse_externals(externals_prop, 
     33                    self.myrep, repopath='dev/trunk/src/my-module') 
     34        self.assertTrue(isinstance(externals_list, list)) 
     35        self.assertEqual(0, len(externals_list)) 
     36 
     37    def test_old_syntax_parsing(self): 
     38        externals_prop = """ 
     39            simple http://example.org/svn/repos/dir1 
     40            # this is a comment that should be ignored. 
     41            spaceless_rev -r10 svn://example.org/svn/repos/dir2 
     42            spaced_rev -r 12 svn+ssh://example.org/svn/repos/dir3 
     43            """ 
     44        externals_list = svn_prop.parse_externals(externals_prop, 
     45                    self.myrep, repopath='dev/trunk/src/my-module') 
     46        self.assertTrue(isinstance(externals_list, list)) 
     47        self.assertEqual(3, len(externals_list)) 
     48        for ext_dict in externals_list: 
     49            self.assertTrue(isinstance(ext_dict, dict)) 
     50            self.assertEqual(ext_dict['repo'], self.myrep) 
     51            self.assertEqual(ext_dict['repopath'], 
     52                             'dev/trunk/src/my-module') 
     53        simple, spaceless, spaced = externals_list 
     54        self.assertEqual(simple['dir'], 'simple') 
     55        self.assertEqual(simple['url'], 
     56                         'http://example.org/svn/repos/dir1') 
     57        self.assertTrue(simple['rev'] is None) 
     58        self.assertEqual(spaceless['dir'], 'spaceless_rev') 
     59        self.assertEqual(spaceless['url'], 
     60                         'svn://example.org/svn/repos/dir2') 
     61        self.assertEqual(10, spaceless['rev']) 
     62        self.assertEqual(spaced['dir'], 'spaced_rev') 
     63        self.assertEqual(spaced['url'], 
     64                         'svn+ssh://example.org/svn/repos/dir3') 
     65        self.assertEqual(12, spaced['rev']) 
     66 
     67    def test_new_syntax_parsing(self): 
     68        externals_prop = u""" 
     69            https://example.org/svn/repos/dir1 simple 
     70            -r10 ^/repos/dir2 spaceless_rev 
     71            -r 12 //example.org/svn/repos/dir3 spaced_rev 
     72            # this is a comment that should be ignored. 
     73            ../dir4@17 peg_rev 
     74            """ 
     75        externals_list = svn_prop.parse_externals(externals_prop, 
     76                    self.myrep, repopath='dev/trunk/src/my-module') 
     77        self.assertTrue(isinstance(externals_list, list)) 
     78        self.assertEqual(4, len(externals_list)) 
     79        for ext_dict in externals_list: 
     80            self.assertTrue(isinstance(ext_dict, dict)) 
     81            self.assertEqual(ext_dict['repo'], self.myrep) 
     82            self.assertEqual(ext_dict['repopath'], 
     83                             'dev/trunk/src/my-module') 
     84        simple, spaceless, spaced, pegged = externals_list 
     85        self.assertEqual(simple['dir'], 'simple') 
     86        self.assertEqual(simple['url'], 
     87                         'https://example.org/svn/repos/dir1') 
     88        self.assertTrue(simple['rev'] is None) 
     89        self.assertEqual(spaceless['dir'], 'spaceless_rev') 
     90        self.assertEqual(spaceless['url'], 
     91                         '^/repos/dir2') 
     92        self.assertEqual(10, spaceless['rev']) 
     93        self.assertEqual(spaced['dir'], 'spaced_rev') 
     94        self.assertEqual(spaced['url'], 
     95                         '//example.org/svn/repos/dir3') 
     96        self.assertEqual(12, spaced['rev']) 
     97        self.assertEqual(pegged['dir'], 'peg_rev') 
     98        self.assertEqual(pegged['url'], 
     99                         '../dir4') 
     100        self.assertEqual(17, pegged['rev']) 
     101 
     102# Repositories filesystem layout for following tests: 
     103# /var/svn 
     104# | 
     105# |-repos (svn root of repository "repos", AKA "myrep") 
     106# | | 
     107# | |-dev 
     108# | | 
     109# | \-devel 
     110# |   |-common 
     111# |   | \-trunk 
     112# |   |  \-log 
     113# |   \-my-prod 
     114# |    \-trunk 
     115# |     \-src 
     116# |      \-my-module 
     117# | 
     118# \-repos-2 (svn root of repository "repos-2", AKA "otherrep") 
     119#  \-dev 
     120 
     121class SvnExternalsRepositoryRelativeLinkTests(unittest.TestCase): 
     122 
     123    def setUp(self): 
     124        self.repo_base = 'svn:random-uuid:/var/svn/repos' 
     125        self.other_repo_base = 'svn:random-uuid:/var/svn/repos-2' 
     126        self.repositories = { 
     127            'myrep': Mock(reponame='myrep', scope='/', 
     128                           get_base=lambda: self.repo_base, 
     129                           get_path_url=lambda path, rev: None), 
     130            'my-devel-rep': Mock(reponame='my-devel-rep', scope='/devel/', 
     131                                   get_base=lambda: self.repo_base, 
     132                                   get_path_url=lambda path, rev: None), 
     133            'my-prod-rep': Mock(reponame='my-prod-rep', 
     134                                 scope='/devel/my-prod/', 
     135                                 get_base=lambda: self.repo_base, 
     136                                 get_path_url=lambda path, rev: None), 
     137            # Note 'my-dev-rep' purposefully has a scope that prefixes 
     138            # 'my-devel-rep'. This tests cases of false matching scopes. 
     139            'my-dev-rep': Mock(reponame='my-dev-rep', scope='/dev/', 
     140                                 get_base=lambda: self.repo_base, 
     141                                 get_path_url=lambda path, rev: None), 
     142            'otherrep': Mock(reponame='otherrep', scope='/', 
     143                              get_base=lambda: self.other_repo_base, 
     144                              get_path_url=lambda path, rev: None), 
     145            } 
     146 
     147    def test_repository_relative_url(self): 
     148        external = {'repo': self.repositories['myrep'], 
     149                    'repopath': 'devel/my-prod/trunk/src/my-module', 
     150                    'dir': 'local', 
     151                    'url': '^/devel/common/trunk/log', 
     152                    'rev': None} 
     153        scoped_external = {'repo': self.repositories['my-devel-rep'], 
     154                           # Note the missing 'devel/' in the repo-path 
     155                           'repopath': 'my-prod/trunk/src/my-module', 
     156                           'dir': 'local', 
     157                           'url': '^/devel/common/trunk/log', 
     158                           'rev': None} 
     159        deep_external = {'repo': self.repositories['my-devel-rep'], 
     160                         # Note the missing 'devel/my-prod/' in the repo-path 
     161                         'repopath': 'trunk/src/my-module', 
     162                         'dir': 'local', 
     163                         'url': '^/devel/common/trunk/log', 
     164                         'rev': None} 
     165         
     166        for ext in [external, scoped_external, deep_external]: 
     167            # Un-scoped repository asserts 
     168            repo = self.repositories['myrep'] 
     169            for scope in ['', '/']: 
     170                repo.scope = scope 
     171                # External as seen from same repository 
     172                self.assertEqual('devel/common/trunk/log', 
     173                    svn_prop.get_repo_relative_path(ext, repo)) 
     174            # Scoped repository asserts 
     175            repo = self.repositories['my-devel-rep'] 
     176            for scope in ['devel', '/devel', 'devel/', '/devel/']: 
     177                repo.scope = scope 
     178                # External as seen from the un-scoped repository 
     179                # Note the missing '/devel/' in the expected href 
     180                self.assertEqual('common/trunk/log', 
     181                        svn_prop.get_repo_relative_path(ext, repo)) 
     182            # Deep-scoped repository (too deep for common) 
     183            self.assertTrue(svn_prop.get_repo_relative_path(ext, 
     184                                self.repositories['my-prod-rep']) is None) 
     185            # Differently-scoped repository (out of range) 
     186            self.assertTrue(svn_prop.get_repo_relative_path(ext, 
     187                                self.repositories['my-dev-rep']) is None) 
     188            # Another repository, not related to external 
     189            self.assertTrue(svn_prop.get_repo_relative_path(ext, 
     190                                self.repositories['otherrep']) is None) 
     191 
     192    def test_directory_relative_url(self): 
     193        external = {'repo': self.repositories['myrep'], 
     194                    'repopath': 'devel/my-prod/trunk/src/my-module', 
     195                    'dir': 'local', 
     196                    'url': '../../../../common/trunk/log', 
     197                    'rev': None} 
     198        scoped_external = {'repo': self.repositories['my-devel-rep'], 
     199                           # Note the missing 'devel/' in the repo-path 
     200                           'repopath': 'my-prod/trunk/src/my-module', 
     201                           'dir': 'local', 
     202                           'url': '../../../../common/trunk/log', 
     203                           'rev': None} 
     204        deep_external = {'repo': self.repositories['my-prod-rep'], 
     205                         # Note the missing 'devel/my-prod/' in the repo-path 
     206                         'repopath': 'trunk/src/my-module', 
     207                         'dir': 'local', 
     208                         # Going even deeper just for fun (and test of course) 
     209                         'url': '../../../../../devel/common/trunk/log', 
     210                         'rev': None} 
     211 
     212        for ext in [external, scoped_external, deep_external]: 
     213            # Un-scoped repository asserts 
     214            self.assertEqual('devel/common/trunk/log', 
     215                             svn_prop.get_repo_relative_path(ext, 
     216                                                 self.repositories['myrep'])) 
     217            # Scoped repository asserts 
     218            self.assertEqual('common/trunk/log', 
     219                             svn_prop.get_repo_relative_path(ext, 
     220                                         self.repositories['my-devel-rep'])) 
     221            # Deep-scoped repository asserts (out of reach) 
     222            self.assertTrue(svn_prop.get_repo_relative_path(ext, 
     223                                self.repositories['my-prod-rep']) is None) 
     224            # Unrelated repository asserts 
     225            self.assertTrue(svn_prop.get_repo_relative_path(ext, 
     226                                    self.repositories['otherrep']) is None) 
     227 
     228    def test_invalid_directory_relative_url(self): 
     229        external = {'repo': self.repositories['myrep'], 
     230                    'repopath': 'devel/my-prod/trunk/src/my-module', 
     231                    'dir': 'local', 
     232                    # Out of repository base! aaah! 
     233                    'url': '../../../../../../common/trunk/log', 
     234                    'rev': None} 
     235        scoped_external = {'repo': self.repositories['my-devel-rep'], 
     236                           # Note the missing 'devel/' in the repo-path 
     237                           'repopath': 'my-prod/trunk/src/my-module', 
     238                           'dir': 'local', 
     239                           'url': '../../../../../../common/trunk/log', 
     240                           'rev': None} 
     241        deep_external = {'repo': self.repositories['my-prod-rep'], 
     242                         # Note the missing 'devel/my-prod/' in the repo-path 
     243                         'repopath': 'trunk/src/my-module', 
     244                         'dir': 'local', 
     245                         # Going even deeper just for fun (and test of course) 
     246                         'url': '../../../../../../../devel/common/trunk/log', 
     247                         'rev': None} 
     248         
     249        for ext in [external, scoped_external, deep_external]: 
     250            self.assertRaises(TracError, svn_prop.get_repo_relative_path, 
     251                              ext, self.repositories['myrep']) 
     252            self.assertRaises(TracError, svn_prop.get_repo_relative_path, 
     253                              ext, self.repositories['my-devel-rep']) 
     254            self.assertRaises(TracError, svn_prop.get_repo_relative_path, 
     255                              ext, self.repositories['my-prod-rep']) 
     256            self.assertTrue(svn_prop.get_repo_relative_path(ext, 
     257                              self.repositories['otherrep']) is None) 
     258 
     259class SvnExternalsUrlBasedLinkTests(unittest.TestCase): 
     260 
     261    def setUp(self): 
     262        self.repo_base = 'svn:random-uuid:/var/svn/repos' 
     263        self.repo_host = 'example.org' 
     264        self.repo_base_url = '//%s/svn/myrep' % (self.repo_host) 
     265        self.other_repo_base = 'svn:random-uuid:/var/svn/repos-2' 
     266        self.other_repo_base_url = '//%s/svn/otherrep' % (self.repo_host) 
     267        self.repositories = { 
     268            'myrep': Mock(reponame='myrep', scope='/', 
     269                           get_base=lambda: self.repo_base, 
     270                           get_path_url=lambda path, rev: 'https:%s/%s' % 
     271                           (self.repo_base_url, path.lstrip('/'))), 
     272            'my-devel-rep': Mock(reponame='my-devel-rep', scope='/devel/', 
     273                                  get_base=lambda: self.repo_base, 
     274                                  get_path_url=lambda path, rev: 
     275                                      'http:%s/devel/%s' % 
     276                                      (self.repo_base_url, path.lstrip('/'))), 
     277            'my-prod-rep': Mock(reponame='my-prod-rep', 
     278                                 scope='/devel/my-prod/', 
     279                                 get_base=lambda: self.repo_base, 
     280                                 get_path_url=lambda path, rev: 
     281                                      'svn+ssh:%s/devel/my-prod/%s' % 
     282                                      (self.repo_base_url, path.lstrip('/'))), 
     283            # Note 'my-dev-rep' purposefully has a scope that prefixes 
     284            # 'my-devel-rep'. This tests cases of false matching scopes. 
     285            'my-dev-rep': Mock(reponame='my-dev-rep', scope='/dev/', 
     286                                get_base=lambda: self.repo_base, 
     287                                get_path_url=lambda path, rev: '%s/dev/%s' % 
     288                                      (self.repo_base_url, path.lstrip('/'))), 
     289            # Extra repository, same base as above, without URL definition 
     290            'my-nourl-rep': Mock(reponame='my-nourl-rep', scope='/devel/', 
     291                                  get_base=lambda: self.repo_base, 
     292                                  get_path_url=lambda path, rev: None), 
     293            'otherrep': Mock(reponame='otherrep', scope='/', 
     294                              get_base=lambda: self.other_repo_base, 
     295                              get_path_url=lambda path, rev: 'svn:%s/%s' % 
     296                                (self.other_repo_base_url, path.lstrip('/'))), 
     297            } 
     298 
     299    def test_fully_qualified_and_scheme_relative_url(self): 
     300        external = {'repo': self.repositories['myrep'], 
     301                    'repopath': 'devel/my-prod/trunk/src/my-module', 
     302                    'dir': 'local', 
     303                    'rev': None} 
     304        req_scheme = random.choice(['http', 'https']) 
     305        req_host = 'local.server' 
     306        req_href = '%s://%s/trac/env/browser/myrep/devel/my-prod/'     \ 
     307                   'trunk/src/my-module' % (req_scheme, req_host) 
     308        use_req_href = random.choice([req_href] * 7 + [None, '', '/']) 
     309 
     310        ext_url = '//remote.server.org/third/party/library' 
     311        for scheme in ['', 'http:', 'https:', 'svn:', 'svn+ssh:']: 
     312            external['url'] = '%s%s' % (scheme, ext_url) 
     313            for reponame, repo in self.repositories.iteritems(): 
     314                expected_scheme = scheme 
     315                # Twists: 
     316                # - If URL is scheme-relative, and repository URL has scheme, 
     317                #   then take the scheme from the URL. 
     318                # - If URL is scheme-relative, and repository URL does not 
     319                #   have scheme, then take the scheme from the request. 
     320                if not scheme: 
     321                    repo_url = self.repositories[reponame]              \ 
     322                               .get_path_url('/', None) or '' 
     323                    if '://' in repo_url: 
     324                        # Inherit scheme from Repository URL 
     325                        expected_scheme = '%s:' % (splittype(repo_url)[0]) 
     326                    elif use_req_href and '://' in use_req_href: 
     327                        # Inherit scheme from request 
     328                        expected_scheme = '%s:' % (req_scheme) 
     329                self.assertEqual('%s%s' % (expected_scheme, ext_url), 
     330                                 svn_prop.get_external_url(external, repo, 
     331                                                       req_href=use_req_href)) 
     332 
     333    def test_server_relative_url(self): 
     334        external = {'repo': self.repositories['myrep'], 
     335                    'repopath': 'devel/my-prod/trunk/src/my-module', 
     336                    'dir': 'local', 
     337                    # External from myrep points to otherrep via server root! 
     338                    'url': '/svn/otherrep/dev', 
     339                    'rev': None} 
     340        req_scheme = random.choice(['http', 'https']) 
     341        req_host = 'local.server' 
     342        req_href = '%s://%s/trac/env/browser/myrep/devel/my-prod/'     \ 
     343                   'trunk/src/my-module' % (req_scheme, req_host) 
     344        use_req_href = random.choice([req_href] * 7 + [None, '', '/']) 
     345        use_req_href = req_href 
     346         
     347        for reponame, repo in self.repositories.iteritems(): 
     348            repo_url = self.repositories[reponame]       \ 
     349                       .get_path_url('/', None) or '' 
     350            expected_link = None 
     351            if repo_url: 
     352                expected_link = '//%s%s' % (self.repo_host, external['url']) 
     353                if '://' in repo_url: 
     354                    expected_link = '%s:%s' % (splittype(repo_url)[0], 
     355                                                expected_link) 
     356            elif (use_req_href or '').rstrip('/'): 
     357                expected_link = '%s://%s%s' % (req_scheme, req_host, 
     358                                                external['url']) 
     359            if expected_link: 
     360                self.assertEqual(expected_link, 
     361                                 svn_prop.get_external_url(external, repo, 
     362                                               req_href=use_req_href)) 
     363            else: 
     364                self.assertTrue(svn_prop.get_external_url(external, repo, 
     365                                               req_href=use_req_href) is None) 
     366     
     367    def test_repository_relative_url(self): 
     368        external = {'repo': self.repositories['myrep'], 
     369                    'repopath': 'devel/my-prod/trunk/src/my-module', 
     370                    'dir': 'local', 
     371                    'url': '^/devel/common/trunk/log', 
     372                    'rev': None} 
     373        scoped_external = {'repo': self.repositories['my-devel-rep'], 
     374                           # Note the missing 'devel/' in the repo-path 
     375                           'repopath': 'my-prod/trunk/src/my-module', 
     376                           'dir': 'local', 
     377                           'url': '^/devel/common/trunk/log', 
     378                           'rev': None} 
     379        deep_external = {'repo': self.repositories['my-devel-rep'], 
     380                         # Note the missing 'devel/my-prod/' in the repo-path 
     381                         'repopath': 'trunk/src/my-module', 
     382                         'dir': 'local', 
     383                         'url': '^/devel/common/trunk/log', 
     384                         'rev': None} 
     385 
     386        expected_link = '%s/devel/common/trunk/log' % (self.repo_base_url) 
     387        for ext in [external, scoped_external, deep_external]: 
     388            self.assertEqual('https:%s' % (expected_link), 
     389                             svn_prop.get_external_url(ext, 
     390                                  self.repositories['myrep'])) 
     391            self.assertEqual('http:%s' % (expected_link), 
     392                             svn_prop.get_external_url(ext, 
     393                                  self.repositories['my-devel-rep'])) 
     394            self.assertEqual('svn+ssh:%s' % (expected_link), 
     395                             svn_prop.get_external_url(ext, 
     396                                  self.repositories['my-prod-rep'])) 
     397            self.assertEqual('%s' % (expected_link), 
     398                             svn_prop.get_external_url(ext, 
     399                                  self.repositories['my-dev-rep'])) 
     400            self.assertTrue(svn_prop.get_external_url(ext, 
     401                                  self.repositories['my-nourl-rep']) is None) 
     402            self.assertTrue(svn_prop.get_external_url(ext, 
     403                                  self.repositories['otherrep']) is None) 
     404 
     405    def test_directory_relative_url(self): 
     406        external = {'repo': self.repositories['myrep'], 
     407                    'repopath': 'devel/my-prod/trunk/src/my-module', 
     408                    'dir': 'local', 
     409                    'url': '../../../../common/trunk/log', 
     410                    'rev': None} 
     411        scoped_external = {'repo': self.repositories['my-devel-rep'], 
     412                           # Note the missing 'devel/' in the repo-path 
     413                           'repopath': 'my-prod/trunk/src/my-module', 
     414                           'dir': 'local', 
     415                           'url': '../../../../common/trunk/log', 
     416                           'rev': None} 
     417        deep_external = {'repo': self.repositories['my-prod-rep'], 
     418                         # Note the missing 'devel/my-prod/' in the repo-path 
     419                         'repopath': 'trunk/src/my-module', 
     420                         'dir': 'local', 
     421                         # Going even deeper just for fun (and test of course) 
     422                         'url': '../../../../../devel/common/trunk/log', 
     423                         'rev': None} 
     424 
     425        expected_link = '%s/devel/common/trunk/log' % (self.repo_base_url) 
     426        for ext in [external, scoped_external, deep_external]: 
     427            self.assertEqual('https:%s' % (expected_link), 
     428                             svn_prop.get_external_url(ext, 
     429                                  self.repositories['myrep'])) 
     430            self.assertEqual('http:%s' % (expected_link), 
     431                             svn_prop.get_external_url(ext, 
     432                                  self.repositories['my-devel-rep'])) 
     433            self.assertEqual('svn+ssh:%s' % (expected_link), 
     434                             svn_prop.get_external_url(ext, 
     435                                  self.repositories['my-prod-rep'])) 
     436            self.assertEqual('%s' % (expected_link), 
     437                             svn_prop.get_external_url(ext, 
     438                                  self.repositories['my-dev-rep'])) 
     439            self.assertTrue(svn_prop.get_external_url(ext, 
     440                                  self.repositories['my-nourl-rep']) is None) 
     441            self.assertTrue(svn_prop.get_external_url(ext, 
     442                                  self.repositories['otherrep']) is None) 
     443 
     444    def test_invalid_directory_relative_url(self): 
     445        external = {'repo': self.repositories['myrep'], 
     446                    'repopath': 'devel/my-prod/trunk/src/my-module', 
     447                    'dir': 'local', 
     448                    # Out of repository base! aaah! 
     449                    'url': '../../../../../../common/trunk/log', 
     450                    'rev': None} 
     451        scoped_external = {'repo': self.repositories['my-devel-rep'], 
     452                           # Note the missing 'devel/' in the repo-path 
     453                           'repopath': 'my-prod/trunk/src/my-module', 
     454                           'dir': 'local', 
     455                           'url': '../../../../../../common/trunk/log', 
     456                           'rev': None} 
     457        deep_external = {'repo': self.repositories['my-prod-rep'], 
     458                        # Note the missing 'devel/my-prod/' in the repo-path 
     459                        'repopath': 'trunk/src/my-module', 
     460                        'dir': 'local', 
     461                        # Going even deeper just for fun (and test of course) 
     462                        'url': '../../../../../../../devel/common/trunk/log', 
     463                        'rev': None} 
     464         
     465        for ext in [external, scoped_external, deep_external]: 
     466            self.assertRaises(TracError, svn_prop.get_external_url, 
     467                              ext, self.repositories['myrep']) 
     468            self.assertRaises(TracError, svn_prop.get_external_url, 
     469                              ext, self.repositories['my-devel-rep']) 
     470            self.assertRaises(TracError, svn_prop.get_external_url, 
     471                              ext, self.repositories['my-prod-rep']) 
     472            self.assertTrue(svn_prop.get_external_url(ext, 
     473                              self.repositories['otherrep']) is None) 
     474 
     475class SvnExternalsMapTests(unittest.TestCase): 
     476 
     477    def setUp(self): 
     478        self.ini_section = { 
     479            '1': '//server/repos1 ' 
     480            'http://trac/proj/browser/Rep1/$path?rev=$rev', 
     481            '2': 'svn://server/repos2/ ' 
     482            'http://trac/proj/browser/Rep2/$path?rev=$rev', 
     483            '3': 'http://theirserver.org/svn/eng-soft ' 
     484            'http://ourserver/viewvc/svn/$path/?pathrev=25914', 
     485            '4': 'svn://anotherserver.com/tools_repository ' 
     486            '//ourserver/tracs/tools/browser/$path?rev=$rev', 
     487            # Testing existing partial match to 1 & 2 
     488            # and also Trac-env-relative pattern. 
     489            # Note that need to specify only repository name (no "/browser") 
     490            '5': '//server/repo Repo/$path?rev=$rev', 
     491            # But should work fine also with "/browser" specified 
     492            # Also testing here for a better (more specific) match than 5 
     493            '6': '//server/repo/sub-proj-a ' 
     494            '/browser/SubProjA/$path?rev=$rev' 
     495            } 
     496        self.map_from_ini = { 
     497            '//server/repos1': 
     498                'http://trac/proj/browser/Rep1/%(path)s?rev=%(rev)s', 
     499            'svn://server/repos2': 
     500                'http://trac/proj/browser/Rep2/%(path)s?rev=%(rev)s', 
     501            'http://theirserver.org/svn/eng-soft': 
     502                'http://ourserver/viewvc/svn/%(path)s/?pathrev=25914', 
     503            'svn://anotherserver.com/tools_repository': 
     504                '//ourserver/tracs/tools/browser/%(path)s?rev=%(rev)s', 
     505            '//server/repo': 'Repo/%(path)s?rev=%(rev)s', 
     506            '//server/repo/sub-proj-a': 
     507                'SubProjA/%(path)s?rev=%(rev)s' 
     508            } 
     509        self.log_messages = [] 
     510        self.log = Mock(debug=lambda msg: self.log_messages.append(msg)) 
     511 
     512    def test_parse_ini_externals_map(self): 
     513        ext_map = {} 
     514        svn_prop.parse_externals_map_ini(ext_map, 
     515                                         self.ini_section.iteritems(), 
     516                                         self.log) 
     517        # No skips 
     518        self.assertEqual([], self.log_messages) 
     519        # Could be done better using Python 3.1 assertDictEqual... 
     520        self.assertEqual(len(self.map_from_ini), len(ext_map)) 
     521        for key, value in self.map_from_ini.iteritems(): 
     522            self.assertTrue(key in ext_map) 
     523            self.assertEqual(value, ext_map[key]) 
     524 
     525    def test_null_pattern_link_expansion(self): 
     526        self.assertTrue(svn_prop.format_external_link( 
     527                                    ext_map={}, 
     528                                    link='http://server/repos1', 
     529                                    rev=None) is None) 
     530 
     531    def test_fully_qualified_pattern_link_expansion(self): 
     532        self.assertEqual('http://trac/proj/browser/Rep1/trunk?rev=%(rev)s', 
     533                         svn_prop.format_external_link( 
     534                            ext_map=self.map_from_ini, 
     535                            link='http://server/repos1/trunk')) 
     536        self.assertEqual('http://trac/proj/browser/Rep2/?rev=%(rev)s', 
     537                         svn_prop.format_external_link( 
     538                                    ext_map=self.map_from_ini, 
     539                                    link='svn://server/repos2')) 
     540        self.assertEqual('http://ourserver/viewvc/svn/trunk/?pathrev=25914', 
     541                         svn_prop.format_external_link( 
     542                           ext_map=self.map_from_ini, 
     543                           link='http://theirserver.org/svn/eng-soft/trunk/')) 
     544        self.assertEqual('//ourserver/tracs/tools/browser/branches/0.6-qa' 
     545                         '?rev=%(rev)s', 
     546                         svn_prop.format_external_link( 
     547                            ext_map=self.map_from_ini, 
     548                            link='svn://anotherserver.com/tools_repository/' 
     549                            'branches/0.6-qa')) 
     550 
     551    def test_trac_relative_link_expansions(self): 
     552        self.assertEqual('SubProjA/trunk/dev?rev=%(rev)s', 
     553                         svn_prop.format_external_link( 
     554                             ext_map=self.map_from_ini, 
     555                             link='//server/repo/sub-proj-a/trunk/dev')) 
     556        self.assertEqual('Repo/sub-proj-b/trunk?rev=%(rev)s', 
     557                         svn_prop.format_external_link( 
     558                             ext_map=self.map_from_ini, 
     559                             link='//server/repo/sub-proj-b/trunk')) 
     560 
     561    def test_bad_partial_match(self): 
     562        self.assertTrue(svn_prop.format_external_link( 
     563                                    ext_map=self.map_from_ini, 
     564                                    link='svn://server/rep', 
     565                                    rev=None) is None) 
     566 
     567    def test_bad_ini(self): 
     568        bad_ini_sections = [ { '1': '1-item-is-not-enough' }, 
     569                             { '1': '3 items-is too-much' }, ] 
     570        for section in bad_ini_sections: 
     571            ext_map = {} 
     572            self.log_messages = [] 
     573            svn_prop.parse_externals_map_ini(ext_map, section.iteritems(), 
     574                                             self.log) 
     575            self.assertEqual(1, len(self.log_messages)) 
     576            self.assertEqual('svn:externals entry 1 doesn\'t contain a ' 
     577                             'space-separated key value pair, skipping.', 
     578                             self.log_messages[0]) 
     579 
     580class SvnExternalsIntegrationTests(unittest.TestCase): 
     581 
     582    def setUp(self): 
     583        ini_section = { 
     584            '1': '//old-server:8000/svn/myrep ' 
     585            '/browser/myrep/$path?rev=$rev', 
     586            '2': 'svn://other-server/repo/sub-proj-a/ ' 
     587            '//ourserver/viewvc/svn/$path?pathrev=$rev' 
     588            } 
     589        self.ext_map = {} 
     590        svn_prop.parse_externals_map_ini(self.ext_map, ini_section.iteritems()) 
     591        self.repo_base = 'svn:random-uuid:/var/svn/repos' 
     592        self.repo_host = 'example.org' 
     593        self.repo_base_url = '//%s/svn/myrep' % (self.repo_host) 
     594        self.other_repo_base = 'svn:random-uuid:/var/svn/repos-2' 
     595        self.other_repo_base_url = '//%s/svn/otherrep' % (self.repo_host) 
     596        self.myrep = Mock(reponame='myrep', scope='/', 
     597                          get_base=lambda: self.repo_base, 
     598                          get_path_url=lambda path, rev: 'https:%s/%s' % 
     599                          (self.repo_base_url, path.lstrip('/'))) 
     600        self.mydevelrep = Mock(reponame='my-devel-rep', scope='/devel/', 
     601                               get_base=lambda: self.repo_base, 
     602                               get_path_url=lambda path, rev: 
     603                                   'http:%s/devel/%s' % 
     604                                   (self.repo_base_url, path.lstrip('/'))) 
     605        self.myprodrep = Mock(reponame='my-prod-rep', 
     606                              scope='/devel/my-prod/', 
     607                              get_base=lambda: self.repo_base, 
     608                              get_path_url=lambda path, rev: 
     609                                   'svn+ssh:%s/devel/my-prod/%s' % 
     610                                   (self.repo_base_url, path.lstrip('/'))) 
     611        self.mydevrep = Mock(reponame='my-dev-rep', scope='/dev/', 
     612                             get_base=lambda: self.repo_base, 
     613                             get_path_url=lambda path, rev: '%s/dev/%s' % 
     614                                   (self.repo_base_url, path.lstrip('/'))) 
     615        # Extra repository, same base as above, without URL definition 
     616        self.mynourlrep = Mock(reponame='my-nourl-rep', scope='/devel/', 
     617                               get_base=lambda: self.repo_base, 
     618                               get_path_url=lambda path, rev: None) 
     619        self.otherrep = Mock(reponame='otherrep', scope='/', 
     620                             get_base=lambda: self.other_repo_base, 
     621                             get_path_url=lambda path, rev: 'svn:%s/%s' % 
     622                               (self.other_repo_base_url, path.lstrip('/'))) 
     623 
     624        self.req_host = '//example.org' 
     625        self.req_href_no_scheme = '%s/trac/site' % (self.req_host) 
     626        self.req_scheme = random.choice(['http', 'https']) 
     627        self.req_href = '%s:%s' % (self.req_scheme, self.req_href_no_scheme) 
     628        self.ext_prop_str = """ 
     629            OldSyntax http://old-server:8000/svn/myrep/devel/common/trunk 
     630            http://example.org/svn/myrep/dev@1011 NewSyntax 
     631            //example.org/svn/myrep/devel/common/trunk SchemeRel 
     632            /svn/otherrep/dev ServerRel 
     633            -r137 ^/devel/common/trunk RepoRel 
     634            ../../../../devel/common/trunk/log DirRel 
     635            ../../../../dev FarDirRel 
     636            ../../../../../../../var/.htpasswd IllegalDirRel 
     637            -r 6 svn://other-server/repo/sub-proj-a/ RemoteProject 
     638            """ 
     639 
     640    def test_externals_on_one_unscoped_repository(self): 
     641        externals = svn_prop.parse_externals(self.ext_prop_str, self.myrep, 
     642                                             'devel/my-prod/trunk/src') 
     643        self.assertTrue(isinstance(externals, list)) 
     644        self.assertEqual(9, len(externals)) 
     645        self.assertEqual('OldSyntax', externals[0]['dir']) 
     646        self.assertEqual({'phase': 2, 'reponame': 'myrep', 
     647                          'href': 'devel/common/trunk', 'rev': None}, 
     648                         svn_prop.get_link(externals[0], [self.myrep], 
     649                                           self.req_href, self.ext_map)) 
     650        self.assertEqual('NewSyntax', externals[1]['dir']) 
     651        self.assertEqual({'phase': 2, 'reponame': 'myrep', 
     652                          'href': 'dev', 'rev': 1011}, 
     653                         svn_prop.get_link(externals[1], [self.myrep], 
     654                                           self.req_href, self.ext_map)) 
     655        self.assertEqual('SchemeRel', externals[2]['dir']) 
     656        self.assertEqual({'phase': 2, 'reponame': 'myrep', 
     657                          'href': 'devel/common/trunk', 'rev': None}, 
     658                         svn_prop.get_link(externals[2], [self.myrep], 
     659                                           self.req_href, self.ext_map)) 
     660        self.assertEqual('ServerRel', externals[3]['dir']) 
     661        self.assertEqual({'phase': 1, 'href': 'https:%s/svn/otherrep/dev' % 
     662                          (self.req_host), 'rev': None}, 
     663                         svn_prop.get_link(externals[3], [self.myrep], 
     664                                           self.req_href, self.ext_map)) 
     665        self.assertEqual('RepoRel', externals[4]['dir']) 
     666        self.assertEqual({'phase': 2, 'reponame': 'myrep', 
     667                          'href': 'devel/common/trunk', 'rev': 137}, 
     668                         svn_prop.get_link(externals[4], [self.myrep], 
     669                                           self.req_href, self.ext_map)) 
     670        self.assertEqual('DirRel', externals[5]['dir']) 
     671        self.assertEqual({'phase': 2, 'reponame': 'myrep', 
     672                          'href': 'devel/common/trunk/log', 'rev': None}, 
     673                         svn_prop.get_link(externals[5], [self.myrep], 
     674                                           self.req_href, self.ext_map)) 
     675        self.assertEqual('FarDirRel', externals[6]['dir']) 
     676        self.assertEqual({'phase': 2, 'reponame': 'myrep', 
     677                          'href': 'dev', 'rev': None}, 
     678                         svn_prop.get_link(externals[6], [self.myrep], 
     679                                           self.req_href, self.ext_map)) 
     680        self.assertEqual('IllegalDirRel', externals[7]['dir']) 
     681        self.assertRaises(TracError, svn_prop.get_link, externals[7], 
     682                                   [self.myrep], self.req_href, self.ext_map) 
     683        self.assertEqual('RemoteProject', externals[8]['dir']) 
     684        self.assertEqual({'phase': 1, 'href': '//ourserver/viewvc/svn/' 
     685                          '?pathrev=6', 'rev': 6}, 
     686                         svn_prop.get_link(externals[8], [self.myrep], 
     687                                           self.req_href, self.ext_map)) 
     688 
     689    def test_externals_on_one_scoped_repository(self): 
     690        externals = svn_prop.parse_externals(self.ext_prop_str, 
     691                                             self.mydevelrep, 
     692                                             'my-prod/trunk/src') 
     693        self.assertTrue(isinstance(externals, list)) 
     694        self.assertEqual(9, len(externals)) 
     695        self.assertEqual('OldSyntax', externals[0]['dir']) 
     696        self.assertEqual({'phase': 1, 'href': 'http://old-server:8000/svn' 
     697                          '/myrep/devel/common/trunk', 'rev': None}, 
     698                         svn_prop.get_link(externals[0], [self.mydevelrep], 
     699                                           self.req_href, self.ext_map)) 
     700        self.assertEqual('NewSyntax', externals[1]['dir']) 
     701        self.assertEqual({'phase': 1, 'rev': 1011,  
     702                          'href': 'http://example.org/svn/myrep/dev'}, 
     703                         svn_prop.get_link(externals[1], [self.mydevelrep], 
     704                                           self.req_href, self.ext_map)) 
     705        self.assertEqual('SchemeRel', externals[2]['dir']) 
     706        self.assertEqual({'phase': 2, 'reponame': 'my-devel-rep', 
     707                          'href': 'common/trunk', 'rev': None}, 
     708                         svn_prop.get_link(externals[2], [self.mydevelrep], 
     709                                           self.req_href, self.ext_map)) 
     710        self.assertEqual('ServerRel', externals[3]['dir']) 
     711        self.assertEqual({'phase': 1, 'href': 'http:%s/svn/otherrep/dev' % 
     712                          (self.req_host), 'rev': None}, 
     713                         svn_prop.get_link(externals[3], [self.mydevelrep], 
     714                                           self.req_href, self.ext_map)) 
     715        self.assertEqual('RepoRel', externals[4]['dir']) 
     716        self.assertEqual({'phase': 2, 'reponame': 'my-devel-rep', 
     717                          'href': 'common/trunk', 'rev': 137}, 
     718                         svn_prop.get_link(externals[4], [self.mydevelrep], 
     719                                           self.req_href, self.ext_map)) 
     720        self.assertEqual('DirRel', externals[5]['dir']) 
     721        self.assertEqual({'phase': 2, 'reponame': 'my-devel-rep', 
     722                          'href': 'common/trunk/log', 'rev': None}, 
     723                         svn_prop.get_link(externals[5], [self.mydevelrep], 
     724                                           self.req_href, self.ext_map)) 
     725        self.assertEqual('FarDirRel', externals[6]['dir']) 
     726        self.assertEqual({'phase': 1, 'href': 'http:%s/svn/myrep/dev' % 
     727                          (self.req_host), 'rev': None}, 
     728                         svn_prop.get_link(externals[6], [self.mydevelrep], 
     729                                       self.req_href, self.ext_map)) 
     730        self.assertEqual('IllegalDirRel', externals[7]['dir']) 
     731        self.assertRaises(TracError, svn_prop.get_link, externals[7], 
     732                               [self.mydevelrep], self.req_href, self.ext_map) 
     733        self.assertEqual('RemoteProject', externals[8]['dir']) 
     734        self.assertEqual({'phase': 1, 'href': '//ourserver/viewvc/svn/' 
     735                          '?pathrev=6', 'rev': 6}, 
     736                         svn_prop.get_link(externals[8], [self.myrep], 
     737                                           self.req_href, self.ext_map)) 
     738 
     739    def test_externals_on_several_scoped_crossing_repositories(self): 
     740        repo_list = [self.myprodrep, self.mydevelrep, 
     741                     self.mydevrep, self.otherrep] 
     742        externals = svn_prop.parse_externals(self.ext_prop_str, self.myprodrep, 
     743                                             'trunk/src') 
     744        self.assertTrue(isinstance(externals, list)) 
     745        self.assertEqual(9, len(externals)) 
     746        self.assertEqual('OldSyntax', externals[0]['dir']) 
     747        self.assertEqual({'phase': 1, 'href': 'http://old-server:8000/svn' 
     748                          '/myrep/devel/common/trunk', 'rev': None}, 
     749                         svn_prop.get_link(externals[0], repo_list, 
     750                                           self.req_href, self.ext_map)) 
     751        self.assertEqual('NewSyntax', externals[1]['dir']) 
     752        self.assertEqual({'phase': 1, 'rev': 1011,  
     753                          'href': 'http://example.org/svn/myrep/dev'}, 
     754                         svn_prop.get_link(externals[1], repo_list, 
     755                                           self.req_href, self.ext_map)) 
     756        self.assertEqual('SchemeRel', externals[2]['dir']) 
     757        self.assertEqual({'phase': 2, 'reponame': 'my-devel-rep', 
     758                          'href': 'common/trunk', 'rev': None}, 
     759                         svn_prop.get_link(externals[2], repo_list, 
     760                                           self.req_href, self.ext_map)) 
     761        self.assertEqual('ServerRel', externals[3]['dir']) 
     762        self.assertEqual({'phase': 2, 'reponame': 'otherrep', 
     763                          'href': 'dev', 'rev': None}, 
     764                         svn_prop.get_link(externals[3], repo_list, 
     765                                           self.req_href, self.ext_map)) 
     766        self.assertEqual('RepoRel', externals[4]['dir']) 
     767        self.assertEqual({'phase': 2, 'reponame': 'my-devel-rep', 
     768                          'href': 'common/trunk', 'rev': 137}, 
     769                         svn_prop.get_link(externals[4], repo_list, 
     770                                           self.req_href, self.ext_map)) 
     771        self.assertEqual('DirRel', externals[5]['dir']) 
     772        self.assertEqual({'phase': 2, 'reponame': 'my-devel-rep', 
     773                          'href': 'common/trunk/log', 'rev': None}, 
     774                         svn_prop.get_link(externals[5], repo_list, 
     775                                           self.req_href, self.ext_map)) 
     776        self.assertEqual('FarDirRel', externals[6]['dir']) 
     777        self.assertEqual({'phase': 2, 'reponame': 'my-dev-rep', 
     778                          'href': '/', 'rev': None}, 
     779                         svn_prop.get_link(externals[6], repo_list, 
     780                                       self.req_href, self.ext_map)) 
     781        self.assertEqual('IllegalDirRel', externals[7]['dir']) 
     782        self.assertRaises(TracError, svn_prop.get_link, externals[7], 
     783                               repo_list, self.req_href, self.ext_map) 
     784        self.assertEqual('RemoteProject', externals[8]['dir']) 
     785        self.assertEqual({'phase': 1, 'href': '//ourserver/viewvc/svn/' 
     786                          '?pathrev=6', 'rev': 6}, 
     787                         svn_prop.get_link(externals[8], [self.myrep], 
     788                                           self.req_href, self.ext_map)) 
     789 
     790    def test_externals_on_urlless_repository_with_root(self): 
     791        repo_list = [self.mynourlrep, self.myrep] 
     792        externals = svn_prop.parse_externals(self.ext_prop_str, 
     793                                             self.mynourlrep, 
     794                                             'my-prod/trunk/src') 
     795        self.assertTrue(isinstance(externals, list)) 
     796        self.assertEqual(9, len(externals)) 
     797        self.assertEqual('OldSyntax', externals[0]['dir']) 
     798        self.assertEqual({'phase': 2, 'reponame': 'myrep', 
     799                          'href': 'devel/common/trunk', 'rev': None}, 
     800                         svn_prop.get_link(externals[0], repo_list, 
     801                                           self.req_href, self.ext_map)) 
     802        self.assertEqual('NewSyntax', externals[1]['dir']) 
     803        self.assertEqual({'phase': 2, 'reponame': 'myrep', 
     804                          'href': 'dev', 'rev': 1011}, 
     805                         svn_prop.get_link(externals[1], repo_list, 
     806                                           self.req_href, self.ext_map)) 
     807        self.assertEqual('SchemeRel', externals[2]['dir']) 
     808        self.assertEqual({'phase': 2, 'reponame': 'myrep', 
     809                          'href': 'devel/common/trunk', 'rev': None}, 
     810                         svn_prop.get_link(externals[2], repo_list, 
     811                                           self.req_href, self.ext_map)) 
     812        self.assertEqual('ServerRel', externals[3]['dir']) 
     813        self.assertEqual({'phase': 1, 'href': '%s:%s/svn/otherrep/dev' % 
     814                          (self.req_scheme, self.req_host), 'rev': None}, 
     815                         svn_prop.get_link(externals[3], repo_list, 
     816                                           self.req_href, self.ext_map)) 
     817        self.assertEqual('RepoRel', externals[4]['dir']) 
     818        self.assertEqual({'phase': 2, 'reponame': 'my-nourl-rep', 
     819                          'href': 'common/trunk', 'rev': 137}, 
     820                         svn_prop.get_link(externals[4], repo_list, 
     821                                           self.req_href, self.ext_map)) 
     822        self.assertEqual('DirRel', externals[5]['dir']) 
     823        self.assertEqual({'phase': 2, 'reponame': 'my-nourl-rep', 
     824                          'href': 'common/trunk/log', 'rev': None}, 
     825                         svn_prop.get_link(externals[5], repo_list, 
     826                                           self.req_href, self.ext_map)) 
     827        self.assertEqual('FarDirRel', externals[6]['dir']) 
     828        self.assertEqual({'phase': 2, 'reponame': 'myrep', 
     829                          'href': 'dev', 'rev': None}, 
     830                         svn_prop.get_link(externals[6], repo_list, 
     831                                       self.req_href, self.ext_map)) 
     832        self.assertEqual('IllegalDirRel', externals[7]['dir']) 
     833        self.assertRaises(TracError, svn_prop.get_link, externals[7], 
     834                               repo_list, self.req_href, self.ext_map) 
     835        self.assertEqual('RemoteProject', externals[8]['dir']) 
     836        self.assertEqual({'phase': 1, 'href': '//ourserver/viewvc/svn/' 
     837                          '?pathrev=6', 'rev': 6}, 
     838                         svn_prop.get_link(externals[8], [self.myrep], 
     839                                           self.req_href, self.ext_map)) 
     840 
     841    def test_externals_on_urlless_repository_without_root(self): 
     842        repo_list = [self.mynourlrep, self.otherrep] 
     843        externals = svn_prop.parse_externals(self.ext_prop_str, 
     844                                             self.mynourlrep, 
     845                                             'my-prod/trunk/src') 
     846        self.assertTrue(isinstance(externals, list)) 
     847        self.assertEqual(9, len(externals)) 
     848        self.assertEqual('OldSyntax', externals[0]['dir']) 
     849        self.assertEqual({'phase': 1, 'href': 'http://old-server:8000/svn' 
     850                          '/myrep/devel/common/trunk', 'rev': None}, 
     851                         svn_prop.get_link(externals[0], repo_list, 
     852                                           self.req_href, self.ext_map)) 
     853        self.assertEqual('NewSyntax', externals[1]['dir']) 
     854        self.assertEqual({'phase': 1, 'rev': 1011,  
     855                          'href': 'http://example.org/svn/myrep/dev'}, 
     856                         svn_prop.get_link(externals[1], repo_list, 
     857                                           self.req_href, self.ext_map)) 
     858        self.assertEqual('SchemeRel', externals[2]['dir']) 
     859        self.assertEqual({'phase': 1, 'href': '%s://example.org/svn/myrep/' 
     860                          'devel/common/trunk' % self.req_scheme, 'rev': None}, 
     861                         svn_prop.get_link(externals[2], repo_list, 
     862                                           self.req_href, self.ext_map)) 
     863        self.assertEqual('ServerRel', externals[3]['dir']) 
     864        self.assertEqual({'phase': 2, 'reponame': 'otherrep', 
     865                          'href': 'dev', 'rev': None}, 
     866                         svn_prop.get_link(externals[3], repo_list, 
     867                                           self.req_href, self.ext_map)) 
     868        self.assertEqual('RepoRel', externals[4]['dir']) 
     869        self.assertEqual({'phase': 2, 'reponame': 'my-nourl-rep', 
     870                          'href': 'common/trunk', 'rev': 137}, 
     871                         svn_prop.get_link(externals[4], repo_list, 
     872                                           self.req_href, self.ext_map)) 
     873        self.assertEqual('DirRel', externals[5]['dir']) 
     874        self.assertEqual({'phase': 2, 'reponame': 'my-nourl-rep', 
     875                          'href': 'common/trunk/log', 'rev': None}, 
     876                         svn_prop.get_link(externals[5], repo_list, 
     877                                           self.req_href, self.ext_map)) 
     878        self.assertEqual('FarDirRel', externals[6]['dir']) 
     879        self.assertTrue(svn_prop.get_link(externals[6], repo_list, 
     880                                       self.req_href, self.ext_map) is None) 
     881        self.assertEqual('IllegalDirRel', externals[7]['dir']) 
     882        self.assertRaises(TracError, svn_prop.get_link, externals[7], 
     883                               repo_list, self.req_href, self.ext_map) 
     884        self.assertEqual('RemoteProject', externals[8]['dir']) 
     885        self.assertEqual({'phase': 1, 'href': '//ourserver/viewvc/svn/' 
     886                          '?pathrev=6', 'rev': 6}, 
     887                         svn_prop.get_link(externals[8], [self.myrep], 
     888                                           self.req_href, self.ext_map)) 
     889 
     890def suite(): 
     891    suite = unittest.TestSuite() 
     892    suite.addTest(unittest.makeSuite(SvnExternalsParserTests)) 
     893    suite.addTest(unittest.makeSuite(SvnExternalsRepositoryRelativeLinkTests)) 
     894    suite.addTest(unittest.makeSuite(SvnExternalsUrlBasedLinkTests)) 
     895    suite.addTest(unittest.makeSuite(SvnExternalsMapTests)) 
     896    suite.addTest(unittest.makeSuite(SvnExternalsIntegrationTests)) 
     897    return suite 
     898 
     899if __name__ == '__main__': 
     900    runner = unittest.TextTestRunner() 
     901    runner.run(suite()) 
  • svn_prop.py

    Property changes on: tests\svn_prop.py
    ___________________________________________________________________
    Added: svn:mime-type
       + text/x-python
    Added: svn:keywords
       + Author Date Id Rev URL
    Added: svn:eol-style
       + native
    
     
    1717#         Christian Boos <cboos@neuf.fr> 
    1818 
    1919import posixpath 
     20from urllib import splittype, splithost 
    2021 
    2122from genshi.builder import tag 
    2223 
     
    2728from trac.versioncontrol.web_ui.changeset import IPropertyDiffRenderer 
    2829from trac.util import Ranges, to_ranges 
    2930from trac.util.translation import _, tag_ 
     31from trac.web.href import Href 
    3032 
     33def remprefix(str, prefix): 
     34    "Removes `prefix` from `str` if `prefix` prefixes `str`." 
     35    if prefix and str.startswith(prefix): 
     36        return str[len(prefix):] 
     37    return str 
     38     
     39def remsuffix(str, suffix): 
     40    "Removes `suffix` from `str` if `suffix` suffixes `str`." 
     41    if suffix and str.endswith(suffix): 
     42        return str[:-len(suffix)] 
     43    return str 
     44                 
     45def parse_ext_line(prop_line, repo, repopath): 
     46    """Parse the svn:externals property line `prop_line` and return a 
     47    dictionary representing the content of the external reference. 
     48    Returns None for invalid external references or comments. 
     49    """ 
     50    prop_line = prop_line.strip().replace('\\', '/') 
     51    if not prop_line or prop_line.startswith('#'): 
     52        return None 
     53    elements = prop_line.split() 
     54    if len(elements) < 2: 
     55        return None 
     56     
     57    ext_dict = {'repo': repo, 'repopath': repopath} 
     58    ext_dict['rev'] = rev_str = None 
     59    if '://' in elements[-1]: 
     60        # Old-style syntax 
     61        ext_dict['dir'] = elements[0] 
     62        ext_dict['url'] = elements[-1] 
     63        if len(elements) >= 3: 
     64            rev_str = ''.join(elements[1:(len(elements) == 4 and 3 or 2)]) 
     65    else: 
     66        # New-style syntax 
     67        ext_dict['dir'] = elements[-1] 
     68        ext_dict['url'] = elements[-2] 
     69        if len(elements) >= 3: 
     70            rev_str = ''.join(elements[0:(len(elements) == 4 and 2 or 1)]) 
     71        elif '@' in ext_dict['url']: 
     72            ext_dict['url'], rev_str = ext_dict['url'].split('@') 
     73            rev_str = '-r%s' % (rev_str) 
     74     
     75    if rev_str: 
     76        if not rev_str.startswith('-r'): 
     77            return None 
     78        if not rev_str[2:].isdigit(): 
     79            return None 
     80        ext_dict['rev'] = int(rev_str[2:]) 
     81         
     82    return ext_dict 
    3183 
     84def parse_externals(prop_str, repo, repopath): 
     85    """Parse a svn:externals property string and return a list of external 
     86    references dictionaries from the valid lines. 
     87    """ 
     88    return filter(None, 
     89                  [parse_ext_line(line, repo, repopath) 
     90                   for line in prop_str.splitlines()]) 
     91 
     92def externals_generator(prop_str, repo, repopath): 
     93    """Same as parse_externals, only generator form.""" 
     94    for ext_line in prop_str.splitlines(): 
     95        ext = parse_ext_line(ext_line, repo, repopath) 
     96        if ext: 
     97            yield ext 
     98 
     99def _convert_dir_rel_to_repo_rel(dir_rel_path, base_repo_path, repo_scope): 
     100    """Traverse the repository path according to '../'-prefixed `dir_rel_path` 
     101    and return an equivalent repository-root-relative path. 
     102    Raise `TracError` if trying to traverse above repository base directory. 
     103    """ 
     104    up_count = dir_rel_path.count('../') 
     105    path_elements = filter(None, dir_rel_path.replace('../', '').split('/')) 
     106    base_elements = filter(None, 
     107                           repo_scope.split('/') + base_repo_path.split('/')) 
     108    if len(base_elements) < up_count: 
     109        raise TracError(_('Cannot traverse outside ' 
     110                          'repository base directory.')) 
     111    return '/'.join(['^'] + base_elements[0:-up_count] + path_elements) 
     112 
     113def get_repo_relative_path(ext_dict, dest_repo): 
     114    """Check whether external is within the repository named `reponame` and 
     115    return path (within `reponame`) to referenced external in case it is. 
     116    Otherwise return None. 
     117    """ 
     118    url = ext_dict['url'] 
     119    ext_repo = ext_dict['repo'] 
     120    if '://' in url: 
     121        # Fully qualified URL 
     122        return None 
     123    if ext_repo.get_base() != dest_repo.get_base(): 
     124        # Not same filesystem-repository 
     125        return None 
     126    if url.startswith('../'): 
     127        # Relative to directory 
     128        url = _convert_dir_rel_to_repo_rel(url, ext_dict['repopath'], 
     129                                           ext_repo.scope) 
     130    if url.startswith('^/'): 
     131        # Relative to repository root 
     132        repo_url = url[2:] 
     133        scope = dest_repo.scope.replace('\\', '/').strip('/') 
     134        scope += '/' if scope else '' 
     135        if repo_url.startswith(scope): 
     136            # Target path within current repository 
     137            # (scoped or not) - generate phase-2 link. 
     138            return Href(remprefix(repo_url, scope))() 
     139        scope = scope.strip('/') 
     140        if repo_url.strip('/') == scope: 
     141            return Href(remprefix(repo_url, scope))() 
     142 
     143def _extract_host_from_url(url): 
     144    scheme, path = splittype(url or '') 
     145    host, path = splithost(path or '') 
     146    return (host or '').lstrip('/') 
     147 
     148def get_external_url(ext_dict, dest_repo, req_href=None): 
     149    """Build a URL for an external definition. If definition already given 
     150    as URL, return it. Otherwise, use `reponame` URL property to build a URL 
     151    for the given external. If `reponame` has no URL defined, 
     152    or the external is not related to that repository, return None. 
     153     
     154    Returned URL is fully-qualified URL string, with or without scheme. 
     155    """ 
     156    url = ext_dict['url'] 
     157    if '://' in url: 
     158        # Fully qualified with scheme - it's the answer! 
     159        return url 
     160    #dest_repo = rm.get_repository(reponame) 
     161    repo_url = (dest_repo.get_path_url('/', None) or '').rstrip('/') 
     162    if url.startswith('//'): 
     163        # Relative to scheme - partial answer 
     164        link = url 
     165        if '://' in repo_url: 
     166            # Use repo_url scheme to extend the answer 
     167            link = '%s:%s' % (splittype(repo_url)[0], url) 
     168        elif req_href and '://' in req_href: 
     169            # Use request href scheme to extend the answer 
     170            link = '%s:%s' % (splittype(req_href)[0], url) 
     171        return link 
     172    ext_repo = ext_dict['repo'] #rm.get_repository(ext_dict['reponame']) 
     173    ext_repo_url = (ext_repo.get_path_url('/', None) or '').rstrip('/') 
     174    if url.startswith('/'): 
     175        # Relative to server root 
     176        dest_repo_host = _extract_host_from_url(repo_url) 
     177        ext_repo_host = _extract_host_from_url(ext_repo_url) 
     178        server_url = link = None 
     179        if dest_repo_host and (dest_repo_host == ext_repo_host or 
     180                               ext_repo.get_base() == dest_repo.get_base()): 
     181            # Repositories related and dest-repos has URL - take its host 
     182            link = '//%s%s' % (dest_repo_host, url) 
     183            server_url = repo_url 
     184        elif req_href: 
     185            # Crazy heuristic - hope that target SVN on same host as this Trac 
     186            link = '//%s%s' % (_extract_host_from_url(req_href), url) 
     187            server_url = req_href 
     188        if '://' in (server_url or ''): 
     189            return '%s:%s' % (splittype(server_url)[0], link) 
     190        return link 
     191    if ext_repo.get_base() != dest_repo.get_base(): 
     192        # Repositories are not related 
     193        return None 
     194    if not repo_url: 
     195        # No URL defined 
     196        return None 
     197    base_repo_url = remsuffix(repo_url, dest_repo.scope 
     198                              .replace('\\', '/').rstrip('/')) 
     199    if url.startswith('../'): 
     200        # Relative to directory 
     201        url = _convert_dir_rel_to_repo_rel(url, ext_dict['repopath'], 
     202                                           ext_repo.scope) 
     203    if url.startswith('^/'): 
     204        # Relative to repository root 
     205        return '%s/%s' % (base_repo_url.rstrip('/'), 
     206                           url[2:].lstrip('/')) 
     207 
     208def parse_externals_map_ini(ext_map, ini_section, log=None): 
     209    for dummy, key_value in ini_section: 
     210        key_value = key_value.split() 
     211        if len(key_value) != 2: 
     212            if log: 
     213                log.debug('svn:externals entry %s doesn\'t contain a ' 
     214                          'space-separated key value pair, skipping.' % dummy) 
     215            continue 
     216        key, value = key_value 
     217        if not '//' in value: 
     218            # Trac-env-relative link 
     219            value = remprefix(value.lstrip('/'), 'browser/') 
     220        ext_map[key.rstrip('/')] = value.replace('%', '%%')        \ 
     221                                   .replace('$path', '%(path)s')    \ 
     222                                   .replace('$rev', '%(rev)s') 
     223    return ext_map 
     224 
     225def format_external_link(ext_map, link, rev=None): 
     226    """Lookup the best available match for `link` in `ext_map` (dictionary 
     227    of externals) and use the map value to replace the link content. 
     228    Returns a string with the matched URL if a match was found, 
     229    otherwise - None. 
     230    """ 
     231    path_elements = [] 
     232    base_url = link.rstrip('/') 
     233    while base_url: 
     234        if base_url in ext_map: 
     235            path = '/'.join(reversed(path_elements)) 
     236            href = ext_map[base_url].replace('%(rev)s', '%%(rev)s') 
     237            return href % {'path': path} 
     238        scheme, url = splittype(base_url) 
     239        # A schemeless match is also good 
     240        if scheme and url and url in ext_map: 
     241            path = '/'.join(reversed(path_elements)) 
     242            href = ext_map[url].replace('%(rev)s', '%%(rev)s') 
     243            if href.startswith('//'): 
     244                href = '%s:%s' % (scheme, href) 
     245            return href % {'path': path} 
     246        base_url, suffix = posixpath.split(base_url) 
     247        if base_url and (base_url == '//' or base_url.endswith(':')): 
     248            break 
     249        path_elements.append(suffix) 
     250 
     251def get_link(ext, repo_list, req_href='', ext_map={}): 
     252    link_dict = {'phase': 0, 'rev': ext['rev']} 
     253    for repo in repo_list: 
     254        # Look for phase-2 link in repository list. 
     255        link = get_repo_relative_path(ext, repo) 
     256        if link: 
     257            link_dict['phase'] = 2 
     258            link_dict['reponame'] = repo.reponame 
     259            link_dict['href'] = link 
     260            return link_dict 
     261        # Maybe phase-1 link? 
     262        phase1_link = get_external_url(ext, repo, req_href) 
     263        if phase1_link: 
     264            # Try converting to phase-2 using repository as external-map 
     265            link = format_external_link( 
     266                        {splittype(repo.get_path_url('/', None) or '')[1] 
     267                         .rstrip('/'): '%(path)s'}, phase1_link) 
     268            if link: 
     269                link_dict['phase'] = 2 
     270                link_dict['reponame'] = repo.reponame 
     271                link_dict['href'] = link 
     272                return link_dict 
     273            elif 0 == link_dict['phase']: 
     274                # Keep the first hit as phase-1 link for later use 
     275                link_dict['phase'] = 1 
     276                link_dict['href'] = phase1_link 
     277    if 1 == link_dict['phase']: 
     278        # Look for conversion with the externals map 
     279        pre_map_link = link_dict['href'] 
     280        link = format_external_link(ext_map, pre_map_link) 
     281        if link: 
     282            link_dict['href'] = link % {'rev': ext['rev']} 
     283            # If link is intra-Trac, it is phase-2, and might begin with a 
     284            # repository name and contain '%(rev)s' pattern. 
     285            if '//' not in link: 
     286                link = remsuffix(link, '?rev=%(rev)s') 
     287                reponame = link.split('/')[0] 
     288                if reponame in [repo.reponame for repo in repo_list]: 
     289                    link = remprefix(link, reponame).lstrip('/') 
     290                    link_dict['phase'] = 2 
     291                    link_dict['reponame'] = reponame 
     292                    link_dict['href'] = link 
     293                else: 
     294                    # No such repository? 
     295                    link_dict['href'] = pre_map_link 
     296        return link_dict 
     297 
     298 
    32299class SubversionPropertyRenderer(Component): 
    33300    implements(IPropertyRenderer) 
    34301 
     
    45312     
    46313    def render_property(self, name, mode, context, props): 
    47314        if name == 'svn:externals': 
    48             return self._render_externals(props[name]) 
     315            return self._render_externals(props[name], context) 
    49316        elif name == 'svn:needs-lock': 
    50317            return self._render_needslock(context) 
    51318        elif name == 'svn:mergeinfo' or name.startswith('svnmerge-'): 
    52319            return self._render_mergeinfo(name, mode, context, props) 
    53320 
    54     def _render_externals(self, prop): 
     321    def _render_externals(self, prop, context): 
     322        rm = RepositoryManager(self.env) 
    55323        if not self._externals_map: 
    56             for dummykey, value in self.config.options('svn:externals'): 
    57                 value = value.split() 
    58                 if len(value) != 2: 
    59                     self.log.warn("svn:externals entry %s doesn't contain " 
    60                             "a space-separated key value pair, skipping.",  
    61                             dummykey) 
    62                     continue 
    63                 key, value = value 
    64                 self._externals_map[key] = value.replace('%', '%%') \ 
    65                                            .replace('$path', '%(path)s') \ 
    66                                            .replace('$rev', '%(rev)s') 
    67         externals = [] 
    68         for external in prop.splitlines(): 
    69             elements = external.split() 
    70             if not elements: 
    71                 continue 
    72             localpath, rev, url = elements[0], '', elements[-1] 
    73             if localpath.startswith('#'): 
    74                 externals.append((external, None, None, None, None)) 
    75                 continue 
    76             if len(elements) == 3: 
    77                 rev = elements[1] 
    78                 rev = rev.replace('-r', '') 
    79             # retrieve a matching entry in the externals map 
    80             prefix = [] 
    81             base_url = url 
    82             while base_url: 
    83                 if base_url in self._externals_map or base_url == u'/': 
    84                     break 
    85                 base_url, pref = posixpath.split(base_url) 
    86                 prefix.append(pref) 
    87             href = self._externals_map.get(base_url) 
    88             revstr = rev and ' at revision '+rev or '' 
    89             if not href and (url.startswith('http://') or  
    90                              url.startswith('https://')): 
    91                 href = url.replace('%', '%%') 
    92             if href: 
    93                 remotepath = '' 
    94                 if prefix: 
    95                     remotepath = posixpath.join(*reversed(prefix)) 
    96                 externals.append((localpath, revstr, base_url, remotepath, 
    97                                   href % {'path': remotepath, 'rev': rev})) 
     324            parse_externals_map_ini(self._externals_map, 
     325                                    self.config.options('svn:externals'), 
     326                                    self.log) 
     327 
     328        reponame = context.resource.parent.id 
     329        src_repo = rm.get_repository(reponame) 
     330        repopath = context.resource.id.replace('\\', '/').strip('/') 
     331        repolist = [src_repo] + [repo for repo in rm.get_real_repositories() 
     332                                               if repo.reponame != reponame] 
     333        req_href = context.req.abs_href('/') 
     334        trs = [] 
     335        for ext in externals_generator(prop, src_repo, repopath): 
     336            link = get_link(ext, repolist, req_href, self._externals_map) 
     337            if link: 
     338                if 2 == link['phase']: 
     339                    # We have intra-Trac link 
     340                    href = Href('/browser')(link['reponame'], link['href']) 
     341                    if ext['rev']: 
     342                        href = '%s?rev=%d' % (href, ext['rev']) 
     343                elif 1 == link['phase']: 
     344                    # The link is external to this Trac 
     345                    href = link['href'] 
     346                tr = tag.tr(tag.td(tag.a(ext['dir'], href=href, 
     347                                         title=_('Jump to external'))), 
     348                            tag.td(tag.a(ext['url'], href=href)), 
     349                            tag.td(tag.a(ext['rev'] or '', href=href))) 
    98350            else: 
    99                 externals.append((localpath, revstr, url, None, None)) 
    100         externals_data = [] 
    101         for localpath, rev, url, remotepath, href in externals: 
    102             label = localpath 
    103             if url is None: 
    104                 title = '' 
    105             elif href: 
    106                 if url: 
    107                     url = ' in ' + url 
    108                 label += rev + url 
    109                 title = ''.join((remotepath, rev, url)) 
    110             else: 
    111                 title = _('No svn:externals configured in trac.ini') 
    112             externals_data.append((label, href, title)) 
    113         return tag.ul([tag.li(tag.a(label, href=href, title=title)) 
    114                        for label, href, title in externals_data]) 
     351                tr = tag.tr(tag.td(tag.a(ext['dir'], title= 
     352                                         _('No matching external')), 
     353                            tag.td(ext['url']), 
     354                            tag.td(ext['rev'] or ''))) 
     355            trs.append(tr) 
     356        return tag.table(tag.tbody(trs)) 
    115357 
    116358    def _render_needslock(self, context): 
    117359        return tag.img(src=context.href.chrome('common/lock-locked.png'),