Edgewall Software

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

File svn-externals-7687-multirepos.patch, 19.4 KB (added by Itamar O <itamarost@…>, 2 years ago)

Ported patch to svn_prop.py and extended to support multirepos concepts

  • svn_prop.py

     
    2727from trac.versioncontrol.web_ui.changeset import IPropertyDiffRenderer 
    2828from trac.util import Ranges, to_ranges 
    2929from trac.util.translation import _, tag_ 
     30from trac.web.href import Href 
     31from urllib import splittype, splithost 
    3032 
    31  
    3233class SubversionPropertyRenderer(Component): 
    3334    implements(IPropertyRenderer) 
    3435 
     
    4546     
    4647    def render_property(self, name, mode, context, props): 
    4748        if name == 'svn:externals': 
    48             return self._render_externals(props[name]) 
     49            return self._render_externals(props[name], context) 
    4950        elif name == 'svn:needs-lock': 
    5051            return self._render_needslock(context) 
    5152        elif name == 'svn:mergeinfo' or name.startswith('svnmerge-'): 
    5253            return self._render_mergeinfo(name, mode, context, props) 
    5354 
    54     def _render_externals(self, prop): 
     55    def _render_externals(self, prop, context): 
     56        """Render the svn:externals list as a table of external references with appropriate links. 
     57 
     58        svn:externals is a multiline property, each line representing one external reference. 
     59         
     60        The "old" syntax of an external reference (pre-svn-1.5) is: 
     61        local/sub/dir   [-r REV]    scheme://fully.qualified/url 
     62        where the revision specification is optional (refers to youngest when omitted), 
     63        and may or may not contain whitespace between '-r' and 'REV' 
     64 
     65        In svn-1.5 a "new" syntax for external references was introduced, with several flavors: 
     66        - Fully qualified: 
     67        [-r REV]    scheme://fully.qualified/url    local/sub/dir   # (again with optional revision spec) 
     68        - Peg revision syntax: 
     69        scheme://fully.qualified/url@REV        local/sub/dir 
     70        - Relative to directory on which the property is defined: 
     71        [-r REV] ../[../]rel/path[@REV]         local/sub/dir       # (at most one of rev-spec & peg-revision) 
     72        - Relative to root of current repository: 
     73        [-r REV] ^/path/from/repos/root[@REV]   local/sub/dir       # (at most one of rev-spec & peg-revision) 
     74        - Relative to the scheme of the URL of the directory on which the property is defined: 
     75        [-r REV] //fully.qualified/url[@REV]    local/sub/dir       # (at most one of rev-spec & peg-revision) 
     76        - Relative to the root URL of the server on which the property is defined: 
     77        [-r REV] /url/from/server/root[@REV]    local/sub/dir       # (at most one of rev-spec & peg-revision) 
     78 
     79        The rendered externals table is of the form (WikiSyntax): 
     80        || [generated-link local/sub/dir] || specified external text || revision number || 
     81        where the renderer performs a best-effort attempt to produce 
     82        the most useful generated link following this logic: 
     83        - For fully qualified URLs (old or new syntax), the level-1 generated-link is the URL 
     84          (prefixed with current scheme, in case of scheme-relative URL). 
     85        - For repository-root-relative paths: 
     86          1. If current repository is not scoped, then the target path is necessarily in the current repository, 
     87             so generate directly level-2 link of the form /browser/ReposName/path/from/repos/root?rev=REV 
     88          2. If current repository is scoped, and the target path is within the scope, proceed as (1). 
     89          3. If current repository is scoped, and the target path is '''NOT''' within the scope: 
     90             (a) If the Repository URL is defined, generate a level-1 link by stripping the scope 
     91                 from the Repository URL and appending the path/from/repos/root. 
     92             (b) Otherwise, go over all other available real repositories that has the same base-path as current one. 
     93                 If another repository OtherRepos is found that has the target path within its scope, 
     94                 directly generate level-2 link of the form /browser/OtherReposName/path/from/repos/root?rev=REV 
     95             (c) If did not find a match, do not generate a link. 
     96        - For directory-relative paths: 
     97          1. Traverse current repository upwards from current location N steps (N = number of '../' prefixing rel-path). 
     98          2. If still in scope of current repository, directly generate level-2 link. 
     99          3. If went out of scope: 
     100             (a) If the Repository URL is defined, generate a level-1 link by traversing the Repository URL. 
     101             (b) Otherwise, traverse the repository dir and go over all other available real repositories that has the same 
     102                 base-path as current one. If another repository OtherRepos is found that has the target path within its 
     103                 scope, directly generate level-2 link of the form /browser/OtherReposName/path/from/repos/root?rev=REV 
     104             (c) If did not find a match, do not generate a link. 
     105        - For server-relative URLS: 
     106          1. If the Repository URL parameter is available for current repository: 
     107             (a) Extract the host from the Repository URL 
     108             (b) Generate a level-1 link by appending the /url/from/server/root to the extracted host. 
     109          2. Otherwise use absolute URL of the current request as base server to generate level-1 link 
     110             (simple heuristic that assumes that the target repository and current Trac environment are served 
     111             via the same host. Of course, this is not always true, so better set the Repository URL explicitly..) 
     112        - In case the generated-link is level-1, perform a refinement phase: 
     113          (1) Lookup the level-1 link in the svn:externals map (see below). 
     114          (2) If found a match, apply the map template to produce a level-2 link. 
     115          (3) Otherwise keep the level-1 link. 
     116           
     117        The svn:externals map is used to transform level-1 links that point directly to a SVN repository 
     118        (that suffers from the inability to point to a revision other than HEAD) into more "friendly" links 
     119        into a repository browser (another Trac instance, ViewVC, ...) (see TracIni#svn:externals-section). 
     120        The svn:externals map that is specified in the TracIni is further enhanced by going over available 
     121        repositories that has their Repository URL defined and adding entries that point back to the current Trac. 
     122        """ 
     123        rm = RepositoryManager(self.env) 
    55124        if not self._externals_map: 
     125            # Create externals map entries from repositories with URL defined. 
     126            for repname, repinfo in rm.get_all_repositories().iteritems(): 
     127                repurl = repinfo.get(u'url', '') 
     128                if '' != repurl: 
     129                    urlmap = '/browser/' 
     130                    if '' != repname: 
     131                        urlmap += repname + '/' 
     132                    self._externals_map[repurl] = urlmap + '%(path)s?rev=%(rev)s' 
    56133            for dummykey, value in self.config.options('svn:externals'): 
    57134                value = value.split() 
    58135                if len(value) != 2: 
     
    64141                self._externals_map[key] = value.replace('%', '%%') \ 
    65142                                           .replace('$path', '%(path)s') \ 
    66143                                           .replace('$rev', '%(rev)s') 
    67         externals = [] 
     144        externals_data = [] 
     145         
     146        def add_externals_url(localpath, url, rev, href): 
     147            if (href or '').endswith('?rev='): 
     148                href = href[:-5] 
     149            externals_data.append(((localpath, url or '', rev or ''), href)) 
     150 
     151        # Use the externals map to build a better href for the external 
     152        def lookup_externals_map(href, rev): 
     153            for key, tmpl in self._externals_map.iteritems(): 
     154                if href.startswith(key): 
     155                    # match 
     156                    path = href[len(key):].lstrip('/') 
     157                    return tmpl % {'path': path, 'rev': rev} 
     158            # no match. keep original. 
     159            return href 
     160##                 
     161##            prefix = [] 
     162##            base_url = url 
     163##            while base_url: 
     164##                if base_url in self._externals_map or base_url == u'/': 
     165##                    break 
     166##                if base_url.endswith(':'): 
     167##                    prefix.append(base_url+'//') 
     168##                    base_url = '' 
     169##                    break 
     170##                base_url, pref = posixpath.split(base_url) 
     171##                prefix.append(pref) 
     172##            href = self._externals_map.get(base_url) 
     173##            if not href and (url.startswith('http://') or  
     174##                             url.startswith('https://') or 
     175##                             url.startswith('/')): 
     176##                href = url.replace('%', '%%') 
     177##            if href: 
     178##                remotepath = '' 
     179##                if prefix: 
     180##                    remotepath = posixpath.join(*reversed(prefix)) 
     181##                add_externals_url(localpath, url, rev, 
     182##                                  href % {'path': remotepath, 'rev': rev}) 
     183##            else: 
     184##                add_externals_url(localpath, url, rev, None) 
     185             
    68186        for external in prop.splitlines(): 
     187            # Go over external lines and generate level-1 or level-2 links 
     188            external.replace('\\', '/') 
     189            if external.startswith('#'): 
     190                # Skip commented lines in prop 
     191                add_externals_url('', external, '', '') 
     192                continue 
    69193            elements = external.split() 
    70194            if not elements: 
    71195                continue 
    72             localpath, rev, url = elements[0], '', elements[-1] 
    73             if localpath.startswith('#'): 
    74                 externals.append((external, None, None, None, None)) 
     196             
     197            # Old-style externals syntax 
     198            if u'://' in elements[-1]: 
     199                localpath, rev, url = elements[0], '', elements[-1] 
     200                if len(elements) > 2: 
     201                    rev = elements[len(elements)==4 and 3 or 2] 
     202                    rev = rev.replace('-r', '') 
     203                add_externals_url(localpath, url, rev, lookup_externals_map(url, rev)) 
    75204                continue 
    76             if len(elements) == 3: 
    77                 rev = elements[1] 
     205             
     206            if 1 >= len(elements): 
     207                continue 
     208             
     209            # New-style externals syntax 
     210            resource = context.resource 
     211            reponame = context.resource.parent.id 
     212            current_dir = context.resource.id.replace('\\', '/') 
     213            repos = rm.get_repository(reponame) 
     214            #target_rev = context.resource.version 
     215            #localpath = resource.id 
     216            # extract localpath, URL and revision ref (-r or peg): 
     217            url, localpath = elements[-2], elements[-1] 
     218            rev = '' 
     219            if len(elements) > 2: 
     220                rev = elements[len(elements)==4 and 1 or 0] 
    78221                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})) 
     222            if '@' in url: 
     223                (url, rev) = url.split('@') 
     224                 
     225            # which flavor is the URL? 
     226             
     227            # Fully qualified with scheme 
     228            if u'://' in url: 
     229                add_externals_url(localpath, url, rev, lookup_externals_map(url, rev)) 
     230                continue 
     231             
     232            # Relative to scheme 
     233            if url.startswith(u'//'): 
     234                href = '%s://%s' % (splittype(context.req.abs_href('/'))[0], url.lstrip('/')) 
     235                add_externals_url(localpath, url, rev, lookup_externals_map(href, rev)) 
     236                continue 
     237 
     238            def lremove(str, substr): 
     239                "Deletes 'substr' from 'str' if 'substr' prefixes 'str'." 
     240                if str.startswith(substr): 
     241                    return str[len(substr):] 
     242                 
     243            def rremove(str, substr): 
     244                "Deletes 'substr' from 'str' if 'substr' suffixes 'str'." 
     245                if str.endswith(substr): 
     246                    return str[:-len(substr)] 
     247 
     248            # Utility function for repos-relative paths 
     249            def build_repos_link(repos, rel_url, rev): 
     250                scope = repos.scope.replace('\\', '/').strip('/') 
     251                if rel_url.startswith(scope): 
     252                    # Target path within current repository (scoped or not) - generate level-2 link. 
     253                    return Href('/').browser(repos.reponame, lremove(rel_url, scope), rev=rev) 
     254                # Target path not within current repository scope. 
     255                reposurl = repos.params.get(u'url', '').rstrip('/') 
     256                base_reposurl = rremove(reposurl, scope) 
     257                if '' != reposurl: 
     258                    # URL defined, so generate level-1 link. 
     259                    return lookup_externals_map('/'.join([base_reposurl, rel_url]), rev) 
     260                return None 
     261                 
     262            # Relative to repository root 
     263            if url.startswith(u'^/'): 
     264                rel_url = url[2:].strip('/') 
     265                href = build_repos_link(repos, rel_url, rev) 
     266                if href is None: 
     267                    # No match for current repository. Look for others. 
     268                    for otherrepos in rm.get_real_repositories(): 
     269                        if otherrepos == repos or otherrepos.get_base() != repos.get_base(): 
     270                            continue 
     271                        href = build_repos_link(otherrepos, rel_url, rev) 
     272                        if href is not None: 
     273                            break 
     274                if href is None: 
     275                    self.log.warn('external URL %s out of all repositories scopes' % url) 
     276                add_externals_url(localpath, url, rev, href) 
     277                continue 
     278             
     279            # Relative to the current directory 
     280            if url.startswith(u'../'): 
     281                def up_one_level(path): 
     282                    return '/'.join(path.split('/')[:-1]) 
     283                 
     284                rel_url = url 
     285                out_of_scope = False 
     286                href = None 
     287                while rel_url.startswith('../'): 
     288                    rel_url = rel_url[3:] 
     289                    if '' == current_dir: 
     290                        # no more levels to traverse. 
     291                        if not out_of_scope: 
     292                            out_of_scope = True 
     293                            reposurl = repos.params.get(u'url', '').rstrip('/') 
     294                            if '' != reposurl: 
     295                                # Repository URL defined, so traverse it. 
     296                                current_dir = reposurl 
     297                            else: 
     298                                # Repository URL not defined, so traverse repository dir instead. 
     299                                current_dir = repos.params['dir'].replace('\\', '/') 
     300                        else: 
     301                            # can't go out of scope again... 
     302                            self.log.warn('external URL %s out of humanly-possible scope' % url) 
     303                            break 
     304                    current_dir = up_one_level(current_dir) 
     305                if not out_of_scope: 
     306                    # Finished traversing without leaving current repository - worthy of level-2 link! 
     307                    href = Href('/').browser(reponame, current_dir if '' != current_dir else None, rel_url, rev=rev) 
     308                elif '' != reposurl: 
     309                    # Left current repository scope, but found Repository URL - worthy of level-1 link. 
     310                    href = lookup_externals_map('/'.join([current_dir, rel_url]), rev) 
     311                else: 
     312                    # Left current repository scope and didn't find Repository URL. 
     313                    # Check if another repository matches the scope. 
     314                    for otherrepos in rm.get_real_repositories(): 
     315                        if otherrepos == repos or otherrepos.get_base() != repos.get_base(): 
     316                            continue 
     317                        scope = otherrepos.scope.replace('\\', '/').strip('/') 
     318                        if rel_url.startswith(scope): 
     319                            # matched - generate level-2 link 
     320                            href = Href('/').browser(otherrepos.reponame, lremove(rel_url, scope), rev=rev) 
     321                            break 
     322                if href is None: 
     323                    self.log.warn('external URL %s out of all repositories scopes' % url) 
     324                add_externals_url(localpath, url, rev, href) 
     325                continue 
     326             
     327            # Relative to server root 
     328            if url.startswith(u'/'): 
     329                reposurl = repos.params.get(u'url', '').rstrip('/') 
     330                if '' != reposurl: 
     331                    # Repository URL defined - generate level-1 link from it. 
     332                    scheme, href = splittype(reposurl) 
     333                else: 
     334                    # Repository URL NOT defined - generate level-1 link from request abs_url. 
     335                    scheme, href = splittype(context.req.abs_href('/')) 
     336                href = '%s://%s%s' % (scheme, splithost(href)[0], url) 
     337                add_externals_url(localpath, url, rev, lookup_externals_map(href, rev)) 
     338                continue 
     339         
     340        # Finish up with actually composing the externals table 
     341        trs = [] 
     342        for label, href in externals_data: 
     343            if not href: 
     344                tr = tag.tr(tag.td(tag.a(label[0], title=\ 
     345                                _('No matching external')), 
     346                            tag.td(label[1]), 
     347                            tag.td(label[2]))) 
    98348            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]) 
     349                tr = tag.tr(tag.td(tag.a(label[0], href=href, 
     350                                   title=_('Jump to external'))), 
     351                            tag.td(tag.a(label[1], href=href)), 
     352                            tag.td(tag.a(label[2], href=href))) 
     353            trs.append(tr) 
     354        return tag.table(tag.tbody(trs)) 
    115355 
    116356    def _render_needslock(self, context): 
    117357        return tag.img(src=context.href.chrome('common/lock-locked.png'),