Edgewall Software

Ticket #7687: svn-externals-7687.4.patch

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

New patch (see comment:40)

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

    Property changes on: trac\versioncontrol\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 
    3144 
     45def parse_externals(prop_str, repo, repopath): 
     46    """Parse svn:externals property string and generate external 
     47    references dictionaries from the valid lines (skip invalid). 
     48    """ 
     49    for prop_line in prop_str.splitlines(): 
     50        prop_line = prop_line.strip().replace('\\', '/') 
     51        if not prop_line or prop_line.startswith('#'): 
     52            continue 
     53        elements = prop_line.split() 
     54        if len(elements) < 2: 
     55            continue 
     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        if rev_str: 
     75            if not rev_str.startswith('-r'): 
     76                continue 
     77            if not rev_str[2:].isdigit(): 
     78                continue 
     79            ext_dict['rev'] = int(rev_str[2:]) 
     80        if ext_dict: 
     81            yield ext_dict 
     82 
     83def _convert_dir_rel_to_repo_rel(dir_rel_path, base_repo_path, repo_scope): 
     84    """Traverse the repository path according to '../'-prefixed `dir_rel_path` 
     85    and return an equivalent repository-root-relative path. 
     86    Raise `TracError` if trying to traverse above repository base directory. 
     87    """ 
     88    up_count = dir_rel_path.count('../') 
     89    path_elements = filter(None, dir_rel_path.replace('../', '').split('/')) 
     90    base_elements = filter(None, 
     91                           repo_scope.split('/') + base_repo_path.split('/')) 
     92    if len(base_elements) < up_count: 
     93        raise TracError(_('Cannot traverse outside ' 
     94                          'repository base directory.')) 
     95    return '/'.join(['^'] + base_elements[0:-up_count] + path_elements) 
     96 
     97def get_repo_relative_path(ext_dict, dest_repo): 
     98    """Check whether external is within the repository named `reponame` and 
     99    return path (within `reponame`) to referenced external in case it is. 
     100    Otherwise return None. 
     101    """ 
     102    url = ext_dict['url'] 
     103    ext_repo = ext_dict['repo'] 
     104    if '://' in url: 
     105        # Fully qualified URL 
     106        return None 
     107    if ext_repo.get_base() != dest_repo.get_base(): 
     108        # Not same filesystem-repository 
     109        return None 
     110    if url.startswith('../'): 
     111        # Relative to directory 
     112        url = _convert_dir_rel_to_repo_rel(url, ext_dict['repopath'], 
     113                                           ext_repo.scope) 
     114    if url.startswith('^/'): 
     115        # Relative to repository root 
     116        repo_url = url[2:] 
     117        scope = dest_repo.scope.replace('\\', '/').strip('/') 
     118        scope += scope and '/' or '' 
     119        if repo_url.startswith(scope): 
     120            # Target path within current repository 
     121            # (scoped or not) - generate phase-2 link. 
     122            return Href(remprefix(repo_url, scope))() 
     123        scope = scope.strip('/') 
     124        if repo_url.strip('/') == scope: 
     125            return Href(remprefix(repo_url, scope))() 
     126 
     127def _extract_host_from_url(url): 
     128    scheme, path = splittype(url or '') 
     129    host, path = splithost(path or '') 
     130    return (host or '').lstrip('/') 
     131 
     132def get_external_url(ext_dict, dest_repo, req_href=None): 
     133    """Build a URL for an external definition. If definition already given 
     134    as URL, return it. Otherwise, use `reponame` URL property to build a URL 
     135    for the given external. If `reponame` has no URL defined, 
     136    or the external is not related to that repository, return None. 
     137     
     138    Returned URL is fully-qualified URL string, with or without scheme. 
     139    """ 
     140    url = ext_dict['url'] 
     141    if '://' in url: 
     142        # Fully qualified with scheme - it's the answer! 
     143        return url 
     144    repo_url = (dest_repo.get_path_url('/', None) or '').rstrip('/') 
     145    if url.startswith('//'): 
     146        # Relative to scheme - partial answer 
     147        link = url 
     148        if '://' in repo_url: 
     149            # Use repo_url scheme to extend the answer 
     150            link = '%s:%s' % (splittype(repo_url)[0], url) 
     151        elif req_href and '://' in req_href: 
     152            # Use request href scheme to extend the answer 
     153            link = '%s:%s' % (splittype(req_href)[0], url) 
     154        return link 
     155    ext_repo = ext_dict['repo'] 
     156    ext_repo_url = (ext_repo.get_path_url('/', None) or '').rstrip('/') 
     157    if url.startswith('/'): 
     158        # Relative to server root 
     159        dest_repo_host = _extract_host_from_url(repo_url) 
     160        ext_repo_host = _extract_host_from_url(ext_repo_url) 
     161        server_url = link = None 
     162        if dest_repo_host and (dest_repo_host == ext_repo_host or 
     163                               ext_repo.get_base() == dest_repo.get_base()): 
     164            # Repositories related and dest-repos has URL - take its host 
     165            link = '//%s%s' % (dest_repo_host, url) 
     166            server_url = repo_url 
     167        elif req_href: 
     168            # Crazy heuristic - hope that target SVN on same host as this Trac 
     169            link = '//%s%s' % (_extract_host_from_url(req_href), url) 
     170            server_url = req_href 
     171        if '://' in (server_url or ''): 
     172            return '%s:%s' % (splittype(server_url)[0], link) 
     173        return link 
     174    if ext_repo.get_base() != dest_repo.get_base(): 
     175        # Repositories are not related 
     176        return None 
     177    if not repo_url: 
     178        # No URL defined 
     179        return None 
     180    base_repo_url = remsuffix(repo_url, dest_repo.scope 
     181                              .replace('\\', '/').rstrip('/')) 
     182    if url.startswith('../'): 
     183        # Relative to directory 
     184        url = _convert_dir_rel_to_repo_rel(url, ext_dict['repopath'], 
     185                                           ext_repo.scope) 
     186    if url.startswith('^/'): 
     187        # Relative to repository root 
     188        return '%s/%s' % (base_repo_url.rstrip('/'), 
     189                           url[2:].lstrip('/')) 
     190 
     191def parse_externals_map_ini(ext_map, ini_section, log=None): 
     192    for dummy, key_value in ini_section: 
     193        key_value = key_value.split() 
     194        if len(key_value) != 2: 
     195            if log: 
     196                log.debug('svn:externals entry %s doesn\'t contain a ' 
     197                          'space-separated key value pair, skipping.' % dummy) 
     198            continue 
     199        key, value = key_value 
     200        if not '//' in value: 
     201            # Trac-env-relative link 
     202            #value = remprefix(value.lstrip('/'), 'browser/') 
     203            value = value.lstrip('/') 
     204        ext_map[key.rstrip('/')] = value.replace('%', '%%')        \ 
     205                                   .replace('$path', '%(path)s')    \ 
     206                                   .replace('$rev', '%(rev)s') 
     207    return ext_map 
     208 
     209def format_external_link(ext_map, link, rev=None): 
     210    """Lookup the best available match for `link` in `ext_map` (dictionary 
     211    of externals) and use the map value to replace the link content. 
     212    Returns a string with the matched URL if a match was found, 
     213    otherwise - None. 
     214    """ 
     215    path_elements = [] 
     216    base_url = link.rstrip('/') 
     217    while base_url: 
     218        if base_url in ext_map: 
     219            path = '/'.join(reversed(path_elements)) 
     220            href = ext_map[base_url].replace('%(rev)s', '%%(rev)s') 
     221            return href % {'path': path} 
     222        scheme, url = splittype(base_url) 
     223        # A schemeless match is also good 
     224        if scheme and url and url in ext_map: 
     225            path = '/'.join(reversed(path_elements)) 
     226            href = ext_map[url].replace('%(rev)s', '%%(rev)s') 
     227            if href.startswith('//'): 
     228                href = '%s:%s' % (scheme, href) 
     229            return href % {'path': path} 
     230        base_url, suffix = posixpath.split(base_url) 
     231        if base_url and (base_url == '//' or base_url.endswith(':')): 
     232            break 
     233        path_elements.append(suffix) 
     234 
     235def get_link(ext, repo_list, req_href='', ext_map={}): 
     236    """Get a link for an external reference `ext` and return link-dictionary. 
     237    Use the repositories in `repo_list` to get repo-relative links, 
     238    use the request href `req_href` for server-relative and scheme information, 
     239    and use `ext_map` to map URLs to other SVN-viewers. 
     240    """ 
     241    link_dict = {'phase': 0, 'rev': ext['rev']} 
     242    for repo in repo_list: 
     243        # Look for phase-2 link in repository list. 
     244        link = get_repo_relative_path(ext, repo) 
     245        if link: 
     246            link_dict['phase'] = 2 
     247            link_dict['reponame'] = repo.reponame 
     248            link_dict['href'] = link 
     249            return link_dict 
     250        # Maybe phase-1 link? 
     251        phase1_link = get_external_url(ext, repo, req_href) 
     252        if phase1_link: 
     253            # Try converting to phase-2 using repository as external-map 
     254            link = format_external_link( 
     255                        {splittype(repo.get_path_url('/', None) or '')[1] 
     256                         .rstrip('/'): '%(path)s'}, phase1_link) 
     257            if link: 
     258                link_dict['phase'] = 2 
     259                link_dict['reponame'] = repo.reponame 
     260                link_dict['href'] = link 
     261                return link_dict 
     262            elif 0 == link_dict['phase']: 
     263                # Keep the first hit as phase-1 link for later use 
     264                link_dict['phase'] = 1 
     265                link_dict['href'] = phase1_link 
     266    if 1 == link_dict['phase']: 
     267        # Look for conversion with the externals map 
     268        pre_map_link = link_dict['href'] 
     269        link = format_external_link(ext_map, pre_map_link) 
     270        if link: 
     271            link_dict['href'] = link % {'rev': ext['rev']} 
     272            # If link is intra-Trac, it is phase-2, and might begin with a 
     273            # repository name and contain '%(rev)s' pattern. 
     274            if '//' not in link: 
     275                elms = link.split('/') 
     276                reponame = elms[0] 
     277                if 'browser' == elms[0]: 
     278                    reponame = elms[1] 
     279                if reponame in [repo.reponame for repo in repo_list]: 
     280                    link = remprefix(link, 'browser/') 
     281                    link = remprefix(link, reponame).lstrip('/') 
     282                    link = remsuffix(link, '?rev=%(rev)s') 
     283                    link_dict['phase'] = 2 
     284                    link_dict['reponame'] = reponame 
     285                    link_dict['href'] = link 
     286                elif 'browser' == elms[0] and '' in [repo.reponame 
     287                                                     for repo in repo_list]: 
     288                    # Could be a phase-2 in the default repository. 
     289                    link = remprefix(link, 'browser/') 
     290                    link = remsuffix(link, '?rev=%(rev)s') 
     291                    link_dict['phase'] = 2 
     292                    link_dict['reponame'] = '' 
     293                    link_dict['href'] = link 
     294                else: 
     295                    # No such repository? Simply use to map item. 
     296                    link_dict['href'] = '/' + link % {'rev': ext['rev'] or ''} 
     297        return link_dict 
     298 
     299 
    32300class SubversionPropertyRenderer(Component): 
    33301    implements(IPropertyRenderer) 
    34302 
     
    45313     
    46314    def render_property(self, name, mode, context, props): 
    47315        if name == 'svn:externals': 
    48             return self._render_externals(props[name]) 
     316            return self._render_externals(props[name], context) 
    49317        elif name == 'svn:needs-lock': 
    50318            return self._render_needslock(context) 
    51319        elif name == 'svn:mergeinfo' or name.startswith('svnmerge-'): 
    52320            return self._render_mergeinfo(name, mode, context, props) 
    53321 
    54     def _render_externals(self, prop): 
     322    def _render_externals(self, prop, context): 
     323        rm = RepositoryManager(self.env) 
    55324        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})) 
     325            parse_externals_map_ini(self._externals_map, 
     326                                    self.config.options('svn:externals'), 
     327                                    self.log) 
     328 
     329        reponame = context.resource.parent.id 
     330        src_repo = rm.get_repository(reponame) 
     331        repopath = context.resource.id.replace('\\', '/').strip('/') 
     332        repolist = [src_repo] + [repo for repo in rm.get_real_repositories() 
     333                                               if repo.reponame != reponame] 
     334        req_href = context.req.abs_href('/') 
     335        trs = [] 
     336        for ext in parse_externals(prop, src_repo, repopath): 
     337            orig_url = ext['url'] 
     338            if '//' in orig_url or orig_url.startswith('/'): 
     339                orig_url_col = tag.a(ext['url'], href=ext['url']) 
    98340            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)) 
     341                orig_url_col = ext['url'] 
     342            link = get_link(ext, repolist, req_href, self._externals_map) 
     343            if link: 
     344                if 2 == link['phase']: 
     345                    # We have intra-Trac link 
     346                    hrepname = link['reponame'] and link['reponame'] or None 
     347                    href = context.req.href.browser(hrepname, link['href']) 
     348                    if ext['rev']: 
     349                        href += '?rev=%d' % (ext['rev']) 
     350                elif 1 == link['phase']: 
     351                    # The link is external to this Trac 
     352                    href = link['href'] 
     353                tr = tag.tr(tag.td(tag.a(ext['dir'], href=href, 
     354                                         title=_('Jump to external'))), 
     355                            tag.td(orig_url_col), 
     356                            tag.td(ext['rev'] or '')) 
    110357            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]) 
     358                tr = tag.tr(tag.td(tag.a(ext['dir'], title= 
     359                                         _('No matching external')), 
     360                            tag.td(orig_url_col), 
     361                            tag.td(ext['rev'] or ''))) 
     362            trs.append(tr) 
     363        return tag.table(tag.tbody(trs)) 
    115364 
    116365    def _render_needslock(self, context): 
    117366        return tag.img(src=context.href.chrome('common/lock-locked.png'),