Edgewall Software

Ticket #7687: svn-externals-7687-multirepos.4.patch

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

Fixed bug introduced by previous patch (rendered no-links) + cboos feedback

  • 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 test_no_externals(self): 
     28        externals_prop = u'' 
     29        externals_list = svn_prop.parse_externals(externals_prop, 
     30                    reponame=u'myrep', repopath=u'dev/trunk/src/my-module') 
     31        self.assertTrue(isinstance(externals_list, list)) 
     32        self.assertEqual(0, len(externals_list)) 
     33 
     34    def test_old_syntax_parsing(self): 
     35        externals_prop = u""" 
     36            simple http://example.org/svn/repos/dir1 
     37            # this is a comment that should be ignored. 
     38            spaceless_rev -r10 svn://example.org/svn/repos/dir2 
     39            spaced_rev -r 12 svn+ssh://example.org/svn/repos/dir3 
     40            """ 
     41        externals_list = svn_prop.parse_externals(externals_prop, 
     42                    reponame=u'myrep', repopath=u'dev/trunk/src/my-module') 
     43        self.assertTrue(isinstance(externals_list, list)) 
     44        self.assertEqual(3, len(externals_list)) 
     45        for ext_dict in externals_list: 
     46            self.assertTrue(isinstance(ext_dict, dict)) 
     47            self.assertEqual(ext_dict['reponame'], u'myrep') 
     48            self.assertEqual(ext_dict['repopath'], 
     49                             u'dev/trunk/src/my-module') 
     50        simple, spaceless, spaced = externals_list 
     51        self.assertEqual(simple['dir'], u'simple') 
     52        self.assertEqual(simple['url'], 
     53                         u'http://example.org/svn/repos/dir1') 
     54        self.assertTrue(simple['rev'] is None) 
     55        self.assertEqual(spaceless['dir'], u'spaceless_rev') 
     56        self.assertEqual(spaceless['url'], 
     57                         u'svn://example.org/svn/repos/dir2') 
     58        self.assertEqual(10, spaceless['rev']) 
     59        self.assertEqual(spaced['dir'], u'spaced_rev') 
     60        self.assertEqual(spaced['url'], 
     61                         u'svn+ssh://example.org/svn/repos/dir3') 
     62        self.assertEqual(12, spaced['rev']) 
     63 
     64    def test_new_syntax_parsing(self): 
     65        externals_prop = u""" 
     66            https://example.org/svn/repos/dir1 simple 
     67            -r10 ^/repos/dir2 spaceless_rev 
     68            -r 12 //example.org/svn/repos/dir3 spaced_rev 
     69            # this is a comment that should be ignored. 
     70            ../dir4@17 peg_rev 
     71            """ 
     72        externals_list = svn_prop.parse_externals(externals_prop, 
     73                    reponame=u'myrep', repopath=u'dev/trunk/src/my-module') 
     74        self.assertTrue(isinstance(externals_list, list)) 
     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['reponame'], u'myrep') 
     79            self.assertEqual(ext_dict['repopath'], 
     80                             u'dev/trunk/src/my-module') 
     81        simple, spaceless, spaced, pegged = externals_list 
     82        self.assertEqual(simple['dir'], u'simple') 
     83        self.assertEqual(simple['url'], 
     84                         u'https://example.org/svn/repos/dir1') 
     85        self.assertTrue(simple['rev'] is None) 
     86        self.assertEqual(spaceless['dir'], u'spaceless_rev') 
     87        self.assertEqual(spaceless['url'], 
     88                         u'^/repos/dir2') 
     89        self.assertEqual(10, spaceless['rev']) 
     90        self.assertEqual(spaced['dir'], u'spaced_rev') 
     91        self.assertEqual(spaced['url'], 
     92                         u'//example.org/svn/repos/dir3') 
     93        self.assertEqual(12, spaced['rev']) 
     94        self.assertEqual(pegged['dir'], u'peg_rev') 
     95        self.assertEqual(pegged['url'], 
     96                         u'../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 = u'svn:random-uuid:/var/svn/repos' 
     122        self.other_repo_base = u'svn:random-uuid:/var/svn/repos-2' 
     123        self.repositories = { 
     124            u'myrep': Mock(reponame=u'myrep', scope=u'/', 
     125                           get_base=lambda: self.repo_base, 
     126                           get_path_url=lambda path, rev: None), 
     127            u'my-devel-rep': Mock(reponame=u'my-devel-rep', scope=u'/devel/', 
     128                                   get_base=lambda: self.repo_base, 
     129                                   get_path_url=lambda path, rev: None), 
     130            u'my-prod-rep': Mock(reponame=u'my-prod-rep', 
     131                                 scope=u'/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            u'my-dev-rep': Mock(reponame=u'my-dev-rep', scope=u'/dev/', 
     137                                 get_base=lambda: self.repo_base, 
     138                                 get_path_url=lambda path, rev: None), 
     139            u'otherrep': Mock(reponame=u'otherrep', scope=u'/', 
     140                              get_base=lambda: self.other_repo_base, 
     141                              get_path_url=lambda path, rev: None), 
     142            } 
     143        self.rm = Mock(get_repository=lambda reponame: 
     144                       self.repositories[reponame]) 
     145 
     146    def test_repository_relative_url(self): 
     147        external = {u'reponame': 'myrep', 
     148                    u'repopath': u'devel/my-prod/trunk/src/my-module', 
     149                    u'dir': u'local', 
     150                    u'url': u'^/devel/common/trunk/log', 
     151                    u'rev': None} 
     152        scoped_external = {u'reponame': 'my-devel-rep', 
     153                           # Note the missing 'devel/' in the repo-path 
     154                           u'repopath': u'my-prod/trunk/src/my-module', 
     155                           u'dir': u'local', 
     156                           u'url': u'^/devel/common/trunk/log', 
     157                           u'rev': None} 
     158        deep_external = {u'reponame': 'my-devel-rep', 
     159                         # Note the missing 'devel/my-prod/' in the repo-path 
     160                         u'repopath': u'trunk/src/my-module', 
     161                         u'dir': u'local', 
     162                         u'url': u'^/devel/common/trunk/log', 
     163                         u'rev': None} 
     164         
     165        for ext in [external, scoped_external, deep_external]: 
     166            # Un-scoped repository asserts 
     167            repo = self.rm.get_repository(u'myrep') 
     168            for scope in [u'', u'/']: 
     169                repo.scope = scope 
     170                # External as seen from same repository 
     171                self.assertEqual(u'myrep/devel/common/trunk/log?rev=%(rev)s', 
     172                    svn_prop.get_repo_relative_path(ext, u'myrep', rm=self.rm)) 
     173            # Scoped repository asserts 
     174            repo = self.rm.get_repository(u'my-devel-rep') 
     175            for scope in [u'devel', u'/devel', u'devel/', u'/devel/']: 
     176                repo.scope = scope 
     177                # External as seen from the un-scoped repository 
     178                # Note the missing '/devel/' in the expected href 
     179                self.assertEqual(u'my-devel-rep/common/trunk/log?rev=%(rev)s', 
     180                        svn_prop.get_repo_relative_path(ext, u'my-devel-rep', 
     181                                                        rm=self.rm)) 
     182            # Deep-scoped repository (too deep for common) 
     183            self.assertTrue(svn_prop.get_repo_relative_path(ext, 
     184                                    u'my-prod-rep', rm=self.rm) is None) 
     185            # Differently-scoped repository (out of range) 
     186            self.assertTrue(svn_prop.get_repo_relative_path(ext, 
     187                                        u'my-dev-rep', rm=self.rm) is None) 
     188            # Another repository, not related to external 
     189            self.assertTrue(svn_prop.get_repo_relative_path(ext, u'otherrep', 
     190                                                        rm=self.rm) is None) 
     191 
     192    def test_directory_relative_url(self): 
     193        external = {u'reponame': u'myrep', 
     194                    u'repopath': u'devel/my-prod/trunk/src/my-module', 
     195                    u'dir': u'local', 
     196                    u'url': u'../../../../common/trunk/log', 
     197                    u'rev': None} 
     198        scoped_external = {u'reponame': u'my-devel-rep', 
     199                           # Note the missing 'devel/' in the repo-path 
     200                           u'repopath': u'my-prod/trunk/src/my-module', 
     201                           u'dir': u'local', 
     202                           u'url': u'../../../../common/trunk/log', 
     203                           u'rev': None} 
     204        deep_external = {u'reponame': u'my-prod-rep', 
     205                         # Note the missing 'devel/my-prod/' in the repo-path 
     206                         u'repopath': u'trunk/src/my-module', 
     207                         u'dir': u'local', 
     208                         # Going even deeper just for fun (and test of course) 
     209                         u'url': u'../../../../../devel/common/trunk/log', 
     210                         u'rev': None} 
     211 
     212        for ext in [external, scoped_external, deep_external]: 
     213            # Un-scoped repository asserts 
     214            self.assertEqual(u'myrep/devel/common/trunk/log?rev=%(rev)s', 
     215                             svn_prop.get_repo_relative_path(ext, u'myrep', 
     216                                                             rm=self.rm)) 
     217            # Scoped repository asserts 
     218            self.assertEqual(u'my-devel-rep/common/trunk/log?rev=%(rev)s', 
     219                             svn_prop.get_repo_relative_path(ext, 
     220                                                             u'my-devel-rep', 
     221                                                             rm=self.rm)) 
     222            # Deep-scoped repository asserts (out of reach) 
     223            self.assertTrue(svn_prop.get_repo_relative_path(ext, 
     224                                        u'my-prod-rep', rm=self.rm) is None) 
     225            # Unrelated repository asserts 
     226            self.assertTrue(svn_prop.get_repo_relative_path(ext, 
     227                                        u'otherrep', rm=self.rm) is None) 
     228 
     229    def test_invalid_directory_relative_url(self): 
     230        external = {u'reponame': u'myrep', 
     231                    u'repopath': u'devel/my-prod/trunk/src/my-module', 
     232                    u'dir': u'local', 
     233                    # Out of repository base! aaah! 
     234                    u'url': u'../../../../../../common/trunk/log', 
     235                    u'rev': None} 
     236        scoped_external = {u'reponame': u'my-devel-rep', 
     237                           # Note the missing 'devel/' in the repo-path 
     238                           u'repopath': u'my-prod/trunk/src/my-module', 
     239                           u'dir': u'local', 
     240                           u'url': u'../../../../../../common/trunk/log', 
     241                           u'rev': None} 
     242        deep_external = {u'reponame': u'my-prod-rep', 
     243                        # Note the missing 'devel/my-prod/' in the repo-path 
     244                        u'repopath': u'trunk/src/my-module', 
     245                        u'dir': u'local', 
     246                        # Going even deeper just for fun (and test of course) 
     247                        u'url': u'../../../../../../../devel/common/trunk/log', 
     248                        u'rev': None} 
     249         
     250        for ext in [external, scoped_external, deep_external]: 
     251            self.assertRaises(TracError, svn_prop.get_repo_relative_path, 
     252                              ext, u'myrep', rm=self.rm) 
     253            self.assertRaises(TracError, svn_prop.get_repo_relative_path, 
     254                              ext, u'my-devel-rep', rm=self.rm) 
     255            self.assertRaises(TracError, svn_prop.get_repo_relative_path, 
     256                              ext, u'my-prod-rep', rm=self.rm) 
     257            self.assertTrue(svn_prop.get_repo_relative_path(ext, u'otherrep', 
     258                                                        rm=self.rm) is None) 
     259 
     260class SvnExternalsUrlBasedLinkTests(unittest.TestCase): 
     261 
     262    def setUp(self): 
     263        self.repo_base = u'svn:random-uuid:/var/svn/repos' 
     264        self.repo_host = u'example.org' 
     265        self.repo_base_url = u'//%s/svn/myrep' % (self.repo_host) 
     266        self.other_repo_base = u'svn:random-uuid:/var/svn/repos-2' 
     267        self.other_repo_base_url = u'//%s/svn/otherrep' % (self.repo_host) 
     268        self.repositories = { 
     269            u'myrep': Mock(reponame=u'myrep', scope=u'/', 
     270                           get_base=lambda: self.repo_base, 
     271                           get_path_url=lambda path, rev: u'https:%s/%s' % 
     272                           (self.repo_base_url, path.lstrip(u'/'))), 
     273            u'my-devel-rep': Mock(reponame=u'my-devel-rep', scope=u'/devel/', 
     274                                  get_base=lambda: self.repo_base, 
     275                                  get_path_url=lambda path, rev: 
     276                                      u'http:%s/devel/%s' % 
     277                                      (self.repo_base_url, path.lstrip(u'/'))), 
     278            u'my-prod-rep': Mock(reponame=u'my-prod-rep', 
     279                                 scope=u'/devel/my-prod/', 
     280                                 get_base=lambda: self.repo_base, 
     281                                 get_path_url=lambda path, rev: 
     282                                      u'svn+ssh:%s/devel/my-prod/%s' % 
     283                                      (self.repo_base_url, path.lstrip(u'/'))), 
     284            # Note 'my-dev-rep' purposefully has a scope that prefixes 
     285            # 'my-devel-rep'. This tests cases of false matching scopes. 
     286            u'my-dev-rep': Mock(reponame=u'my-dev-rep', scope=u'/dev/', 
     287                                get_base=lambda: self.repo_base, 
     288                                get_path_url=lambda path, rev: u'%s/dev/%s' % 
     289                                      (self.repo_base_url, path.lstrip(u'/'))), 
     290            # Extra repository, same base as above, without URL definition 
     291            u'my-nourl-rep': Mock(reponame=u'my-nourl-rep', scope=u'/devel/', 
     292                                  get_base=lambda: self.repo_base, 
     293                                  get_path_url=lambda path, rev: None), 
     294            u'otherrep': Mock(reponame=u'otherrep', scope=u'/', 
     295                              get_base=lambda: self.other_repo_base, 
     296                              get_path_url=lambda path, rev: u'svn:%s/%s' % 
     297                                (self.other_repo_base_url, path.lstrip(u'/'))), 
     298            } 
     299        self.rm = Mock(get_repository=lambda reponame: 
     300                       self.repositories[reponame]) 
     301 
     302    def test_fully_qualified_and_scheme_relative_url(self): 
     303        external = {u'reponame': 'myrep', 
     304                    u'repopath': u'devel/my-prod/trunk/src/my-module', 
     305                    u'dir': u'local', 
     306                    u'rev': None} 
     307        req_scheme = random.choice([u'http', u'https']) 
     308        req_host = u'local.server' 
     309        req_href = u'%s://%s/trac/env/browser/myrep/devel/my-prod/'     \ 
     310                   'trunk/src/my-module' % (req_scheme, req_host) 
     311        use_req_href = random.choice([req_href] * 7 + [None, u'', u'/']) 
     312 
     313        ext_url = u'//remote.server.org/third/party/library' 
     314        for scheme in [u'', u'http:', u'https:', u'svn:', u'svn+ssh:']: 
     315            external[u'url'] = u'%s%s' % (scheme, ext_url) 
     316            for reponame in self.repositories.keys(): 
     317                expected_scheme = scheme 
     318                # Twists: 
     319                # - If URL is scheme-relative, and repository URL has scheme, 
     320                #   then take the scheme from the URL. 
     321                # - If URL is scheme-relative, and repository URL does not 
     322                #   have scheme, then take the scheme from the request. 
     323                if not scheme: 
     324                    repo_url = self.rm.get_repository(reponame)             \ 
     325                               .get_path_url(u'/', None) or u'' 
     326                    if u'://' in repo_url: 
     327                        # Inherit scheme from Repository URL 
     328                        expected_scheme = u'%s:' % (splittype(repo_url)[0]) 
     329                    elif use_req_href and u'://' in use_req_href: 
     330                        # Inherit scheme from request 
     331                        expected_scheme = u'%s:' % (req_scheme) 
     332                self.assertEqual(u'%s%s' % (expected_scheme, ext_url), 
     333                                 svn_prop.get_external_url(external, reponame, 
     334                                           rm=self.rm, req_href=use_req_href)) 
     335 
     336    def test_server_relative_url(self): 
     337        external = {u'reponame': 'myrep', 
     338                    u'repopath': u'devel/my-prod/trunk/src/my-module', 
     339                    u'dir': u'local', 
     340                    # External from myrep points to otherrep via server root! 
     341                    u'url': u'/svn/otherrep/dev', 
     342                    u'rev': None} 
     343        req_scheme = random.choice([u'http', u'https']) 
     344        req_host = u'local.server' 
     345        req_href = u'%s://%s/trac/env/browser/myrep/devel/my-prod/'     \ 
     346                   'trunk/src/my-module' % (req_scheme, req_host) 
     347        use_req_href = random.choice([req_href] * 7 + [None, u'', u'/']) 
     348        use_req_href = req_href 
     349         
     350        for reponame in self.repositories.keys(): 
     351            repo_url = self.rm.get_repository(reponame)     \ 
     352                       .get_path_url(u'/', None) or u'' 
     353            expected_link = None 
     354            if repo_url: 
     355                expected_link = u'//%s%s' % (self.repo_host, external[u'url']) 
     356                if u'://' in repo_url: 
     357                    expected_link = u'%s:%s' % (splittype(repo_url)[0], 
     358                                                expected_link) 
     359            elif (use_req_href or u'').rstrip(u'/'): 
     360                expected_link = u'%s://%s%s' % (req_scheme, req_host, 
     361                                                external[u'url']) 
     362            if expected_link: 
     363                self.assertEqual(expected_link, 
     364                                 svn_prop.get_external_url(external, reponame, 
     365                                           rm=self.rm, req_href=use_req_href)) 
     366            else: 
     367                self.assertTrue(svn_prop.get_external_url(external, reponame, 
     368                                   rm=self.rm, req_href=use_req_href) is None) 
     369     
     370    def test_repository_relative_url(self): 
     371        external = {u'reponame': 'myrep', 
     372                    u'repopath': u'devel/my-prod/trunk/src/my-module', 
     373                    u'dir': u'local', 
     374                    u'url': u'^/devel/common/trunk/log', 
     375                    u'rev': None} 
     376        scoped_external = {u'reponame': 'my-devel-rep', 
     377                           # Note the missing 'devel/' in the repo-path 
     378                           u'repopath': u'my-prod/trunk/src/my-module', 
     379                           u'dir': u'local', 
     380                           u'url': u'^/devel/common/trunk/log', 
     381                           u'rev': None} 
     382        deep_external = {u'reponame': 'my-devel-rep', 
     383                         # Note the missing 'devel/my-prod/' in the repo-path 
     384                         u'repopath': u'trunk/src/my-module', 
     385                         u'dir': u'local', 
     386                         u'url': u'^/devel/common/trunk/log', 
     387                         u'rev': None} 
     388 
     389        expected_link = u'%s/devel/common/trunk/log' % (self.repo_base_url) 
     390        for ext in [external, scoped_external, deep_external]: 
     391            self.assertEqual(u'https:%s' % (expected_link), 
     392                             svn_prop.get_external_url(ext, 
     393                                           u'myrep', rm=self.rm)) 
     394            self.assertEqual(u'http:%s' % (expected_link), 
     395                             svn_prop.get_external_url(ext, 
     396                                           u'my-devel-rep', rm=self.rm)) 
     397            self.assertEqual(u'svn+ssh:%s' % (expected_link), 
     398                             svn_prop.get_external_url(ext, 
     399                                           u'my-prod-rep', rm=self.rm)) 
     400            self.assertEqual(u'%s' % (expected_link), 
     401                             svn_prop.get_external_url(ext, 
     402                                           u'my-dev-rep', rm=self.rm)) 
     403            self.assertTrue(svn_prop.get_external_url(ext, u'my-nourl-rep', 
     404                                                      rm=self.rm) is None) 
     405            self.assertTrue(svn_prop.get_external_url(ext, u'otherrep', 
     406                                                      rm=self.rm) is None) 
     407 
     408    def test_directory_relative_url(self): 
     409        external = {u'reponame': u'myrep', 
     410                    u'repopath': u'devel/my-prod/trunk/src/my-module', 
     411                    u'dir': u'local', 
     412                    u'url': u'../../../../common/trunk/log', 
     413                    u'rev': None} 
     414        scoped_external = {u'reponame': u'my-devel-rep', 
     415                           # Note the missing 'devel/' in the repo-path 
     416                           u'repopath': u'my-prod/trunk/src/my-module', 
     417                           u'dir': u'local', 
     418                           u'url': u'../../../../common/trunk/log', 
     419                           u'rev': None} 
     420        deep_external = {u'reponame': u'my-prod-rep', 
     421                         # Note the missing 'devel/my-prod/' in the repo-path 
     422                         u'repopath': u'trunk/src/my-module', 
     423                         u'dir': u'local', 
     424                         # Going even deeper just for fun (and test of course) 
     425                         u'url': u'../../../../../devel/common/trunk/log', 
     426                         u'rev': None} 
     427 
     428        expected_link = u'%s/devel/common/trunk/log' % (self.repo_base_url) 
     429        for ext in [external, scoped_external, deep_external]: 
     430            self.assertEqual(u'https:%s' % (expected_link), 
     431                             svn_prop.get_external_url(ext, 
     432                                           u'myrep', rm=self.rm)) 
     433            self.assertEqual(u'http:%s' % (expected_link), 
     434                             svn_prop.get_external_url(ext, 
     435                                           u'my-devel-rep', rm=self.rm)) 
     436            self.assertEqual(u'svn+ssh:%s' % (expected_link), 
     437                             svn_prop.get_external_url(ext, 
     438                                           u'my-prod-rep', rm=self.rm)) 
     439            self.assertEqual(u'%s' % (expected_link), 
     440                             svn_prop.get_external_url(ext, 
     441                                           u'my-dev-rep', rm=self.rm)) 
     442            self.assertTrue(svn_prop.get_external_url(ext, u'my-nourl-rep', 
     443                                                      rm=self.rm) is None) 
     444            self.assertTrue(svn_prop.get_external_url(ext, u'otherrep', 
     445                                                      rm=self.rm) is None) 
     446 
     447    def test_invalid_directory_relative_url(self): 
     448        external = {u'reponame': u'myrep', 
     449                    u'repopath': u'devel/my-prod/trunk/src/my-module', 
     450                    u'dir': u'local', 
     451                    # Out of repository base! aaah! 
     452                    u'url': u'../../../../../../common/trunk/log', 
     453                    u'rev': None} 
     454        scoped_external = {u'reponame': u'my-devel-rep', 
     455                           # Note the missing 'devel/' in the repo-path 
     456                           u'repopath': u'my-prod/trunk/src/my-module', 
     457                           u'dir': u'local', 
     458                           u'url': u'../../../../../../common/trunk/log', 
     459                           u'rev': None} 
     460        deep_external = {u'reponame': u'my-prod-rep', 
     461                        # Note the missing 'devel/my-prod/' in the repo-path 
     462                        u'repopath': u'trunk/src/my-module', 
     463                        u'dir': u'local', 
     464                        # Going even deeper just for fun (and test of course) 
     465                        u'url': u'../../../../../../../devel/common/trunk/log', 
     466                        u'rev': None} 
     467         
     468        for ext in [external, scoped_external, deep_external]: 
     469            self.assertRaises(TracError, svn_prop.get_external_url, 
     470                              ext, u'myrep', rm=self.rm) 
     471            self.assertRaises(TracError, svn_prop.get_external_url, 
     472                              ext, u'my-devel-rep', rm=self.rm) 
     473            self.assertRaises(TracError, svn_prop.get_external_url, 
     474                              ext, u'my-prod-rep', rm=self.rm) 
     475            self.assertTrue(svn_prop.get_external_url(ext, u'otherrep', 
     476                                                    rm=self.rm) is None) 
     477 
     478class SvnExternalsMapTests(unittest.TestCase): 
     479 
     480    def setUp(self): 
     481        self.ini_section = { 
     482            '1': u'//server/repos1 ' 
     483            u'http://trac/proj/browser/Rep1/$path?rev=$rev', 
     484            '2': u'svn://server/repos2/ ' 
     485            u'http://trac/proj/browser/Rep2/$path?rev=$rev', 
     486            '3': u'http://theirserver.org/svn/eng-soft ' 
     487            u'http://ourserver/viewvc/svn/$path/?pathrev=25914', 
     488            '4': u'svn://anotherserver.com/tools_repository ' 
     489            u'//ourserver/tracs/tools/browser/$path?rev=$rev', 
     490            # Testing existing partial match to 1 & 2 
     491            # and also Trac-env-relative pattern. 
     492            # Note that need to specify only repository name (no "/browser") 
     493            '5': u'//server/repo Repo/$path?rev=$rev', 
     494            # But should work fine also with "/browser" specified 
     495            # Also testing here for a better (more specific) match than 5 
     496            '6': u'//server/repo/sub-proj-a ' 
     497            u'/browser/SubProjA/$path?rev=$rev' 
     498            } 
     499        self.map_from_ini = { 
     500            u'//server/repos1': 
     501                u'http://trac/proj/browser/Rep1/%(path)s?rev=%(rev)s', 
     502            u'svn://server/repos2': 
     503                u'http://trac/proj/browser/Rep2/%(path)s?rev=%(rev)s', 
     504            u'http://theirserver.org/svn/eng-soft': 
     505                u'http://ourserver/viewvc/svn/%(path)s/?pathrev=25914', 
     506            u'svn://anotherserver.com/tools_repository': 
     507                u'//ourserver/tracs/tools/browser/%(path)s?rev=%(rev)s', 
     508            u'//server/repo': u'Repo/%(path)s?rev=%(rev)s', 
     509            u'//server/repo/sub-proj-a': 
     510                u'SubProjA/%(path)s?rev=%(rev)s' 
     511            } 
     512        self.log_messages = [] 
     513        self.log = Mock(debug=lambda msg: self.log_messages.append(msg)) 
     514 
     515    def test_parse_ini_externals_map(self): 
     516        ext_map = {} 
     517        svn_prop.parse_externals_map_ini(ext_map, 
     518                                         self.ini_section.iteritems(), 
     519                                         self.log) 
     520        # No skips 
     521        self.assertEqual([], self.log_messages) 
     522        # Could be done better using Python 3.1 assertDictEqual... 
     523        self.assertEqual(len(self.map_from_ini), len(ext_map)) 
     524        for key, value in self.map_from_ini.iteritems(): 
     525            self.assertTrue(key in ext_map) 
     526            self.assertEqual(value, ext_map[key]) 
     527 
     528    def test_null_pattern_link_expansion(self): 
     529        self.assertTrue(svn_prop.format_external_link( 
     530                                    ext_map={}, 
     531                                    link=u'http://server/repos1', 
     532                                    rev=None) is None) 
     533 
     534    def test_fully_qualified_pattern_link_expansion(self): 
     535        self.assertEqual(u'http://trac/proj/browser/Rep2/?rev=%(rev)s', 
     536                         svn_prop.format_external_link( 
     537                                    ext_map=self.map_from_ini, 
     538                                    link=u'svn://server/repos2')) 
     539        self.assertEqual(u'http://ourserver/viewvc/svn/trunk/?pathrev=25914', 
     540                         svn_prop.format_external_link( 
     541                           ext_map=self.map_from_ini, 
     542                           link=u'http://theirserver.org/svn/eng-soft/trunk/')) 
     543        self.assertEqual(u'//ourserver/tracs/tools/browser/branches/0.6-qa' 
     544                         '?rev=%(rev)s', 
     545                         svn_prop.format_external_link( 
     546                            ext_map=self.map_from_ini, 
     547                            link=u'svn://anotherserver.com/tools_repository/' 
     548                            'branches/0.6-qa')) 
     549 
     550    def test_trac_relative_link_expansions(self): 
     551        self.assertEqual(u'SubProjA/trunk/dev?rev=%(rev)s', 
     552                         svn_prop.format_external_link( 
     553                             ext_map=self.map_from_ini, 
     554                             link=u'//server/repo/sub-proj-a/trunk/dev')) 
     555        self.assertEqual(u'Repo/sub-proj-b/trunk?rev=%(rev)s', 
     556                         svn_prop.format_external_link( 
     557                             ext_map=self.map_from_ini, 
     558                             link=u'//server/repo/sub-proj-b/trunk')) 
     559 
     560    def test_bad_partial_match(self): 
     561        self.assertTrue(svn_prop.format_external_link( 
     562                                    ext_map=self.map_from_ini, 
     563                                    link=u'svn://server/rep', 
     564                                    rev=None) is None) 
     565 
     566    def test_bad_ini(self): 
     567        bad_ini_sections = [ { '1': u'1-item-is-not-enough' }, 
     568                             { '1': u'3 items-is too-much' }, ] 
     569        for section in bad_ini_sections: 
     570            ext_map = {} 
     571            self.log_messages = [] 
     572            svn_prop.parse_externals_map_ini(ext_map, section.iteritems(), 
     573                                             self.log) 
     574            self.assertEqual(1, len(self.log_messages)) 
     575            self.assertEqual(u'svn:externals entry 1 doesn\'t contain a ' 
     576                             'space-separated key value pair, skipping.', 
     577                             self.log_messages[0]) 
     578 
     579def suite(): 
     580    suite = unittest.TestSuite() 
     581    suite.addTest(unittest.makeSuite(SvnExternalsParserTests)) 
     582    suite.addTest(unittest.makeSuite(SvnExternalsRepositoryRelativeLinkTests)) 
     583    suite.addTest(unittest.makeSuite(SvnExternalsUrlBasedLinkTests)) 
     584    suite.addTest(unittest.makeSuite(SvnExternalsMapTests)) 
     585    return suite 
     586 
     587if __name__ == '__main__': 
     588    runner = unittest.TextTestRunner() 
     589    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 
     44                 
     45def parse_ext_line(prop_line, reponame, 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 = {u'reponame': reponame, u'repopath': repopath} 
     58    ext_dict[u'rev'] = rev_str = None 
     59    if u'://' in elements[-1]: 
     60        # Old-style syntax 
     61        ext_dict[u'dir'] = elements[0] 
     62        ext_dict[u'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[u'dir'] = elements[-1] 
     68        ext_dict[u'url'] = elements[-2] 
     69        if len(elements) >= 3: 
     70            rev_str = ''.join(elements[0:(len(elements) == 4 and 2 or 1)]) 
     71        elif u'@' in ext_dict[u'url']: 
     72            ext_dict[u'url'], rev_str = ext_dict[u'url'].split('@') 
     73            rev_str = u'-r%s' % (rev_str) 
     74     
     75    if rev_str: 
     76        if not rev_str.startswith(u'-r'): 
     77            return None 
     78        if not rev_str[2:].isdigit(): 
     79            return None 
     80        ext_dict[u'rev'] = int(rev_str[2:]) 
     81         
     82    return ext_dict 
    3183 
     84def parse_externals(prop_str, reponame, 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, reponame, repopath) 
     90                   for line in prop_str.splitlines()]) 
     91 
     92def externals_generator(prop_str, reponame, repopath): 
     93    """Same as parse_externals, only generator form.""" 
     94    for ext_line in prop_str.splitlines(): 
     95        ext = parse_ext_line(ext_line, reponame, 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(u'../') 
     105    path_elements = filter(None, dir_rel_path.replace(u'../', u'').split(u'/')) 
     106    base_elements = filter(None, 
     107                           repo_scope.split(u'/') + base_repo_path.split(u'/')) 
     108    if len(base_elements) < up_count: 
     109        raise TracError(_(u'Cannot traverse outside ' 
     110                          'repository base directory.')) 
     111    return u'/'.join([u'^'] + base_elements[0:-up_count] + path_elements) 
     112 
     113def get_repo_relative_path(ext_dict, reponame, rm): 
     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[u'url'] 
     119    ext_repo = rm.get_repository(ext_dict[u'reponame']) 
     120    dest_repo = rm.get_repository(reponame) 
     121    if u'://' in url: 
     122        # Fully qualified URL 
     123        return None 
     124    if ext_repo.get_base() != dest_repo.get_base(): 
     125        # Not same filesystem-repository 
     126        return None 
     127    if url.startswith(u'../'): 
     128        # Relative to directory 
     129        url = _convert_dir_rel_to_repo_rel(url, ext_dict[u'repopath'], 
     130                                           ext_repo.scope) 
     131    if url.startswith(u'^/'): 
     132        # Relative to repository root 
     133        repo_url = url[2:] 
     134        scope = dest_repo.scope.replace(u'\\', u'/').strip(u'/') 
     135        scope += u'/' if scope else u'' 
     136        if repo_url.startswith(scope): 
     137            # Target path within current repository 
     138            # (scoped or not) - generate phase-2 link. 
     139            return u'%s?rev=%%(rev)s'       \ 
     140                   % (Href(dest_repo.reponame)(remprefix(repo_url, scope))) 
     141 
     142def _extract_host_from_url(url): 
     143    scheme, path = splittype(url or u'') 
     144    host, path = splithost(path or u'') 
     145    return (host or u'').lstrip(u'/') 
     146 
     147def get_external_url(ext_dict, reponame, rm, req_href=None): 
     148    """Build a URL for an external definition. If definition already given 
     149    as URL, return it. Otherwise, use `reponame` URL property to build a URL 
     150    for the given external. If `reponame` has no URL defined, 
     151    or the external is not related to that repository, return None. 
     152     
     153    Returned URL is fully-qualified URL string, with or without scheme. 
     154    """ 
     155    url = ext_dict[u'url'] 
     156    if u'://' in url: 
     157        # Fully qualified with scheme - it's the answer! 
     158        return url 
     159    dest_repo = rm.get_repository(reponame) 
     160    repo_url = (dest_repo.get_path_url(u'/', None) or u'').rstrip(u'/') 
     161    if url.startswith(u'//'): 
     162        # Relative to scheme - partial answer 
     163        link = url 
     164        if u'://' in repo_url: 
     165            # Use repo_url scheme to extend the answer 
     166            link = u'%s:%s' % (splittype(repo_url)[0], url) 
     167        elif req_href and u'://' in req_href: 
     168            # Use request href scheme to extend the answer 
     169            link = u'%s:%s' % (splittype(req_href)[0], url) 
     170        return link 
     171    ext_repo = rm.get_repository(ext_dict[u'reponame']) 
     172    ext_repo_url = (ext_repo.get_path_url(u'/', None) or u'').rstrip(u'/') 
     173    if url.startswith(u'/'): 
     174        # Relative to server root 
     175        dest_repo_host = _extract_host_from_url(repo_url) 
     176        ext_repo_host = _extract_host_from_url(ext_repo_url) 
     177        server_url = link = None 
     178        if dest_repo_host and (dest_repo_host == ext_repo_host or 
     179                               ext_repo.get_base() == dest_repo.get_base()): 
     180            # Repositories related and dest-repos has URL - take its host 
     181            link = u'//%s%s' % (dest_repo_host, url) 
     182            server_url = repo_url 
     183        elif req_href: 
     184            # Crazy heuristic - hope that target SVN on same host as this Trac 
     185            link = u'//%s%s' % (_extract_host_from_url(req_href), url) 
     186            server_url = req_href 
     187        if u'://' in (server_url or u''): 
     188            return u'%s:%s' % (splittype(server_url)[0], link) 
     189        return link 
     190    if ext_repo.get_base() != dest_repo.get_base(): 
     191        # Repositories are not related 
     192        return None 
     193    if not repo_url: 
     194        # No URL defined 
     195        return None 
     196    base_repo_url = remsuffix(repo_url, dest_repo.scope 
     197                              .replace(u'\\', u'/').rstrip(u'/')) 
     198    if url.startswith(u'../'): 
     199        # Relative to directory 
     200        url = _convert_dir_rel_to_repo_rel(url, ext_dict[u'repopath'], 
     201                                           ext_repo.scope) 
     202    if url.startswith(u'^/'): 
     203        # Relative to repository root 
     204        return u'%s/%s' % (base_repo_url.rstrip(u'/'), 
     205                           url[2:].lstrip(u'/')) 
     206 
     207def parse_externals_map_ini(ext_map, ini_section, log=None): 
     208    for dummy, key_value in ini_section: 
     209        key_value = key_value.split() 
     210        if len(key_value) != 2: 
     211            if log: 
     212                log.debug(u'svn:externals entry %s doesn\'t contain a ' 
     213                          'space-separated key value pair, skipping.' % dummy) 
     214            continue 
     215        key, value = key_value 
     216        if not u'//' in value: 
     217            # Trac-env-relative link 
     218            value = remprefix(value.lstrip(u'/'), u'browser/') 
     219        ext_map[key.rstrip(u'/')] = value.replace('%', '%%')        \ 
     220                                   .replace('$path', '%(path)s')    \ 
     221                                   .replace('$rev', '%(rev)s') 
     222    return ext_map 
     223 
     224def format_external_link(ext_map, link, rev=None): 
     225    """Lookup the best available match for `link` in `ext_map` (dictionary 
     226    of externals) and use the map value to replace the link content. 
     227    Returns a string with the matched URL if a match was found, 
     228    otherwise - None. 
     229    """ 
     230    path_elements = [] 
     231    base_url = link.rstrip(u'/') 
     232    while base_url: 
     233        if base_url in ext_map: 
     234            path = u'/'.join(reversed(path_elements)) 
     235            href = ext_map[base_url].replace(u'%(rev)s', u'%%(rev)s') 
     236            return href % {u'path': path} 
     237        base_url, suffix = posixpath.split(base_url) 
     238        path_elements.append(suffix) 
     239 
     240 
    32241class SubversionPropertyRenderer(Component): 
    33242    implements(IPropertyRenderer) 
    34243 
     
    45254     
    46255    def render_property(self, name, mode, context, props): 
    47256        if name == 'svn:externals': 
    48             return self._render_externals(props[name]) 
     257            return self._render_externals(props[name], context) 
    49258        elif name == 'svn:needs-lock': 
    50259            return self._render_needslock(context) 
    51260        elif name == 'svn:mergeinfo' or name.startswith('svnmerge-'): 
    52261            return self._render_mergeinfo(name, mode, context, props) 
    53262 
    54     def _render_externals(self, prop): 
     263    def _render_externals(self, prop, context): 
     264        rm = RepositoryManager(self.env) 
    55265        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: 
     266            parse_externals_map_ini(self._externals_map, 
     267                                    self.config.options('svn:externals'), 
     268                                    self.log) 
     269        externals_data = [] 
     270 
     271        def add_link(ext, link): 
     272            if link is None: 
     273                # No match 
     274                externals_data.append(((ext[u'dir'], ext[u'url'] or '', 
     275                                        ext[u'rev'] or ''), u'')) 
     276                return 
     277            if not u'//' in link: 
     278                # Phase-2 
     279                link = u'/browser/%s' % (link) 
     280            link = remsuffix(link % {u'rev': ext[u'rev'] or u''}, u'?rev=') 
     281            externals_data.append(((ext[u'dir'], ext[u'url'] or '', 
     282                                    ext[u'rev'] or ''), link)) 
     283 
     284        reponame = context.resource.parent.id 
     285        repopath = context.resource.id.replace(u'\\', u'/').strip(u'/') 
     286        repolist = [reponame] + [repo.reponame 
     287                                 for repo in rm.get_real_repositories() 
     288                                 if repo.reponame != reponame] 
     289        req_href = context.req.abs_href('/') 
     290        for ext in externals_generator(prop, reponame, repopath): 
     291            for name in repolist: 
     292                link = get_repo_relative_path(ext, name, rm) 
     293                if link: 
     294                    # Got phase-2 link - all done. 
     295                    break 
     296            if link: 
     297                add_link(ext, link) 
    71298                continue 
    72             localpath, rev, url = elements[0], '', elements[-1] 
    73             if localpath.startswith('#'): 
    74                 externals.append((external, None, None, None, None)) 
     299            last_phase1 = None 
     300            for name in repolist: 
     301                phase1_link = get_external_url(ext, name, rm, req_href) 
     302                if phase1_link: 
     303                    last_phase1 = phase1_link 
     304                    # Try converting to phase-2 link using repositories 
     305                    for othername in repolist: 
     306                        otherrepo = rm.get_repository(othername) 
     307                        link = format_external_link( 
     308                            {(otherrepo.get_path_url(u'/', None) or u'') 
     309                             .rstrip(u'/'): 
     310                             u'%s/%%(path)s?rev=%%(rev)s' % othername}, 
     311                             phase1_link) 
     312                        if link: 
     313                            break 
     314                    if not link: 
     315                        # Try matching link with externals map 
     316                        link = format_external_link(self._externals_map, 
     317                                                    phase1_link) 
     318                    if link: 
     319                        break 
     320            link = link or last_phase1 
     321            if link: 
     322                add_link(ext, link) 
    75323                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            add_link(ext, None) 
     325                 
     326        # Finish up with actually composing the externals table 
     327        trs = [] 
     328        for label, href in externals_data: 
     329            if not href: 
     330                tr = tag.tr(tag.td(tag.a(label[0], title= 
     331                                _('No matching external')), 
     332                            tag.td(label[1]), 
     333                            tag.td(label[2]))) 
    98334            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]) 
     335                tr = tag.tr(tag.td(tag.a(label[0], href=href, 
     336                                   title=_('Jump to external'))), 
     337                            tag.td(tag.a(label[1], href=href)), 
     338                            tag.td(tag.a(label[2], href=href))) 
     339            trs.append(tr) 
     340        return tag.table(tag.tbody(trs)) 
    115341 
    116342    def _render_needslock(self, context): 
    117343        return tag.img(src=context.href.chrome('common/lock-locked.png'),