| 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) |
| 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 | |
| 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]))) |