Ticket #1847: vc-refactoring_for_trac-0.9.patch
| File vc-refactoring_for_trac-0.9.patch, 28.8 KB (added by cboos, 3 years ago) |
|---|
-
htdocs/css/trac.css
257 257 .wikipage { padding-left: 18px } 258 258 .wikipage h1, .wikipage h2, .wikipage h3 { margin-left: -18px } 259 259 260 a.missing:link, a.missing:visited { background: #fafaf0; color: #998 }260 a.missing:link, a.missing:visited, span.missing { background: #fafaf0; color: #998 } 261 261 a.missing:hover { color: #000; } 262 262 a.closed:link, a.closed:visited { text-decoration: line-through } 263 263 -
htdocs/css/browser.css
2 2 * html #prefs { width: 34em } /* Set width only for IE */ 3 3 #prefs fieldset label { display: block } 4 4 #prefs .buttons { margin-top: -1.6em } 5 #prefs .button { margin-left: 1.6em } 5 6 #prefs .choice { margin-top: -0.6em } 7 #prefs .rev { align: right } 6 8 7 9 #legend { clear: right; } 8 10 … … 24 26 color: #888; 25 27 white-space: nowrap; 26 28 } 29 #dirlist td.rev { 30 font-family: courier; 31 letter-spacing: -0.02em; 32 font-size: 90%; 33 text-align: right; 34 } 27 35 #dirlist td.size { 28 36 color: #888; 29 37 white-space: nowrap; … … 59 67 #chglist td.old_path { font-style: italic } 60 68 #chglist td.date { font-size: 85%; vertical-align: top; padding-top: 0.55em; white-space: nowrap } 61 69 #chglist td.author { font-size: 85%; vertical-align: top; padding-top: 0.55em } 62 #chglist td.rev, #chglist td.chgset { text-align: right } 70 #chglist td.rev, #chglist td.chgset { 71 font-family: courier; 72 letter-spacing: -0.02em; 73 font-size: 90%; 74 text-align: right; 75 } 63 76 #chglist td.rev a, #chglist td.chgset a { border-bottom: none } 64 77 #chglist td.summary { width: 100%; font-size: 85%; vertical-align: middle; white-space: nowrap } 65 78 #chglist td.summary * { margin-top: 0; margin-bottom: 0 } -
trac/env.py
22 22 from trac.config import Configuration 23 23 from trac.core import Component, ComponentManager, implements, Interface, \ 24 24 ExtensionPoint, TracError 25 from trac.versioncontrol import ScmBackendManager 25 26 26 27 __all__ = ['Environment', 'IEnvironmentSetupParticipant', 'open_environment'] 27 28 … … 120 121 and component_name.startswith(pattern[:-1]): 121 122 return enabled 122 123 124 # versioncontrol components are enabled if the repository is configured 125 if component_name.startswith('trac.versioncontrol'): 126 return self.config.get('trac', 'repository_dir') != '' 127 123 128 # By default, all components in the trac package are enabled 124 129 return component_name.startswith('trac.') 125 130 … … 146 151 """Return the version control repository configured for this 147 152 environment. 148 153 149 The repository is wrapped in a `CachedRepository`.150 151 154 @param authname: user name for authorization 152 155 """ 153 from trac.versioncontrol.cache import CachedRepository154 from trac.versioncontrol.svn_authz import SubversionAuthorizer155 from trac.versioncontrol.svn_fs import SubversionRepository156 156 repos_dir = self.config.get('trac', 'repository_dir') 157 157 if not repos_dir: 158 158 raise EnvironmentError, 'Path to repository not configured' 159 authz = None 160 if authname: 161 authz = SubversionAuthorizer(self, authname) 162 repos = SubversionRepository(repos_dir, authz, self.log) 163 return CachedRepository(self.get_db_cnx(), repos, authz, self.log) 159 return ScmBackendManager(self).get_repository(repos_dir, authname) 164 160 165 161 def create(self, db_str=None): 166 162 """Create the basic directory structure of the environment, initialize -
trac/db_default.py
445 445 'trac.ticket.roadmap', 'trac.ticket.web_ui', 446 446 'trac.Timeline', 447 447 'trac.versioncontrol.web_ui', 448 'trac.versioncontrol.svn_fs', 448 449 'trac.wiki.macros', 'trac.wiki.web_ui', 449 450 'trac.web.auth') -
trac/versioncontrol/api.py
16 16 17 17 from __future__ import generators 18 18 from trac.perm import PermissionError 19 from trac.core import * 19 20 21 ### Source Configuration Management interface and manager 22 23 class IScmBackend(Interface): 24 """SCM backend for Trac""" 25 26 def identifiers(self): 27 """SCM string prefixes that are supported by the backend, 28 and their relative priorities. 29 30 Highest number is highest priority. 31 """ 32 33 def repository(self, scheme, args, authname): 34 """Get a Repository object. 35 36 `scheme` is the scheme that was used to select this backend, 37 `args` is the remaining specification for the repository and 38 `authname` is the user name, for authentication purpose. 39 """ 40 41 def init_repository(self, **params): # TBD 42 """Initialize a repository""" 43 44 45 class ScmBackendManager(Component): 46 """TODO: share some code with the DatabaseBackendManager""" 47 48 backends = ExtensionPoint(IScmBackend) 49 50 def __init__(self): 51 self._backend_map = None 52 53 def get_repository(self, repos_str, authname): 54 if ':' in repos_str and len(repos_str) > 2 and repos_str[1] != ':': 55 scheme, args = repos_str.split(':', 1) 56 else: 57 scheme, args = 'svn', repos_str 58 backend = self._get_backend(scheme) 59 return backend.repository(scheme, args, authname) 60 61 def _get_backend(self, scheme): 62 if not self._backend_map: 63 self._backend_map = {} 64 for backend in self.backends: 65 for ident, prio in backend.identifiers(): 66 if ident in self._backend_map: 67 highest = self._backend_map[ident][1] 68 else: 69 highest = 0 70 if prio > highest: 71 self._backend_map[ident] = (backend, prio) 72 if not scheme in self._backend_map: 73 raise TracError, 'Unsupported SCM "%s"' % scheme 74 return self._backend_map[scheme][0] 75 76 77 ### Abstract classes for the backends 78 20 79 class Repository(object): 21 80 """ 22 81 Base class for a repository provided by a version control system. … … 40 99 """ 41 100 raise NotImplementedError 42 101 102 def get_changesets(self, start, stop): 103 """ 104 Generate Changeset belonging to the given time period (start, stop). 105 """ 106 rev = self.youngest_rev 107 while rev: 108 if self.authz.has_permission_for_changeset(rev): 109 chgset = self.get_changeset(rev) 110 if chgset.date < start: 111 return 112 if chgset.date < stop: 113 yield chgset 114 rev = self.previous_rev(rev) 115 43 116 def has_node(self, path, rev): 44 117 """ 45 118 Tell if there's a node at the specified (path,rev) combination. … … 122 195 'None' is a valid revision value and represents the youngest revision. 123 196 """ 124 197 return NotImplementedError 198 199 def short_rev(self, rev): 200 """ 201 Return a compact representation of a revision in the repos. 202 """ 203 return self.normalize_rev(rev) 125 204 126 205 127 206 class Node(object): -
trac/versioncontrol/tests/svn_fs.py
100 100 self.assertEqual(11, self.repos.normalize_rev(11)) 101 101 102 102 def test_rev_navigation(self): 103 self.assertEqual( 0, self.repos.oldest_rev)103 self.assertEqual(1, self.repos.oldest_rev) 104 104 self.assertEqual(None, self.repos.previous_rev(0)) 105 self.assertEqual( 0, self.repos.previous_rev(1))105 self.assertEqual(None, self.repos.previous_rev(1)) 106 106 self.assertEqual(12, self.repos.youngest_rev) 107 107 self.assertEqual(6, self.repos.next_rev(5)) 108 108 self.assertEqual(7, self.repos.next_rev(6)) -
trac/versioncontrol/svn_fs.py
17 17 from __future__ import generators 18 18 19 19 from trac.util import TracError 20 from trac.versioncontrol import Changeset, Node, Repository 20 from trac.versioncontrol import Changeset, Node, Repository, IScmBackend 21 from trac.versioncontrol.cache import CachedRepository 22 from trac.versioncontrol.svn_authz import SubversionAuthorizer 23 from trac.versioncontrol.web_ui import ChangesetModule, BrowserModule 24 from trac.core import * 21 25 22 26 import os.path 23 27 import time 24 28 import weakref 25 29 import posixpath 26 30 27 from svn import fs, repos, core, delta 31 try: 32 from svn import fs, repos, core, delta 33 has_subversion = True 34 except ImportError: 35 has_subversion = False 36 class dummy_svn(object): 37 svn_node_dir = 1 38 svn_node_file = 2 39 def apr_pool_destroy(): pass 40 def apr_terminate(): pass 41 def apr_pool_clear(): pass 42 core = dummy_svn() 28 43 29 44 _kindmap = {core.svn_node_dir: Node.DIRECTORY, 30 45 core.svn_node_file: Node.FILE} 31 46 47 48 ### Components 49 50 class SvnFsBackend(Component): 51 52 implements(IScmBackend) 53 54 def identifiers(self): 55 global has_subversion 56 if has_subversion: 57 yield ("direct-svn-fs", 8) 58 yield ("svn-fs", 4) 59 yield ("svn", 2) 60 61 def repository(self, scheme, args, authname): 62 """Return a `SubversionRepository`. 63 64 The repository is generally wrapped in a `CachedRepository`, 65 unless `direct-svn-fs` is the specified scheme. 66 """ 67 authz = None 68 if authname: 69 authz = SubversionAuthorizer(self.env, authname) 70 repos = SubversionRepository(args, authz, self.log) 71 if scheme == 'direct-svn-fs': 72 return repos 73 else: 74 db = self.env.get_db_cnx() 75 return CachedRepository(db, repos, authz, self.log) 76 77 78 class SvnFsBrowserModule(BrowserModule): 79 pass 80 81 class SvnFsChangesetModule(ChangesetModule): 82 pass 83 84 85 ### Helpers 86 32 87 application_pool = None 33 88 34 89 … … 58 113 """Remove leading "/", except for the root""" 59 114 return path and path.strip('/') or '/' 60 115 61 def _ scoped_path(scope, fullpath):116 def _path_within_scope(scope, fullpath): 62 117 """Remove the leading scope from repository paths""" 63 118 if fullpath: 64 119 if scope == '/': … … 168 223 del self._weakref 169 224 170 225 # Initialize application-level pool 171 Pool() 226 if has_subversion: 227 Pool() 172 228 173 174 229 class SubversionRepository(Repository): 175 230 """ 176 231 Repository implementation based on the svn.fs API. … … 185 240 self.pool = Pool() 186 241 187 242 # Remove any trailing slash or else subversion might abort 188 if not os.path.split(path)[1]: 189 path = os.path.split(path)[0] 243 path = os.path.normpath(path).replace('\\', '/') 190 244 self.path = repos.svn_repos_find_root_path(path, self.pool()) 191 245 if self.path is None: 192 246 raise TracError, \ … … 208 262 self.scope = '/' 209 263 self.log.debug("Opening subversion file-system at %s with scope %s" \ 210 264 % (self.path, self.scope)) 265 self.youngest = None 266 self.oldest = None 211 267 212 self.rev = fs.youngest_rev(self.fs_ptr, self.pool())213 214 self.history = None215 if self.scope != '/':216 self.history = []217 for path,rev in _get_history(self.scope[1:], self.authz,218 self.fs_ptr, self.pool, 0, self.rev):219 self.history.append(rev)220 221 268 def __del__(self): 222 269 self.close() 223 270 … … 246 293 self.log.debug("Closing subversion file-system at %s" % self.path) 247 294 self.repos = None 248 295 self.fs_ptr = None 249 self.rev = None250 296 self.pool = None 251 297 252 298 def get_changeset(self, rev): … … 263 309 return SubversionNode(path, rev, self.authz, self.scope, self.fs_ptr, 264 310 self.pool) 265 311 312 def _history(self, path, start, end, limit=None): 313 scoped_path = self.scope[1:]+path 314 return _get_history(scoped_path, self.authz, self.fs_ptr, self.pool, 315 start, end, limit) 316 266 317 def get_oldest_rev(self): 267 rev = 0 268 if self.scope == '/': 269 return rev 270 return self.history[-1] 318 if self.oldest is None: 319 self.oldest = 1 320 if self.scope != '/': 321 self.oldest = self.next_rev(0) 322 return self.oldest 271 323 272 324 def get_youngest_rev(self): 273 rev = self.rev 274 if self.scope == '/': 275 return rev 276 return self.history[0] 325 if not self.youngest: 326 self.youngest = fs.youngest_rev(self.fs_ptr, self.pool()) 327 if self.scope != '/': 328 for path, rev in self._history('', 0, self.youngest, limit=1): 329 self.youngest = rev 330 return self.youngest 277 331 278 332 def previous_rev(self, rev): 279 rev = int(rev) 280 if rev == 0: 281 return None 282 if self.scope == '/': 283 return rev - 1 284 idx = self.history.index(rev) 285 if idx + 1 < len(self.history): 286 return self.history[idx + 1] 333 rev = self.normalize_rev(rev) 334 if rev > 1: # don't use oldest here, as it's too expensive 335 try: 336 for path, prev in self._history('', 0, rev-1, limit=1): 337 return prev 338 except SystemError: 339 pass 287 340 return None 288 341 289 342 def next_rev(self, rev): 290 rev = int(rev)291 if rev == self.rev:292 return None293 if self.scope == '/':294 return rev + 1295 if rev == 0:296 return self.oldest_rev297 idx = self.history.index(rev)298 if idx > 0:299 return self.history[idx - 1]343 rev = self.normalize_rev(rev) 344 next = rev + 1 345 youngest = self.youngest_rev 346 while next <= youngest: 347 try: 348 for path, next in self._history('', rev+1, next, limit=1): 349 return next 350 next += 1 351 except SystemError: # i.e. "null arg to internal routine" 352 return next # a 'delete' event is also interesting... 300 353 return None 301 354 302 355 def rev_older_than(self, rev1, rev2): … … 328 381 older = None # 'older' is the currently examined history tuple 329 382 for p, r in _get_history(self.scope + path, self.authz, 330 383 self.fs_ptr, subpool, 0, rev, limit): 331 older = (_scoped_path(self.scope, p), r, Changeset.ADD) 384 older = (_path_within_scope(self.scope, p), r, 385 Changeset.ADD) 332 386 rev = self.previous_rev(r) 333 387 if newer: 334 388 if older[0] == path: … … 405 459 pool = Pool(self.pool) 406 460 for path, rev in _get_history(self.scoped_path, self.authz, self.fs_ptr, 407 461 pool, 0, self._requested_rev, limit): 408 scoped_path = _scoped_path(self.scope, path)409 if rev > 0 and scoped_path:410 older = ( scoped_path, rev, Changeset.ADD)462 path = _path_within_scope(self.scope, path) 463 if rev > 0 and path: 464 older = (path, rev, Changeset.ADD) 411 465 if newer: 412 466 change = newer[0] == older[0] and Changeset.EDIT or \ 413 467 Changeset.COPY … … 472 526 continue 473 527 if not path.startswith(self.scope[1:]): 474 528 continue 475 base_path = _ scoped_path(self.scope, change.base_path)529 base_path = _path_within_scope(self.scope, change.base_path) 476 530 action = '' 477 531 if not change.path: 478 532 action = Changeset.DELETE -
trac/versioncontrol/web_ui/changeset.py
35 35 36 36 class ChangesetModule(Component): 37 37 38 abstract = True 39 38 40 implements(INavigationContributor, IPermissionRequestor, IRequestHandler, 39 41 ITimelineEventProvider, IWikiSyntaxProvider, ISearchSource) 40 42 … … 106 108 'changeset_show_files')) 107 109 db = self.env.get_db_cnx() 108 110 repos = self.env.get_repository() 109 authzperm = SubversionAuthorizer(self.env, req.authname) 110 rev = repos.youngest_rev 111 while rev: 112 if not authzperm.has_permission_for_changeset(rev): 113 rev = repos.previous_rev(rev) 114 continue 111 for chgset in repos.get_changesets(start, stop): 112 message = chgset.message or '--' 113 if format == 'rss': 114 title = 'Changeset <em>[%s]</em>: %s' \ 115 % (util.escape(chgset.rev), 116 util.escape(util.shorten_line(message))) 117 href = self.env.abs_href.changeset(chgset.rev) 118 message = wiki_to_html(message, self.env, db, 119 absurls=True) 120 else: 121 title = 'Changeset <em>[%s]</em> by %s' \ 122 % (util.escape(chgset.rev), 123 util.escape(chgset.author)) 124 href = self.env.href.changeset(chgset.rev) 125 message = wiki_to_oneliner(message, self.env, db, 126 shorten=True) 127 if show_files: 128 files = [] 129 for chg in chgset.get_changes(): 130 if show_files > 0 and len(files) >= show_files: 131 files.append('...') 132 break 133 files.append('<span class="%s">%s</span>' 134 % (chg[2], util.escape(chg[0]))) 135 message = '<span class="changes">' + ', '.join(files) +\ 136 '</span>: ' + message 137 yield 'changeset', href, title, chgset.date, chgset.author,\ 138 message 115 139 116 chgset = repos.get_changeset(rev)117 if chgset.date < start:118 return119 if chgset.date < stop:120 message = chgset.message or '--'121 if format == 'rss':122 title = 'Changeset <em>[%s]</em>: %s' \123 % (util.escape(chgset.rev),124 util.escape(util.shorten_line(message)))125 href = self.env.abs_href.changeset(chgset.rev)126 message = wiki_to_html(message, self.env, db,127 absurls=True)128 else:129 title = 'Changeset <em>[%s]</em> by %s' \130 % (util.escape(chgset.rev),131 util.escape(chgset.author))132 href = self.env.href.changeset(chgset.rev)133 message = wiki_to_oneliner(message, self.env, db,134 shorten=True)135 if show_files:136 files = []137 for chg in chgset.get_changes():138 if show_files > 0 and len(files) >= show_files:139 files.append('...')140 break141 files.append('<span class="%s">%s</span>'142 % (chg[2], util.escape(chg[0])))143 message = '<span class="changes">' + ', '.join(files) +\144 '</span>: ' + message145 yield 'changeset', href, title, chgset.date, chgset.author,\146 message147 rev = repos.previous_rev(rev)148 149 140 # Internal methods 150 141 151 142 def _render_html(self, req, repos, chgset, diff_options): … … 169 160 youngest_rev = repos.youngest_rev 170 161 if str(chgset.rev) != str(youngest_rev): 171 162 next_rev = repos.next_rev(chgset.rev) 172 add_link(req, 'next', self.env.href.changeset(next_rev), 173 'Changeset %s' % next_rev) 163 if next_rev: 164 add_link(req, 'next', self.env.href.changeset(next_rev), 165 'Changeset %s' % next_rev) 174 166 add_link(req, 'last', self.env.href.changeset(youngest_rev), 175 167 'Changeset %s' % youngest_rev) 176 168 … … 180 172 info = {'change': change} 181 173 if base_path: 182 174 info['path.old'] = base_path 183 info['rev.old'] = base_rev175 info['rev.old'] = repos.short_rev(base_rev) 184 176 info['browser_href.old'] = self.env.href.browser(base_path, 185 177 rev=base_rev) 186 178 if path: 187 179 info['path.new'] = path 188 info['rev.new'] = chgset.rev180 info['rev.new'] = repos.short_rev(chgset.rev) 189 181 info['browser_href.new'] = self.env.href.browser(path, 190 182 rev=chgset.rev) 191 183 if change in (Changeset.COPY, Changeset.EDIT, Changeset.MOVE): -
trac/versioncontrol/web_ui/browser.py
50 50 51 51 class BrowserModule(Component): 52 52 53 abstract = True 54 53 55 implements(INavigationContributor, IPermissionRequestor, IRequestHandler, 54 56 IWikiSyntaxProvider) 55 57 -
trac/scripts/admin.py
541 541 prompt = 'Database connection string [%s]> ' % ddb 542 542 returnvals.append(raw_input(prompt).strip() or ddb) 543 543 print 544 print ' Please specify the absolute path to the project Subversion repository.' 545 print ' Repository must be local, and trac-admin requires read+write' 546 print ' permission to initialize the Trac database.' 544 print ' Please specify the absolute path to the project SCM repository,' 545 print ' or leave it blank to use Trac without a repository.' 547 546 print 548 drp = '/var/svn/test' 549 prompt = 'Path to repository [%s]> ' % drp 550 returnvals.append(raw_input(prompt).strip() or drp) 547 print ' Also, the repository can be specified later.' 548 print 549 drp = 'svn:/path/to/repos/optional_scope_within_repos' 550 prompt = 'Path to repository, e.g. "%s"> ' % drp 551 returnvals.append(raw_input(prompt).strip()) 551 552 print 552 553 print ' Please enter location of Trac page templates.' 553 554 print ' Default is the location of the site-wide templates installed with Trac.' … … 605 606 self._do_wiki_load(default_dir('wiki'), cursor) 606 607 cnx.commit() 607 608 608 print ' Indexing repository' 609 repos = self.__env.get_repository() 610 repos.sync() 609 if repository_dir: 610 try: 611 repos = self.__env.get_repository() 612 if repos: 613 print ' Indexing repository' 614 repos.sync() 615 except util.TracError, e: 616 print>>sys.stderr, """ 617 Warning: %s 611 618 619 You should install the support libraries or the plugin needed 620 for the SCM corresponding to: 621 622 %s 623 """ % (str(e), repository_dir) 612 624 except Exception, e: 613 625 print 'Failed to initialize environment.', e 614 626 traceback.print_exc() -
trac/loader.py
45 45 def enable_modules(egg_path, modules): 46 46 """Automatically enable any components provided by plugins loaded from 47 47 the environment plugins directory.""" 48 if os.path.dirname(egg_path) == os.path.realpath(plugins_dir): 48 plugins_path = os.path.realpath(plugins_dir) 49 if os.name == 'nt': 50 plugins_path = plugins_path.lower() 51 if os.path.dirname(egg_path) == plugins_path: 49 52 for module in modules: 50 53 env.config.setdefault('components', module + '.*', 'enabled') 51 54 -
templates/browser.cs
