Changeset 9125
- Timestamp:
- Feb 3, 2010, 6:07:11 PM (14 years ago)
- Location:
- trunk
- Files:
-
- 2 deleted
- 52 edited
- 18 copied
Legend:
- Unmodified
- Added
- Removed
-
trunk
- Property svn:mergeinfo changed
-
trunk/TESTING-README
- Property svn:eol-style deleted
- Property svn:mime-type deleted
-
trunk/contrib/bugzilla2trac.py
- Property svn:keywords deleted
r8743 r9125 17 17 Reworked, Jeroen Ruigrok van der Werven <asmodai@in-nomine.org> 18 18 19 $Id $19 $Id: bugzilla2trac.py 8743 2009-10-30 21:49:24Z rblank $ 20 20 """ 21 21 -
trunk/contrib/checkwiki.py
r7628 r9125 46 46 "TracQuery", 47 47 "TracReports", 48 "TracRepositoryAdmin", 48 49 "TracRevisionLog", 49 50 "TracRoadmap", -
trunk/sample-plugins/revision_links.py
r6326 r9125 23 23 def get_wiki_syntax(self): 24 24 def revlink(f, match, fullmatch): 25 rev = match.split(' ', 1)[1] # ignore keyword 26 return self._format_revision_link(f, 'revision', rev, rev, 25 elts = match.split() 26 rev = elts[1] # ignore keyword 27 reponame = '' 28 if len(elts) > 2: # reponame specified 29 reponame = elts[-1] 30 return self._format_revision_link(f, 'revision', reponame, rev, rev, 27 31 fullmatch) 28 32 29 yield (r"!?(?:%s)\s+%s" % ("|".join(self.KEYWORDS), 30 ChangesetModule.CHANGESET_ID), 31 revlink) 33 yield (r"!?(?:%s)\s+%s(?:\s+in\s+\w+)?" % 34 ("|".join(self.KEYWORDS), ChangesetModule.CHANGESET_ID), revlink) 32 35 33 36 def get_link_resolvers(self): 34 yield ('revision', self._format_revision_link) 37 def resolverev(f, ns, rev, label, fullmatch): 38 return self._format_revision_link(f, ns, '', rev, label, fullmatch) 39 yield ('revision', resolverev) 35 40 36 def _format_revision_link(self, formatter, ns, rev, label, fullmatch=None): 41 def _format_revision_link(self, formatter, ns, reponame, rev, label, 42 fullmatch=None): 37 43 rev, params, fragment = formatter.split_link(rev) 38 44 try: 39 changeset = self.env.get_repository().get_changeset(rev) 40 return tag.a(label, class_="changeset", 41 title=shorten_line(changeset.message), 42 href=(formatter.href.changeset(rev) + 43 params + fragment)) 45 repos = self.env.get_repository(reponame) 46 if repos: 47 changeset = repos.get_changeset(rev) 48 return tag.a(label, class_="changeset", 49 title=shorten_line(changeset.message), 50 href=(formatter.href.changeset(rev) + 51 params + fragment)) 44 52 except NoSuchChangeset: 45 return tag.a(label, class_="missing changeset", 46 href=formatter.href.changeset(rev), 47 rel="nofollow") 53 pass 54 return tag.a(label, class_="missing changeset", rel="nofollow", 55 href=formatter.href.changeset(rev)) 56 -
trunk/setup.cfg
r8527 r9125 1 1 [egg_info] 2 tag_build = dev2 tag_build = multirepos 3 3 tag_svn_revision = true 4 4 -
trunk/setup.py
r8999 r9125 109 109 trac.timeline = trac.timeline.web_ui 110 110 trac.versioncontrol.admin = trac.versioncontrol.admin 111 trac.versioncontrol.svn_authz = trac.versioncontrol.svn_authz 111 112 trac.versioncontrol.svn_fs = trac.versioncontrol.svn_fs 112 113 trac.versioncontrol.svn_prop = trac.versioncontrol.svn_prop … … 122 123 tracopt.perm.authz_policy = tracopt.perm.authz_policy 123 124 tracopt.perm.config_perm_provider = tracopt.perm.config_perm_provider 125 tracopt.ticket.commit_updater = tracopt.ticket.commit_updater 124 126 """, 125 127 -
trunk/trac/admin/console.py
r9037 r9125 71 71 import readline 72 72 delims = readline.get_completer_delims() 73 for c in '-/: ':73 for c in '-/:()': 74 74 delims = delims.replace(c, '') 75 75 readline.set_completer_delims(delims) … … 308 308 ## Initenv 309 309 _help_initenv = [ 310 ('initenv', '[<projectname> <db> <repostype> <repospath>]',310 ('initenv', '[<projectname> <db> [<repostype> <repospath>]]', 311 311 """Create and initialize a new environment 312 312 … … 349 349 prompt = _("Database connection string [%(default)s]> ", default=ddb) 350 350 returnvals.append(raw_input(prompt).strip() or ddb) 351 printout(_("""352 Please specify the type of version control system,353 By default, it will be svn.354 355 If you don't want to use Trac with version control integration,356 choose the default here and don\'t specify a repository directory.357 in the next question.358 """))359 drpt = 'svn'360 prompt = _("Repository type [%(default)s]> ", default=drpt)361 returnvals.append(raw_input(prompt).strip() or drpt)362 printout(_("""363 Please specify the absolute path to the version control364 repository, or leave it blank to use Trac without a repository.365 You can also set the repository location later.366 """))367 prompt = _("Path to repository [/path/to/repos]> ")368 returnvals.append(raw_input(prompt).strip())369 351 print 370 352 return returnvals … … 394 376 project_name = None 395 377 db_str = None 378 repository_type = None 396 379 repository_dir = None 397 380 if len(arg) == 1 and not arg[0]: 398 returnvals = self.get_initenv_args() 399 project_name, db_str, repository_type, repository_dir = returnvals 400 elif len(arg) != 4: 381 project_name, db_str = self.get_initenv_args() 382 elif len(arg) == 2: 383 project_name, db_str = arg 384 elif len(arg) == 4: 385 project_name, db_str, repository_type, repository_dir = arg 386 else: 401 387 initenv_error('Wrong number of arguments: %d' % len(arg)) 402 388 return 2 403 else:404 project_name, db_str, repository_type, repository_dir = arg[:4]405 389 406 390 try: 407 391 printout(_("Creating and Initializing Project")) 408 392 options = [ 393 ('project', 'name', project_name), 409 394 ('trac', 'database', db_str), 410 ('trac', 'repository_type', repository_type),411 ('trac', 'repository_dir', repository_dir),412 ('project', 'name', project_name),413 395 ] 396 if repository_dir: 397 options.extend([ 398 ('trac', 'repository_type', repository_type), 399 ('trac', 'repository_dir', repository_dir), 400 ]) 414 401 if inherit_paths: 415 402 options.append(('inherit', 'file', … … 436 423 repos = self.__env.get_repository() 437 424 if repos: 438 printout(_(" Indexing repository"))425 printout(_(" Indexing default repository")) 439 426 repos.sync(self._resync_feedback) 440 427 except TracError, e: 441 428 printerr(_(""" 442 429 --------------------------------------------------------------------- 443 Warning: couldn't index the repository.430 Warning: couldn't index the default repository. 444 431 445 432 This can happen for a variety of reasons: wrong repository type, … … 449 436 You can nevertheless start using your Trac environment, but 450 437 you'll need to check again your trac.ini file and the [trac] 451 repository_type and repository_path settings in order to enable 452 the Trac repository browser. 438 repository_type and repository_path settings. 453 439 """)) 454 440 except Exception, e: -
trunk/trac/admin/tests/console-tests.txt
r8956 r9125 12 12 attachment list List attachments of a resource 13 13 attachment remove Remove an attachment from a resource 14 changeset added Notify trac about changesets added to a repository 15 changeset modified Notify trac about changesets modified in a repository 14 16 component add Add a new component 15 17 component chown Change component ownership … … 36 38 priority order Move a priority value up or down in the list 37 39 priority remove Remove a priority value 40 repository add Add a source repository 41 repository alias Create an alias for a repository 42 repository list List source repositories 43 repository remove Remove a source repository 44 repository resync Re-synchronize trac with repositories 45 repository set Set an attribute of a repository 46 repository sync Resume synchronization of repositories 38 47 resolution add Add a resolution value option 39 48 resolution change Change a resolution value … … 41 50 resolution order Move a resolution value up or down in the list 42 51 resolution remove Remove a resolution value 43 resync Re-synchronize trac with the repository44 52 severity add Add a severity value option 45 53 severity change Change a severity value -
trunk/trac/db_default.py
r8734 r9125 18 18 19 19 # Database version identifier. Used for automatic upgrades. 20 db_version = 2 220 db_version = 24 21 21 22 22 def __mkreports(reports): … … 86 86 87 87 # Version control cache 88 Table('revision', key='rev')[ 88 Table('repository', key=('id', 'name'))[ 89 Column('id', type='int'), 90 Column('name'), 91 Column('value')], 92 Table('revision', key=('repos', 'rev'))[ 93 Column('repos', type='int'), 89 94 Column('rev'), 90 95 Column('time', type='int'), 91 96 Column('author'), 92 97 Column('message'), 93 Index(['time'])], 94 Table('node_change', key=('rev', 'path', 'change_type'))[ 98 Index(['repos', 'time'])], 99 Table('node_change', key=('repos', 'rev', 'path', 'change_type'))[ 100 Column('repos', type='int'), 95 101 Column('rev'), 96 102 Column('path'), … … 99 105 Column('base_path'), 100 106 Column('base_rev'), 101 Index(['re v'])],107 Index(['repos', 'rev'])], 102 108 103 109 # Ticket system … … 388 394 ('name', 'value'), 389 395 (('database_version', str(db_version)), 390 ('initial_database_version', str(db_version)), 391 ('youngest_rev', ''))), 396 ('initial_database_version', str(db_version)))), 392 397 ('report', 393 398 ('author', 'title', 'query', 'description'), -
trunk/trac/env.py
r9037 r9125 282 282 cname = cname[:idx] 283 283 284 # versioncontrol components are enabled if the repository is configured285 # FIXME: this shouldn't be hardcoded like this286 if component_name.startswith('trac.versioncontrol.'):287 return self.config.get('trac', 'repository_dir') != ''288 289 284 # By default, all components in the trac package are enabled 290 285 return component_name.startswith('trac.') or None … … 319 314 del self.log._trac_handler 320 315 321 def get_repository(self, authname=None):316 def get_repository(self, reponame=None, authname=None): 322 317 """Return the version control repository configured for this 323 318 environment. … … 325 320 @param authname: user name for authorization 326 321 """ 327 return RepositoryManager(self).get_repository( authname)322 return RepositoryManager(self).get_repository(reponame) 328 323 329 324 def create(self, options=[]): … … 468 463 """Upgrade database. 469 464 470 Each db version should have its own upgrade module, name s465 Each db version should have its own upgrade module, named 471 466 upgrades/dbN.py, where 'N' is the version number (int). 472 467 -
trunk/trac/htdocs/css/browser.css
r8833 r9125 45 45 /* Styles for the directory entries table 46 46 (extends the styles for "table.listing") */ 47 #dirlist { margin-top: 0 }48 #dirlist td.rev, #dirlist td.age, #dirlist td.change {47 table.dirlist { margin-top: 0 } 48 table.dirlist td.rev, table.dirlist td.age, table.dirlist td.change { 49 49 color: #888; 50 50 white-space: nowrap; 51 vertical-align: baseline;52 } 53 #dirlist td.rev {51 vertical-align: middle; 52 } 53 table.dirlist td.rev { 54 54 font-family: monospace; 55 55 letter-spacing: -0.08em; … … 57 57 text-align: right; 58 58 } 59 #dirlist td.size {59 table.dirlist td.size { 60 60 color: #888; 61 61 white-space: nowrap; … … 64 64 font-size: 70%; 65 65 } 66 #dirlist td.age {66 table.dirlist td.age { 67 67 border-width: 0 2px 0 0; 68 68 border-style: solid; 69 69 font-size: 85%; 70 70 } 71 #dirlist td.name { width: 100% }72 #dirlist td.name a, #dirlist td.name span {71 table.dirlist td.name { width: 100% } 72 table.dirlist td.name a, table.dirlist td.name span { 73 73 background-position: 0% 50%; 74 74 background-repeat: no-repeat; 75 75 padding-left: 20px; 76 76 } 77 #dirlist td.name a.parent { background-image: url(../parent.png) }78 #dirlist td.name div { white-space: pre }79 #dirlist tr span.expander {77 table.dirlist td.name a.parent { background-image: url(../parent.png) } 78 table.dirlist td.name div { white-space: pre } 79 table.dirlist tr span.expander { 80 80 background-image: url(../expander_normal.png); 81 81 cursor: pointer; … … 83 83 margin-left: 4px; 84 84 } 85 #dirlist tr span.expander:hover {85 table.dirlist tr span.expander:hover { 86 86 background-image: url(../expander_normal_hover.png); 87 87 } 88 #dirlist tr.expanded span.expander {88 table.dirlist tr.expanded span.expander { 89 89 background-image: url(../expander_open.png); 90 90 padding-left: 12px; 91 91 margin-left: 0; 92 92 } 93 #dirlist tr.expanded span.expander:hover {93 table.dirlist tr.expanded span.expander:hover { 94 94 background-image: url(../expander_open_hover.png); 95 95 } 96 #dirlist td.name a.dir { background-image: url(../folder.png) } 97 #dirlist td.name a.file { background-image: url(../file.png); display: block } 98 #dirlist td.name a, #dirlist td.rev a { border-bottom: none } 99 #dirlist td.rev { text-align: right } 100 #dirlist td.change { 101 font-size: 85%; 102 vertical-align: middle; 103 white-space: nowrap 104 } 105 106 #dirlist td span.loading { 96 table.dirlist td.name a.dir { background-image: url(../folder.png) } 97 table.dirlist td.name a.file { background-image: url(../file.png); display: block } 98 table.dirlist td.name a, table.dirlist td.rev a { border-bottom: none } 99 table.dirlist td.change { font-size: 85% } 100 table.dirlist td.rev a.chgset { 101 background-repeat: no-repeat; 102 background-image: url(../changeset.png); 103 background-position: 100% 50%; 104 padding: 0 0 0 5px; 105 margin: 0 5px 0 0; 106 } 107 table.dirlist td.description { padding-left: 2em } 108 table.dirlist td.description > :first-child { margin-top: 0 } 109 table.dirlist td.description > :last-child { margin-bottom: 0 } 110 111 table.dirlist td span.loading { 107 112 background-image: url(../loading.gif); 108 113 font-style: italic 109 114 } 115 116 #content.browser div.description { padding: 0 0.5em } 110 117 111 118 /* Style for the ''View Changes'' button and the diff preparation form */ … … 127 134 /* Styles for the revision log table (extends the styles for "table.listing") */ 128 135 table.chglist { margin-top: 0 } 136 .chglist td.diff, .chglist td.rev, .chglist td.age, 137 .chglist td.author, .chglist td.change { 138 white-space: nowrap; 139 vertical-align: middle; 140 } 129 141 .chglist td.change span { 130 142 border: 1px solid #999; … … 133 145 width: .8em; height: .8em; 134 146 } 135 .chglist td.diff { white-space: nowrap}147 .chglist td.diff { padding: 1px } 136 148 .chglist td.change .comment { display: none } 137 .chglist td.old_path { font-style: italic } 138 .chglist td.date { 149 .chglist td.age { 139 150 font-size: 85%; 140 vertical-align: top;141 151 padding-top: 0.55em; 142 white-space: nowrap; 143 } 144 .chglist td.author { font-size: 85%; vertical-align: top; padding-top: 0.55em } 145 .chglist td.rev, .chglist td.chgset { 152 } 153 .chglist td.author { font-size: 85%; } 154 .chglist td.rev { 146 155 font-family: monospace; 147 156 letter-spacing: -0.08em; … … 149 158 text-align: right; 150 159 } 151 .chglist td.rev a, .chglist td.chgset a { border-bottom: none } 152 .chglist td.summary { 160 .chglist td.rev a { border-bottom: none } 161 .chglist td.rev a.chgset { 162 background-repeat: no-repeat; 163 background-image: url(../changeset.png); 164 background-position: 100% 50%; 165 padding: 0 0 0 5px; 166 margin: 0 5px 0 0; 167 } 168 169 .chglist td.summary, .chglist td.log { 153 170 width: 100%; 154 171 font-size: 85%; 155 172 vertical-align: middle; 156 white-space: nowrap; 157 } 158 .chglist tr.verbose td.summary { 173 } 174 .chglist td.summary *, .chglist td.log * { margin-top: 0 } 175 /* verbose mode */ 176 .chglist tr.verbose { border-top: none } 177 .chglist tr.verbose td.filler, .chglist tr.verbose td.log { 159 178 border: none; 179 border-bottom: 1px solid #ddd; 160 180 color: #333; 161 padding: .5em 1em 1em 2em; 162 white-space: normal; 163 } 164 165 .chglist td.summary * { margin-top: 0 } 181 } 182 .chglist tr.verbose td { border: none; } 183 .chglist tr.verbose td.diff, .chglist tr.verbose td.filler { 184 border-left: 1px solid #ddd; 185 } 186 .chglist tr.verbose td.summary, .chglist tr.verbose td.log { 187 border-right: 1px solid #ddd; 188 } 166 189 167 190 #paging { margin: 1em 0 } -
trunk/trac/htdocs/css/trac.css
r9030 r9125 88 88 option { border-bottom: 1px dotted #d7d7d7 } 89 89 fieldset { border: 1px solid #d7d7d7; padding: .5em; margin: 1em 0 } 90 form p.hint, formspan.hint { color: #666; font-size: 85%; font-style: italic; margin: .5em 0;90 p.hint, span.hint { color: #666; font-size: 85%; font-style: italic; margin: .5em 0; 91 91 padding-left: 1em; 92 92 } -
trunk/trac/htdocs/js/blame.js
r7679 r9125 2 2 (function($){ 3 3 4 window.enableBlame = function(url, original_path) {4 window.enableBlame = function(url, reponame, original_path) { 5 5 var message = null; 6 6 var message_rev = null; … … 18 18 a.removeAttr("href"); 19 19 href = href.slice(href.indexOf("changeset/") + 10); 20 if (reponame) 21 href = href.substr(reponame.length); 20 22 var sep = href.indexOf("/"); 21 23 if ( sep > 0 ) … … 72 74 highlight_rev = message_rev; 73 75 74 $.get(url + rev.substr(1), {annotate: annotate_path}, function(data) { 76 $.get(url + [rev.substr(1), reponame].join("/"), 77 {annotate: annotate_path}, function(data) { 75 78 // remove former message panel if any 76 79 if (message) -
trunk/trac/mimeview/api.py
r8899 r9125 128 128 @classmethod 129 129 def from_request(cls, req, resource=None, id=False, version=False, 130 absurls=False):130 parent=False, absurls=False): 131 131 """Create a rendering context from a request. 132 132 … … 158 158 href = None 159 159 perm = None 160 self = cls(Resource(resource, id=id, version=version ), href=href,161 perm=perm)160 self = cls(Resource(resource, id=id, version=version, parent=parent), 161 href=href, perm=perm) 162 162 self.req = req 163 163 return self … … 172 172 return '<%s %s>' % (type(self).__name__, ' - '.join(reversed(path))) 173 173 174 def __call__(self, resource=None, id=False, version=False ):174 def __call__(self, resource=None, id=False, version=False, parent=False): 175 175 """Create a nested rendering context. 176 176 … … 196 196 """ 197 197 if resource: 198 resource = Resource(resource, id=id, version=version) 198 resource = Resource(resource, id=id, version=version, 199 parent=parent) 199 200 else: 200 201 resource = self.resource -
trunk/trac/resource.py
r8899 r9125 188 188 return resource 189 189 190 191 190 def __call__(self, realm=False, id=False, version=False, parent=False): 192 191 """Create a new Resource using the current resource as a template. … … 207 206 "<Resource u', attachment:file.txt'>" 208 207 """ 209 return self.__call__(realm, id, version, self) 210 208 return Resource(realm, id, version, self) 211 209 212 210 -
trunk/trac/templates/diff_view.html
r8564 r9125 33 33 <input type="hidden" name="version" value="$new_version" /> 34 34 <input type="hidden" name="old_version" value="$old_version" /> 35 ${diff_options_fields(diff)}35 <xi:include href="diff_options.html" /> 36 36 </div> 37 37 </form> -
trunk/trac/templates/error.html
r9030 r9125 85 85 ==== Python Traceback ==== 86 86 {{{ 87 ${t raceback}87 ${to_unicode(traceback)} 88 88 }}}</textarea> 89 89 <span class="inlinebuttons"> -
trunk/trac/templates/macros.html
r8907 r9125 19 19 pretty_size(size) 20 20 }</span></py:def> 21 22 <!--! Display author information, eventually obfuscating the e-mail address23 -24 - We take care to not insert any extra space.25 -->26 <py:def function="authorinfo(author, email_map=None)"><py:choose><py:when test="author"><py:with27 vars="author = show_email_addresses and email_map and '@' not in author and email_map[author] or author">${28 author and format_author(author) or 'anonymous'29 }</py:with></py:when><py:otherwise>anonymous</py:otherwise></py:choose></py:def>30 31 <!--! Display a sequence of path components.32 -33 - Each component is a link to the corresponding location in the browser.34 -->35 <py:def function="browser_path_links(path_links,rev=None)">36 <py:for each="idx, part in enumerate(path_links)"><py:with37 vars="first = idx == 0; last = idx == len(path_links) - 1"><a38 class="${classes('pathentry', first=first)}"39 title="${first and _('Go to root directory') or _('View %(folder)s', folder=part.name)}"40 href="$part.href">$part.name</a><py:if41 test="not last"><span class="pathentry sep">/</span></py:if></py:with></py:for>42 <py:if test="rev"><span class="pathentry sep">@</span>43 <a class="pathentry" href="${href.changeset(rev)}" title="View changeset $rev">$rev</a>44 </py:if>45 <br style="clear: both" />46 </py:def>47 21 48 22 <!--! Add Previous/Up/Next navigation links … … 75 49 </ul> 76 50 77 <!--! Add diff option fields (to be used inside a form)78 -79 - `diff` the datastructure which contains diff options80 -81 -->82 <py:def function="diff_options_fields(diff)">83 <label for="style">View differences</label>84 <select id="style" name="style">85 <option selected="${diff.style == 'inline' or None}"86 value="inline">inline</option>87 <option selected="${diff.style == 'sidebyside' or None}"88 value="sidebyside">side by side</option>89 </select>90 <div class="field">91 Show <input type="text" name="contextlines" id="contextlines" size="2"92 maxlength="3" value="${diff.options.contextlines < 0 and 'all' or diff.options.contextlines}" />93 <label for="contextlines">lines around each change</label>94 </div>95 <fieldset id="ignore" py:with="options = diff.options">96 <legend>Ignore:</legend>97 <div class="field">98 <input type="checkbox" id="ignoreblanklines" name="ignoreblanklines"99 checked="${options.ignoreblanklines or None}" />100 <label for="ignoreblanklines">Blank lines</label>101 </div>102 <div class="field">103 <input type="checkbox" id="ignorecase" name="ignorecase"104 checked="${options.ignorecase or None}" />105 <label for="ignorecase">Case changes</label>106 </div>107 <div class="field">108 <input type="checkbox" id="ignorewhitespace" name="ignorewhitespace"109 checked="${options.ignorewhitespace or None}" />110 <label for="ignorewhitespace">White space changes</label>111 </div>112 </fieldset>113 <div class="buttons">114 <input type="submit" name="update" value="${_('Update')}" />115 </div>116 </py:def>117 51 118 52 <!--! Display a div for visualizing a preview of a file content -
trunk/trac/test.py
r8802 r9125 98 98 """Fake permission class. Necessary as Mock can not be used with operator 99 99 overloading.""" 100 101 username = '' 102 100 103 def has_permission(self, action, realm_or_resource=None, id=False, 101 104 version=False): -
trunk/trac/ticket/admin.py
r8910 r9125 25 25 from trac.util.text import print_table, printout, exception_to_unicode 26 26 from trac.util.translation import _, N_, gettext 27 from trac.web.chrome import add_notice, add_script, add_warning, Chrome27 from trac.web.chrome import Chrome, add_notice, add_warning 28 28 29 29 -
trunk/trac/util/__init__.py
r9045 r9125 871 871 value = max 872 872 return value 873 874 def pathjoin(*args): 875 """Strip `/` from the arguments and join them with a single `/`.""" 876 return '/'.join(filter(None, (each.strip('/') for each in args if each))) -
trunk/trac/util/text.py
r8884 r9125 249 249 return address 250 250 251 def breakable_path(path): 252 """Make a path breakable after path separators, and conversely, avoid 253 breaking at spaces. 254 """ 255 if not path: 256 return path 257 prefix = '' 258 if path.startswith('/'): # Avoid breaking after a leading / 259 prefix = '/' 260 path = path[1:] 261 return prefix + path.replace('/', u'/\u200b').replace('\\', u'\\\u200b') \ 262 .replace(' ', u'\u00a0') 263 264 def normalize_whitespace(text, to_space=u'\u00a0', remove=u'\u200b'): 265 """Normalize whitespace in a string, by replacing special spaces by normal 266 spaces and removing zero-width spaces.""" 267 if not text: 268 return text 269 return text.replace(u'\u00a0', ' ').replace(u'\u200b', '') 270 251 271 # -- Conversion 252 272 -
trunk/trac/versioncontrol/admin.py
r8899 r9125 14 14 import sys 15 15 16 from trac.admin import IAdminCommandProvider 16 from trac.admin import IAdminCommandProvider, IAdminPanelProvider 17 from trac.config import _TRUE_VALUES 17 18 from trac.core import * 18 from trac.util.text import printout 19 from trac.util.text import breakable_path, normalize_whitespace, print_table, \ 20 printout 19 21 from trac.util.translation import _, ngettext 22 from trac.versioncontrol import DbRepositoryProvider, RepositoryManager, \ 23 is_default 24 from trac.web.chrome import Chrome, add_notice, add_warning 20 25 21 26 … … 23 28 """trac-admin command provider for version control administration.""" 24 29 25 implements(IAdminCommandProvider )30 implements(IAdminCommandProvider, IAdminPanelProvider) 26 31 27 32 # IAdminCommandProvider methods 28 33 29 34 def get_admin_commands(self): 30 yield ('resync', '[rev]', 31 """Re-synchronize trac with the repository 35 yield ('changeset added', '<repos> <rev> [rev] [...]', 36 """Notify trac about changesets added to a repository 37 38 This command should be called from a post-commit hook. It will 39 trigger a cache update and notify components about the addition. 40 """, 41 self._complete_repos, self._do_changeset_added) 42 yield ('changeset modified', '<repos> <rev> [rev] [...]', 43 """Notify trac about changesets modified in a repository 44 45 This command should be called from a post-revprop hook after 46 revision properties like the commit message, author or date 47 have been changed. It will trigger a cache update for the given 48 revisions and notify components about the change. 49 """, 50 self._complete_repos, self._do_changeset_modified) 51 yield ('repository list', '', 52 'List source repositories', 53 None, self._do_list) 54 yield ('repository resync', '<repos> [rev]', 55 """Re-synchronize trac with repositories 32 56 33 57 When [rev] is specified, only that revision is synchronized. 34 58 Otherwise, the complete revision history is synchronized. Note 35 59 that this operation can take a long time to complete. 36 """, 37 None, self._do_resync) 38 39 def _do_resync(self, rev=None): 40 if rev: 41 self.env.get_repository().sync_changeset(rev) 42 printout(_('%(rev)s resynced.', rev=rev)) 43 return 44 from trac.versioncontrol.cache import CACHE_METADATA_KEYS 45 printout(_('Resyncing repository history... ')) 60 If synchronization gets interrupted, it can be resumed later 61 using the `sync` command. 62 63 To synchronize all repositories, specify "*" as the repository. 64 """, 65 self._complete_repos, self._do_resync) 66 yield ('repository sync', '<repos> [rev]', 67 """Resume synchronization of repositories 68 69 Similar to `resync`, but doesn't clear the already synchronized 70 changesets. Useful for resuming an interrupted `resync`. 71 72 To synchronize all repositories, specify "*" as the repository. 73 """, 74 self._complete_repos, self._do_sync) 75 76 def get_reponames(self): 77 rm = RepositoryManager(self.env) 78 return [reponame or _('(default)') for reponame 79 in rm.get_all_repositories()] 80 81 def _complete_repos(self, args): 82 if len(args) == 1: 83 return self.get_reponames() 84 85 def _do_changeset_added(self, reponame, *revs): 86 if is_default(reponame): 87 reponame = '' 88 rm = RepositoryManager(self.env) 89 rm.notify('changeset_added', reponame, revs) 90 91 def _do_changeset_modified(self, reponame, *revs): 92 if is_default(reponame): 93 reponame = '' 94 rm = RepositoryManager(self.env) 95 rm.notify('changeset_modified', reponame, revs) 96 97 def _do_list(self): 98 rm = RepositoryManager(self.env) 99 values = [] 100 for (reponame, info) in sorted(rm.get_all_repositories().iteritems()): 101 alias = '' 102 if 'alias' in info: 103 alias = info['alias'] or _('(default)') 104 values.append((reponame or _('(default)'), info.get('type', ''), 105 alias, info.get('dir', ''))) 106 print_table(values, [_('Name'), _('Type'), _('Alias'), _('Directory')]) 107 108 def _sync(self, reponame, rev, clean): 109 rm = RepositoryManager(self.env) 110 if reponame == '*': 111 if rev is not None: 112 raise TracError(_('Cannot synchronize a single revision ' 113 'on multiple repositories')) 114 repositories = rm.get_real_repositories() 115 else: 116 if is_default(reponame): 117 reponame = '' 118 repos = rm.get_repository(reponame) 119 if repos is None: 120 raise TracError(_("Unknown repository '%(reponame)s'", 121 reponame=reponame or _('(default)'))) 122 if rev is not None: 123 repos.sync_changeset(rev) 124 printout(_('%(rev)s resynced on %(reponame)s.', rev=rev, 125 reponame=repos.reponame or _('(default)'))) 126 return 127 repositories = [repos] 128 46 129 db = self.env.get_db_cnx() 47 130 cursor = db.cursor() 48 cursor.execute("DELETE FROM revision") 49 cursor.execute("DELETE FROM node_change") 50 cursor.executemany("DELETE FROM system WHERE name=%s", 51 [(k,) for k in CACHE_METADATA_KEYS]) 52 cursor.executemany("INSERT INTO system (name, value) VALUES (%s, %s)", 53 [(k, '') for k in CACHE_METADATA_KEYS]) 54 db.commit() 55 self.env.get_repository().sync(self._resync_feedback) 56 cursor.execute("SELECT count(rev) FROM revision") 57 for cnt, in cursor: 58 printout(ngettext('%(num)s revision cached.', 59 '%(num)s revisions cached.', num=cnt)) 131 for repos in sorted(repositories, key=lambda r: r.reponame): 132 printout(_('Resyncing repository history for %(reponame)s... ', 133 reponame=repos.reponame or _('(default)'))) 134 repos.sync(self._sync_feedback, clean=clean) 135 cursor.execute("SELECT count(rev) FROM revision WHERE repos=%s", 136 (repos.id,)) 137 for cnt, in cursor: 138 printout(ngettext('%(num)s revision cached.', 139 '%(num)s revisions cached.', num=cnt)) 60 140 printout(_('Done.')) 61 141 62 def _ resync_feedback(self, rev):142 def _sync_feedback(self, rev): 63 143 sys.stdout.write(' [%s]\r' % rev) 64 144 sys.stdout.flush() 145 146 def _do_resync(self, reponame, rev=None): 147 self._sync(reponame, rev, clean=True) 148 149 def _do_sync(self, reponame, rev=None): 150 self._sync(reponame, rev, clean=False) 151 152 # IAdminPanelProvider methods 153 154 def get_admin_panels(self, req): 155 if 'TICKET_ADMIN' in req.perm: 156 yield ('versioncontrol', 'Version Control', 'repository', 157 _('Repositories')) 158 159 def render_admin_panel(self, req, category, page, path_info): 160 req.perm.require('TICKET_ADMIN') 161 162 # Retrieve info for all repositories 163 rm = RepositoryManager(self.env) 164 all_repos = rm.get_all_repositories() 165 db_provider = self.env[DbRepositoryProvider] 166 167 if path_info: 168 # Detail view 169 reponame = not is_default(path_info) and path_info or '' 170 info = all_repos.get(reponame) 171 if info is None: 172 raise TracError(_('Repository %(name)s does not exist.', 173 name=path_info)) 174 if req.method == 'POST': 175 if req.args.get('cancel'): 176 req.redirect(req.href.admin(category, page)) 177 178 elif db_provider and req.args.get('save'): 179 # Modify repository 180 changes = {} 181 for field in db_provider.repository_attrs: 182 value = normalize_whitespace(req.args.get(field)) 183 if (value is not None or field == 'hidden') \ 184 and value != info.get(field): 185 changes[field] = value 186 if changes: 187 db_provider.modify_repository(reponame, changes) 188 add_notice(req, _('Your changes have been saved.')) 189 name = req.args.get('name') 190 if 'dir' in changes: 191 msg = _('You should now run "trac-admin $ENV ' 192 'repository resync %(name)s" to synchronize ' 193 'Trac with the repository.', name=name) 194 add_notice(req, msg) 195 elif 'type' in changes: 196 msg = _('You may have to run "trac-admin $ENV ' 197 'repository resync %(name)s" to synchronize ' 198 'Trac with the repository.', name=name) 199 add_notice(req, msg) 200 if name and name != path_info and not 'alias' in info: 201 msg = _('You will need to update your post-commit ' 202 'hook to call "trac-admin $ENV changeset ' 203 'added" with the new repository name.') 204 add_notice(req, msg) 205 req.redirect(req.href.admin(category, page)) 206 207 Chrome(self.env).add_wiki_toolbars(req) 208 data = {'view': 'detail', 'reponame': reponame} 209 210 else: 211 # List view 212 if req.method == 'POST': 213 # Add a repository 214 if db_provider and req.args.get('add_repos'): 215 name = req.args.get('name') 216 type_ = req.args.get('type') 217 dir = req.args.get('dir') 218 if name is not None and type_ is not None and dir: 219 # Avoid errors when copy/pasting paths 220 dir = normalize_whitespace(dir) 221 db_provider.add_repository(name, dir, type_) 222 add_notice(req, _('The repository "%(name)s" has been ' 223 'added.', name=name)) 224 msg = _('You should now run "trac-admin $ENV ' 225 'repository resync %(name)s" to synchronize ' 226 'Trac with the repository.', 227 name=name or _('(default)')) 228 add_notice(req, msg) 229 msg = _('You should also set up a post-commit hook ' 230 'on the repository to call "trac-admin $ENV ' 231 'changeset added %(name)s $REV" for each ' 232 'committed changeset.', name=name) 233 add_notice(req, msg) 234 req.redirect(req.href.admin(category, page)) 235 add_warning(req, _('Missing arguments to add a ' 236 'repository.')) 237 238 # Add a repository alias 239 elif db_provider and req.args.get('add_alias'): 240 name = req.args.get('name') 241 alias = req.args.get('alias') 242 if name is not None and alias is not None: 243 db_provider.add_alias(name, alias) 244 add_notice(req, _('The alias "%(name)s" has been ' 245 'added.', name=name)) 246 req.redirect(req.href.admin(category, page)) 247 add_warning(req, _('Missing arguments to add an ' 248 'alias.')) 249 250 # Refresh the list of repositories 251 elif req.args.get('refresh'): 252 req.redirect(req.href.admin(category, page)) 253 254 # Remove repositories 255 elif db_provider and req.args.get('remove'): 256 sel = req.args.getlist('sel') 257 if sel: 258 for name in sel: 259 db_provider.remove_repository(name) 260 add_notice(req, _('The selected repositories have ' 261 'been removed.')) 262 req.redirect(req.href.admin(category, page)) 263 add_warning(req, _('No repositories were selected.')) 264 265 data = {'view': 'list'} 266 267 # Find repositories that are editable 268 db_repos = {} 269 if db_provider is not None: 270 db_repos = dict(db_provider.get_repositories()) 271 272 # Prepare common rendering data 273 repositories = dict((reponame, self._extend_info(reponame, info.copy(), 274 reponame in db_repos)) 275 for (reponame, info) in all_repos.iteritems()) 276 types = sorted([''] + rm.get_supported_types()) 277 data.update({'types': types, 'default_type': rm.repository_type, 278 'repositories': repositories}) 279 280 return 'admin_repositories.html', data 281 282 def _extend_info(self, reponame, info, editable): 283 """Extend repository info for rendering.""" 284 info['name'] = reponame 285 if info.get('dir') is not None: 286 info['prettydir'] = breakable_path(info['dir']) or '' 287 if info.get('alias') == '': 288 info['alias'] = _('(default)') 289 info['hidden'] = info.get('hidden') in _TRUE_VALUES 290 info['editable'] = editable 291 if not info.get('alias'): 292 try: 293 repos = RepositoryManager(self.env).get_repository(reponame) 294 info['rev'] = repos.get_youngest_rev() 295 except Exception: 296 pass 297 return info -
trunk/trac/versioncontrol/api.py
r8899 r9125 16 16 17 17 import os.path 18 import time 18 19 19 20 try: … … 23 24 threading._get_ident = lambda: 0 24 25 25 from trac.config import Option 26 from trac.admin import AdminCommandError, IAdminCommandProvider 27 from trac.config import ListOption, Option 26 28 from trac.core import * 27 from trac.perm import PermissionError 28 from trac.resource import IResourceManager, ResourceNotFound 29 from trac.util.text import to_unicode 29 from trac.resource import IResourceManager, Resource, ResourceNotFound 30 from trac.util.text import printout, to_unicode 30 31 from trac.util.translation import _ 31 32 from trac.web.api import IRequestFilter 33 34 35 def is_default(reponame): 36 """Check whether `reponame` is the default repository.""" 37 return not reponame or reponame in ('(default)', _('(default)')) 32 38 33 39 … … 52 58 """ 53 59 54 def get_repository(repos_type, repos_dir, authname):60 def get_repository(repos_type, repos_dir, params): 55 61 """Return a Repository instance for the given repository type and dir. 56 62 """ 63 64 65 class IRepositoryProvider(Interface): 66 """Provide known named instances of Repository.""" 67 68 def get_repositories(): 69 """Generate repository information for known repositories. 70 71 Repository information is a key,value pair, where the value is 72 a dictionary which must contain at the very least one of the following 73 entries: 74 - `'dir'`: the repository directory which can be used by the 75 connector to create a `Repository` instance 76 - `'alias'`: if set, it is the name of another repository. 77 Optional entries: 78 - `'type'`: the type of the repository (if not given, the default 79 repository type will be used) 80 """ 81 82 83 class IRepositoryChangeListener(Interface): 84 """Listen for changes in repositories.""" 85 86 def changeset_added(repos, changeset): 87 """Called after a changeset has been added to a repository.""" 88 89 def changeset_modified(repos, changeset, old_changeset): 90 """Called after a changeset has been modified in a repository. 91 92 The `old_changeset` argument contains the metadata of the changeset 93 prior to the modification. It is `None` if the old metadata cannot 94 be retrieved. 95 """ 96 97 98 class DbRepositoryProvider(Component): 99 """Component providing repositories registered in the DB.""" 100 101 implements(IRepositoryProvider, IAdminCommandProvider) 102 103 repository_attrs = ('alias', 'description', 'dir', 'hidden', 'name', 104 'type', 'url') 105 106 # IRepositoryProvider methods 107 108 def get_repositories(self): 109 """Retrieve repositories specified in the repository DB table.""" 110 db = self.env.get_db_cnx() 111 cursor = db.cursor() 112 cursor.execute("SELECT id,name,value FROM repository " 113 "WHERE name IN (%s)" % ",".join( 114 "'%s'" % each for each in self.repository_attrs)) 115 repos = {} 116 for id, name, value in cursor: 117 if value is not None: 118 repos.setdefault(id, {})[name] = value 119 reponames = {} 120 for id, info in repos.iteritems(): 121 if 'name' in info and ('dir' in info or 'alias' in info): 122 info['id'] = id 123 reponames[info['name']] = info 124 return reponames.iteritems() 125 126 # IAdminCommandProvider methods 127 128 def get_admin_commands(self): 129 yield ('repository add', '<repos> <dir> [type]', 130 'Add a source repository', 131 self._complete_add, self._do_add) 132 yield ('repository alias', '<name> <target>', 133 'Create an alias for a repository', 134 self._complete_alias, self._do_alias) 135 yield ('repository remove', '<repos>', 136 'Remove a source repository', 137 self._complete_repos, self._do_remove) 138 yield ('repository set', '<repos> <key> <value>', 139 """Set an attribute of a repository 140 141 The following keys are supported: %s 142 """ % ', '.join(self.repository_attrs), 143 self._complete_set, self._do_set) 144 145 def get_reponames(self): 146 rm = RepositoryManager(self.env) 147 return [reponame or _('(default)') for reponame 148 in rm.get_all_repositories()] 149 150 def _complete_add(self, args): 151 if len(args) == 2: 152 return get_dir_list(args[-1], True) 153 elif len(args) == 3: 154 return RepositoryManager(self.env).get_supported_types() 155 156 def _complete_alias(self, args): 157 if len(args) == 2: 158 return self.get_reponames() 159 160 def _complete_repos(self, args): 161 if len(args) == 1: 162 return self.get_reponames() 163 164 def _complete_set(self, args): 165 if len(args) == 1: 166 return self.get_reponames() 167 elif len(args) == 2: 168 return self.repository_attrs 169 170 def _do_add(self, reponame, dir, type_=None): 171 self.add_repository(reponame, dir, type_) 172 173 def _do_alias(self, reponame, target): 174 self.add_alias(reponame, target) 175 176 def _do_remove(self, reponame): 177 self.remove_repository(reponame) 178 179 def _do_set(self, reponame, key, value): 180 if key not in self.repository_attrs: 181 raise AdminCommandError(_('Invalid key "%(key)s"', key=key)) 182 self.modify_repository(reponame, {key: value}) 183 if not reponame: 184 reponame = _('(default)') 185 if key == 'dir': 186 printout(_('You should now run "repository resync %(name)s".', 187 name=reponame)) 188 elif key == 'type': 189 printout(_('You may have to run "repository resync %(name)s".', 190 name=reponame)) 191 192 # Public interface 193 194 def add_repository(self, reponame, dir, type_=None): 195 """Add a repository.""" 196 if is_default(reponame): 197 reponame = '' 198 rm = RepositoryManager(self.env) 199 if type_ and type_ not in rm.get_supported_types(): 200 raise TracError(_("The repository type '%(type)s' is not " 201 "supported", type=type_)) 202 db = self.env.get_db_cnx() 203 id = rm.get_repository_id(reponame, db) 204 cursor = db.cursor() 205 cursor.executemany("INSERT INTO repository (id, name, value) " 206 "VALUES (%s, %s, %s)", 207 [(id, 'dir', dir), 208 (id, 'type', type_ or '')]) 209 db.commit() 210 rm.reload_repositories() 211 212 def add_alias(self, reponame, target): 213 """Create an alias repository.""" 214 if is_default(reponame): 215 reponame = '' 216 if is_default(target): 217 target = '' 218 rm = RepositoryManager(self.env) 219 db = self.env.get_db_cnx() 220 id = rm.get_repository_id(reponame, db) 221 cursor = db.cursor() 222 cursor.executemany("INSERT INTO repository (id, name, value) " 223 "VALUES (%s, %s, %s)", 224 [(id, 'dir', None), 225 (id, 'alias', target)]) 226 db.commit() 227 rm.reload_repositories() 228 229 def remove_repository(self, reponame): 230 """Remove a repository.""" 231 if is_default(reponame): 232 reponame = '' 233 rm = RepositoryManager(self.env) 234 db = self.env.get_db_cnx() 235 id = rm.get_repository_id(reponame, db) 236 cursor = db.cursor() 237 cursor.execute("DELETE FROM repository WHERE id=%s", (id,)) 238 cursor.execute("DELETE FROM revision WHERE repos=%s", (id,)) 239 cursor.execute("DELETE FROM node_change WHERE repos=%s", (id,)) 240 db.commit() 241 rm.reload_repositories() 242 243 def modify_repository(self, reponame, changes): 244 """Modify attributes of a repository.""" 245 if is_default(reponame): 246 reponame = '' 247 rm = RepositoryManager(self.env) 248 db = self.env.get_db_cnx() 249 id = rm.get_repository_id(reponame, db) 250 cursor = db.cursor() 251 for (k, v) in changes.iteritems(): 252 if k not in self.repository_attrs: 253 continue 254 if k in('alias', 'name') and is_default(v): 255 v = '' 256 cursor.execute("UPDATE repository SET value=%s " 257 "WHERE id=%s AND name=%s", (v, id, k)) 258 cursor.execute("SELECT value FROM repository " 259 "WHERE id=%s AND name=%s", (id, k)) 260 if not cursor.fetchone(): 261 cursor.execute("INSERT INTO repository (id, name, value) " 262 "VALUES (%s, %s, %s)", (id, k, v)) 263 db.commit() 264 rm.reload_repositories() 57 265 58 266 … … 60 268 """Version control system manager.""" 61 269 62 implements(IRequestFilter, IResourceManager )270 implements(IRequestFilter, IResourceManager, IRepositoryProvider) 63 271 64 272 connectors = ExtensionPoint(IRepositoryConnector) 273 providers = ExtensionPoint(IRepositoryProvider) 274 change_listeners = ExtensionPoint(IRepositoryChangeListener) 65 275 66 276 repository_type = Option('trac', 'repository_type', 'svn', 67 """Repository connector type. (''since 0.10'')""") 277 """Default repository connector type. (''since 0.10'') 278 279 This is also used as the default repository type for repositories 280 defined in [[TracIni#repositories-section repositories]] or using the 281 "Repositories" admin panel. (''since 0.12'') 282 """) 283 68 284 repository_dir = Option('trac', 'repository_dir', '', 69 """Path to local repository. This can also be a relative path 70 (''since 0.11'').""") 285 """Path to the default repository. This can also be a relative path 286 (''since 0.11''). 287 288 This option is deprecated, and repositories should be defined in the 289 [[TracIni#repositories-section repositories]] section, or using the 290 "Repositories" admin panel. (''since 0.12'')""") 291 292 repository_sync_per_request = ListOption('trac', 293 'repository_sync_per_request', '(default)', 294 doc="""List of repositories that should be synchronized on every page 295 request. 296 297 Leave this option empty if you have set up post-commit hooks calling 298 `trac-admin $ENV changeset added` on all your repositories 299 (recommended). Otherwise, set it to a comma-separated list of 300 repository names. Note that this will negatively affect performance, 301 and will prevent changeset listeners from receiving events from the 302 repositories specified here. The default is to synchronize the default 303 repository, for backward compatibility. (''since 0.12'')""") 71 304 72 305 def __init__(self): 73 306 self._cache = {} 74 307 self._lock = threading.Lock() 75 self._connector = None 308 self._connectors = None 309 self._all_repositories = None 76 310 77 311 # IRequestFilter methods … … 80 314 from trac.web.chrome import Chrome, add_warning 81 315 if handler is not Chrome(self.env): 82 try: 83 self.get_repository(req.authname).sync() 84 except TracError, e: 85 add_warning(req, _("Can't synchronize with the repository " 86 "(%(error)s). Look in the Trac log for more " 87 "information.", error=to_unicode(e.message))) 88 316 for reponame in self.repository_sync_per_request: 317 start = time.time() 318 if is_default(reponame): 319 reponame = '' 320 try: 321 repo = self.get_repository(reponame) 322 if repo: 323 repo.sync() 324 except TracError, e: 325 add_warning(req, 326 _("Can't synchronize with repository \"%(name)s\" " 327 "(%(error)s). Look in the Trac log for more " 328 "information.", name=reponame or _('(default)'), 329 error=to_unicode(e.message))) 330 self.log.info("Synchronized '%s' repository in %0.2f seconds", 331 reponame, time.time() - start) 89 332 return handler 90 333 … … 97 340 yield 'changeset' 98 341 yield 'source' 342 yield 'repository' 99 343 100 344 def get_resource_description(self, resource, format=None, **kwargs): 101 345 if resource.realm == 'changeset': 102 return _("Changeset %(rev)s", rev=resource.id) 346 reponame, id = resource.parent.id, resource.id 347 if reponame: 348 return _("Changeset %(rev)s in %(repo)s", rev=id, repo=reponame) 349 else: 350 return _("Changeset %(rev)s", rev=id) 103 351 elif resource.realm == 'source': 104 version = '' 352 reponame, id = resource.parent.id, resource.id 353 version = in_repo = '' 105 354 if format == 'summary': 106 repos = resource.env.get_repository( ) # no perm.username!355 repos = resource.env.get_repository(reponame) 107 356 node = repos.get_node(resource.id, resource.version) 108 357 if node.isdir: 109 kind = _(" Directory")358 kind = _("directory") 110 359 elif node.isfile: 111 kind = _(" File")360 kind = _("file") 112 361 if resource.version: 113 362 version = _("at version %(rev)s", rev=resource.version) 114 363 else: 115 kind = _(" Path")364 kind = _("path") 116 365 if resource.version: 117 366 version = '@%s' % resource.version 118 return '%s %s%s' % (kind, resource.id, version) 367 if reponame: 368 in_repo = _(" in %(repo)s", repo=reponame) 369 return ''.join([kind, ' ', id, version, in_repo]) 370 elif resource.realm == 'repository': 371 return _("Repository %(repo)s", repo=resource.id) 372 373 def get_resource_url(self, resource, href, **kwargs): 374 if resource.realm == 'changeset': 375 return href.changeset(resource.id, resource.parent.id or None) 376 elif resource.realm == 'source': 377 return href.source(resource.parent.id or None, resource.id) 378 elif resource.realm == 'repository': 379 return href.source(resource.id or None) 380 381 # IRepositoryProvider methods 382 383 def get_repositories(self): 384 """Retrieve repositories specified in TracIni. 385 386 The `[repositories]` section can be used to specify a list 387 of repositories. 388 """ 389 repositories = self.config['repositories'] 390 reponames = {} 391 # eventually add pre-0.12 default repository 392 if self.repository_dir: 393 reponames[''] = {'dir': self.repository_dir} 394 # first pass to gather the <name>.dir entries 395 for option in repositories: 396 if option.endswith('.dir'): 397 reponames[option[:-4]] = {} 398 # second pass to gather aliases 399 for option in repositories: 400 alias = repositories.get(option) 401 if '.' not in option: # Support <alias> = <repo> syntax 402 option += '.alias' 403 if option.endswith('.alias') and alias in reponames: 404 reponames.setdefault(option[:-6], {})['alias'] = alias 405 # third pass to gather the <name>.<detail> entries 406 for option in repositories: 407 if '.' in option: 408 name, detail = option.rsplit('.', 1) 409 if name in reponames and detail != 'alias': 410 reponames[name][detail] = repositories.get(option) 411 412 for reponame, info in reponames.iteritems(): 413 yield (reponame, info) 119 414 120 415 # Public API methods 121 416 122 def get_repository(self, authname): 417 def get_supported_types(self): 418 """Return the list of supported repository types.""" 419 types = set(type_ for connector in self.connectors 420 for (type_, prio) in connector.get_supported_types() 421 if prio >= 0) 422 return list(types) 423 424 def get_repositories_by_dir(self, directory): 425 """Retrieve the repositories based on the given directory. 426 427 :param directory: the key for identifying the repositories. 428 :return: list of Repository instances. 429 """ 430 directory = os.path.join(os.path.normcase(directory), '') 431 repositories = [] 432 for reponame, repoinfo in self.get_all_repositories().iteritems(): 433 dir = repoinfo.get('dir') 434 if dir: 435 dir = os.path.join(os.path.normcase(dir), '') 436 if dir.startswith(directory): 437 repos = self.get_repository(reponame) 438 if repos: 439 repositories.append(repos) 440 return repositories 441 442 def get_repository_id(self, reponame, db=None): 443 """Return a unique id for the given repository name.""" 444 handle_ta = False 445 if db is None: 446 db = self.env.get_db_cnx() 447 handle_ta = True 448 cursor = db.cursor() 449 cursor.execute("SELECT id FROM repository " 450 "WHERE name='name' AND value=%s", (reponame,)) 451 for id, in cursor: 452 return id 453 cursor.execute("SELECT COALESCE(MAX(id),0) FROM repository") 454 id = cursor.fetchone()[0] + 1 455 cursor.execute("INSERT INTO repository (id, name, value) " 456 "VALUES (%s,%s,%s)", (id, 'name', reponame)) 457 if handle_ta: 458 db.commit() 459 return id 460 461 def get_repository(self, reponame): 462 """Retrieve the appropriate Repository for the given name. 463 464 :param reponame: the key for specifying the repository. 465 If no name is given, take the default 466 repository. 467 :return: if no corresponding repository was defined, 468 simply return `None`. 469 """ 470 reponame = reponame or '' 471 repoinfo = self.get_all_repositories().get(reponame, {}) 472 if 'alias' in repoinfo: 473 reponame = repoinfo['alias'] 474 repoinfo = self.get_all_repositories().get(reponame, {}) 475 rdir = repoinfo.get('dir') 476 if not rdir: 477 return None 478 rtype = repoinfo.get('type') or self.repository_type 479 480 # get a Repository for the reponame (use a thread-level cache) 123 481 db = self.env.get_db_cnx() # prevent possible deadlock, see #4465 124 482 try: 125 483 self._lock.acquire() 126 if not self._connector:127 candidates = [128 (prio, connector)129 for connector in self.connectors130 for repos_type, prio in connector.get_supported_types()131 if repos_type == self.repository_type132 ]133 if candidates:134 prio, connector = max(candidates)135 if prio < 0: # error condition136 raise TracError(137 _('Unsupported version control system "%(name)s"'138 ': "%(error)s" ', name=self.repository_type,139 error=to_unicode(connector.error)))140 self._connector = connector141 else:142 raise TracError(143 _('Unsupported version control system "%(name)s": '144 'Can\'t find an appropriate component, maybe the '145 'corresponding plugin was not enabled? ',146 name=self.repository_type))147 484 tid = threading._get_ident() 148 485 if tid in self._cache: 149 repos = self._cache[tid]486 repositories = self._cache[tid] 150 487 else: 151 rtype, rdir = self.repository_type, self.repository_dir 488 repositories = self._cache[tid] = {} 489 repos = repositories.get(reponame) 490 if not repos: 152 491 if not os.path.isabs(rdir): 153 492 rdir = os.path.join(self.env.path, rdir) 154 repos = self._connector.get_repository(rtype, rdir, authname) 155 self._cache[tid] = repos 493 connector = self._get_connector(rtype) 494 repos = connector.get_repository(rtype, rdir, repoinfo.copy()) 495 repositories[reponame] = repos 156 496 return repos 157 497 finally: 158 498 self._lock.release() 159 499 500 def get_repository_by_path(self, path): 501 """Retrieve a matching Repository for the given path. 502 503 :param path: the eventually scoped repository-scoped path 504 :return: a `(reponame, repos, path)` triple, where `path` is 505 the remaining part of `path` once the `reponame` has 506 been truncated, if needed. 507 """ 508 matches = [] 509 path = path and path.strip('/') + '/' or '/' 510 for reponame in self.get_all_repositories().keys(): 511 stripped_reponame = reponame.strip('/') + '/' 512 if path.startswith(stripped_reponame): 513 matches.append((len(stripped_reponame), reponame)) 514 if matches: 515 matches.sort() 516 length, reponame = matches[-1] 517 path = path[length:] 518 else: 519 reponame = '' 520 return (reponame, self.get_repository(reponame), 521 path.rstrip('/') or '/') 522 523 def get_default_repository(self, context): 524 """Recover the appropriate repository from the current context. 525 526 Lookup the closest source or changeset resource in the context 527 hierarchy and return the name of its associated repository. 528 """ 529 while context: 530 if context.resource.realm in ('source', 'changeset'): 531 return context.resource.parent.id 532 context = context.parent 533 534 def get_all_repositories(self): 535 """Return a dictionary of repository information, indexed by name.""" 536 if not self._all_repositories: 537 self._all_repositories = {} 538 for provider in self.providers: 539 for reponame, info in provider.get_repositories(): 540 if reponame in self._all_repositories: 541 self.log.warn("Discarding duplicate repository '%s'", 542 reponame) 543 else: 544 info['name'] = reponame 545 if 'id' not in info: 546 info['id'] = self.get_repository_id(reponame) 547 self._all_repositories[reponame] = info 548 return self._all_repositories 549 550 def get_real_repositories(self): 551 """Return a set of all real repositories (i.e. excluding aliases).""" 552 repositories = set() 553 for reponame in self.get_all_repositories(): 554 try: 555 repos = self.get_repository(reponame) 556 if repos is not None: 557 repositories.add(repos) 558 except TracError: 559 pass # Skip invalid repositories 560 return repositories 561 562 def reload_repositories(self): 563 """Reload the repositories from the providers.""" 564 self._lock.acquire() 565 try: 566 # FIXME: trac-admin doesn't reload the environment 567 self._cache = {} 568 self._all_repositories = None 569 finally: 570 self._lock.release() 571 self.config.touch() # Force environment reload 572 573 def notify(self, event, reponame, revs): 574 """Notify repositories and change listeners about repository events. 575 576 The supported events are the names of the methods defined in the 577 `IRepositoryChangeListener` interface. 578 """ 579 self.log.debug("Event %s on %s for changesets %r", 580 event, reponame, revs) 581 582 # Notify a repository by name, and all repositories with the same 583 # base, or all repositories by base or by repository dir 584 repos = self.get_repository(reponame) 585 repositories = [] 586 if repos: 587 base = repos.get_base() 588 else: 589 repositories = self.get_repositories_by_dir(reponame) 590 if repositories: 591 base = None 592 else: 593 base = reponame 594 if base: 595 repositories = [r for r in self.get_real_repositories() 596 if r.get_base() == base] 597 if not repositories: 598 self.log.warn("Found no repositories matching '%s' base.", 599 base or reponame) 600 return 601 for repos in sorted(repositories, key=lambda r: r.reponame): 602 repos.sync() 603 for rev in revs: 604 args = [] 605 if event == 'changeset_modified': 606 args.append(repos.sync_changeset(rev)) 607 try: 608 changeset = repos.get_changeset(rev) 609 except NoSuchChangeset: 610 continue 611 self.log.debug("Event %s on %s for revision %s", 612 event, repos.reponame or '(default)', rev) 613 for listener in self.change_listeners: 614 getattr(listener, event)(repos, changeset, *args) 615 160 616 def shutdown(self, tid=None): 161 617 if tid: … … 163 619 try: 164 620 self._lock.acquire() 165 repos = self._cache.pop(tid, None)166 if repos:621 repositories = self._cache.pop(tid, {}) 622 for reponame, repos in repositories.iteritems(): 167 623 repos.close() 168 624 finally: 169 625 self._lock.release() 626 627 # private methods 628 629 def _get_connector(self, rtype): 630 """Retrieve the appropriate connector for the given repository type. 631 632 Note that the self._lock must be held when calling this method. 633 """ 634 if self._connectors is None: 635 # build an environment-level cache for the preferred connectors 636 self._connectors = {} 637 for connector in self.connectors: 638 for type_, prio in connector.get_supported_types(): 639 keep = (connector, prio) 640 if type_ in self._connectors and \ 641 prio <= self._connectors[type_][1]: 642 keep = None 643 if keep: 644 self._connectors[type_] = keep 645 if rtype in self._connectors: 646 connector, prio = self._connectors[rtype] 647 if prio >= 0: # no error condition 648 return connector 649 else: 650 raise TracError( 651 _('Unsupported version control system "%(name)s"' 652 ': %(error)s', name=rtype, 653 error=to_unicode(connector.error))) 654 else: 655 raise TracError( 656 _('Unsupported version control system "%(name)s": ' 657 'Can\'t find an appropriate component, maybe the ' 658 'corresponding plugin was not enabled? ', name=rtype)) 170 659 171 660 … … 186 675 """Base class for a repository provided by a version control system.""" 187 676 188 def __init__(self, name, authz, log): 677 def __init__(self, name, params, log): 678 """Initialize a repository. 679 680 :param name: a unique name identifying the repository, usually a 681 type-specific prefix followed by the path to the 682 repository. 683 :param params: a `dict` of parameters for the repository. Contains 684 the name of the repository under the key "name" and 685 the surrogate key that identifies the repository in 686 the database under the key "id". 687 :param log: a logger instance. 688 """ 189 689 self.name = name 190 self.authz = authz or Authorizer() 690 self.params = params 691 self.reponame = params['name'] 692 self.id = params['id'] 191 693 self.log = log 694 self.resource = Resource('repository', self.reponame) 192 695 193 696 def close(self): … … 195 698 raise NotImplementedError 196 699 700 def get_base(self): 701 """Return the name of the base repository for this repository. 702 703 This function returns the name of the base repository to which scoped 704 repositories belong. For non-scoped repositories, it returns the 705 repository name. 706 """ 707 return self.name 708 197 709 def clear(self, youngest_rev=None): 198 710 """Clear any data that may have been cached in instance properties. … … 203 715 pass 204 716 205 def sync(self, rev_callback=None ):717 def sync(self, rev_callback=None, clean=False): 206 718 """Perform a sync of the repository cache, if relevant. 207 719 … … 209 721 The backend will call this function for each `rev` it decided to 210 722 synchronize, once the synchronization changes are committed to the 211 cache. 723 cache. When `clean` is `True`, the cache is cleaned first. 212 724 """ 213 725 pass 214 726 215 727 def sync_changeset(self, rev): 216 """Resync the repository cache for the given `rev`, if relevant.""" 217 raise NotImplementedError 728 """Resync the repository cache for the given `rev`, if relevant. 729 730 Returns a "metadata-only" changeset containing the metadata prior to 731 the resync, or `None` if the old values cannot be retrieved (typically 732 when the repository is not cached). 733 """ 734 return None 218 735 219 736 def get_quickjump_entries(self, rev): … … 227 744 return [] 228 745 746 def get_path_url(self, path, rev): 747 """Return the repository URL for the given path and revision. 748 749 The returned URL can be `None`, meaning that no URL has been specified 750 for the repository, an absolute URL, or a scheme-relative URL starting 751 with `//`, in which case the scheme of the request should be prepended. 752 """ 753 return None 754 229 755 def get_changeset(self, rev): 230 """Retrieve a Changeset corresponding to the given revision `rev`.""" 231 raise NotImplementedError 756 """Retrieve a Changeset corresponding to the given revision `rev`.""" 757 raise NotImplementedError 758 759 def get_changeset_uid(self, rev): 760 """Return a globally unique identifier for the ''rev'' changeset. 761 762 Two changesets from different repositories can sometimes refer to 763 the ''very same'' changeset (e.g. the repositories are clones). 764 """ 232 765 233 766 def get_changesets(self, start, stop): … … 236 769 rev = self.youngest_rev 237 770 while rev: 238 if self.authz.has_permission_for_changeset(rev): 239 chgset = self.get_changeset(rev) 240 if chgset.date < start: 241 return 242 if chgset.date < stop: 243 yield chgset 771 chgset = self.get_changeset(rev) 772 if chgset.date < start: 773 return 774 if chgset.date < stop: 775 yield chgset 244 776 rev = self.previous_rev(rev) 245 777 … … 307 839 308 840 def get_path_history(self, path, rev=None, limit=None): 309 """Retrieve all the revisions containing this path 841 """Retrieve all the revisions containing this path. 310 842 311 843 If given, `rev` is used as a starting point (i.e. no revision … … 348 880 raise NotImplementedError 349 881 882 def can_view(self, perm): 883 """Return True if view permission is granted on the repository.""" 884 return 'BROWSER_VIEW' in perm(self.resource.child('source', '/')) 885 350 886 351 887 class Node(object): … … 354 890 DIRECTORY = "dir" 355 891 FILE = "file" 892 893 resource = property(lambda self: Resource('source', self.created_path, 894 version=self.created_rev, 895 parent=self.repos.resource)) 356 896 357 897 # created_path and created_rev properties refer to the Node "creation" … … 363 903 created_path = None 364 904 365 def __init__(self, path, rev, kind):905 def __init__(self, repos, path, rev, kind): 366 906 assert kind in (Node.DIRECTORY, Node.FILE), \ 367 907 "Unknown node kind %s" % kind 908 self.repos = repos 368 909 self.path = to_unicode(path) 369 910 self.rev = rev … … 455 996 isfile = property(lambda x: x.kind == Node.FILE) 456 997 998 def can_view(self, perm): 999 """Return True if view permission is granted on the node.""" 1000 return (self.isdir and 'BROWSER_VIEW' or 'FILE_VIEW') \ 1001 in perm(self.resource) 1002 457 1003 458 1004 class Changeset(object): … … 470 1016 ALL_CHANGES = DIFF_CHANGES + OTHER_CHANGES 471 1017 472 def __init__(self, rev, message, author, date): 1018 resource = property(lambda self: Resource('changeset', self.rev, 1019 parent=self.repos.resource)) 1020 1021 def __init__(self, repos, rev, message, author, date): 1022 self.repos = repos 473 1023 self.rev = rev 474 1024 self.message = message or '' 475 1025 self.author = author or '' 476 1026 self.date = date 477 1027 478 1028 def get_properties(self): 479 1029 """Returns the properties (meta-data) of the node, as a dictionary. … … 488 1038 489 1039 def get_changes(self): 490 """Generator that produces a tuple for every change in the changeset 1040 """Generator that produces a tuple for every change in the changeset. 491 1041 492 1042 The tuple will contain `(path, kind, change, base_path, base_rev)`, … … 501 1051 raise NotImplementedError 502 1052 503 504 class PermissionDenied(PermissionError): 505 """Exception raised by an authorizer. 506 507 This exception is raise if the user has insufficient permissions 508 to view a specific part of the repository. 509 """ 510 def __str__(self): 511 return self.action 512 513 514 class Authorizer(object): 515 """Controls the view access to parts of the repository. 516 517 Base class for authorizers that are responsible to granting or denying 518 access to view certain parts of a repository. 519 """ 520 521 def assert_permission(self, path): 522 if not self.has_permission(path): 523 raise PermissionDenied(_('Insufficient permissions to access ' 524 '%(path)s', path=path)) 525 526 def assert_permission_for_changeset(self, rev): 527 if not self.has_permission_for_changeset(rev): 528 raise PermissionDenied(_('Insufficient permissions to access ' 529 'changeset %(id)s', id=rev)) 530 531 def has_permission(self, path): 532 return True 533 534 def has_permission_for_changeset(self, rev): 535 return True 1053 def can_view(self, perm): 1054 """Return True if view permission is granted on the changeset.""" 1055 return 'CHANGESET_VIEW' in perm(self.resource) 1056 1057 1058 # Note: Since Trac 0.12, Exception PermissionDenied class is gone, 1059 # and class Authorizer is gone as well. 1060 # 1061 # Fine-grained permissions are now handled via normal permission policies. -
trunk/trac/versioncontrol/cache.py
r8987 r9125 17 17 from datetime import datetime 18 18 import os 19 import posixpath 20 19 20 from trac.cache import CacheProxy 21 21 from trac.core import TracError 22 22 from trac.util.datefmt import utc, to_timestamp 23 23 from trac.util.translation import _ 24 from trac.versioncontrol import Changeset, Node, Repository, Authorizer, \ 25 NoSuchChangeset 24 from trac.versioncontrol import Changeset, Node, Repository, NoSuchChangeset 26 25 27 26 … … 43 42 scope = property(lambda self: self.repos.scope) 44 43 45 def __init__(self, getdb, repos, authz, log): 46 Repository.__init__(self, repos.name, authz, log) 47 if callable(getdb): 48 self.getdb = getdb 49 else: 50 self.getdb = lambda: getdb 44 def __init__(self, env, repos, log): 45 self.env = env 51 46 self.repos = repos 47 self.metadata = CacheProxy(self.__class__.__module__ + '.' 48 + self.__class__.__name__ + '.metadata:' 49 + str(self.repos.id), self._metadata, 50 self.env) 51 Repository.__init__(self, repos.name, repos.params, log) 52 52 53 53 def close(self): 54 54 self.repos.close() 55 55 56 def get_base(self): 57 return self.repos.get_base() 58 56 59 def get_quickjump_entries(self, rev): 57 for category, name, path, rev in self.repos.get_quickjump_entries(rev): 58 yield category, name, path, rev 60 return self.repos.get_quickjump_entries(self.normalize_rev(rev)) 61 62 def get_path_url(self, path, rev): 63 return self.repos.get_path_url(path, rev) 59 64 60 65 def get_changeset(self, rev): 61 return CachedChangeset(self.repos, self.repos.normalize_rev(rev), 62 self.getdb, self.authz) 66 return CachedChangeset(self.repos, self.normalize_rev(rev), self.env) 67 68 def get_changeset_uid(self, rev): 69 return self.repos.get_changeset_uid(rev) 63 70 64 71 def get_changesets(self, start, stop): 65 db = self. getdb()72 db = self.env.get_db_cnx() 66 73 cursor = db.cursor() 67 74 cursor.execute("SELECT rev FROM revision " 68 "WHERE time >= %s AND time < %s "75 "WHERE repos=%s AND time >= %s AND time < %s " 69 76 "ORDER BY time DESC, rev DESC", 70 (to_timestamp(start), to_timestamp(stop))) 77 (self.id, to_timestamp(start), 78 to_timestamp(stop))) 71 79 for rev, in cursor: 72 80 try: 73 if self.authz.has_permission_for_changeset(rev): 74 yield self.get_changeset(rev) 81 yield self.get_changeset(rev) 75 82 except NoSuchChangeset: 76 83 pass # skip changesets currently being resync'ed … … 78 85 def sync_changeset(self, rev): 79 86 cset = self.repos.get_changeset(rev) 80 db = self.getdb() 81 cursor = db.cursor() 87 db = self.env.get_db_cnx() 88 cursor = db.cursor() 89 cursor.execute("SELECT time,author,message FROM revision " 90 "WHERE repos=%s AND rev=%s", 91 (self.id, str(cset.rev))) 92 old_changeset = None 93 for time, author, message in cursor: 94 date = datetime.fromtimestamp(time, utc) 95 old_changeset = Changeset(cset.rev, message, author, date) 96 82 97 cursor.execute("UPDATE revision SET time=%s, author=%s, message=%s " 83 "WHERE re v=%s", (to_timestamp(cset.date),84 85 (str(cset.rev))))98 "WHERE repos=%s AND rev=%s", 99 (to_timestamp(cset.date), cset.author, cset.message, 100 self.id, str(cset.rev))) 86 101 db.commit() 102 return old_changeset 87 103 88 def sync(self, feedback=None): 89 db = self.getdb() 90 cursor = db.cursor() 91 cursor.execute("SELECT name, value FROM system WHERE name IN (%s)" % 92 ','.join(["'%s'" % key for key in CACHE_METADATA_KEYS])) 93 metadata = {} 94 for name, value in cursor: 95 metadata[name] = value 104 def _metadata(self, db): 105 """Retrieve data for the cached `metadata` attribute.""" 106 cursor = db.cursor() 107 cursor.execute("SELECT name, value FROM repository " 108 "WHERE id=%%s AND name IN (%s)" % 109 ','.join(['%s'] * len(CACHE_METADATA_KEYS)), 110 (self.id,) + CACHE_METADATA_KEYS) 111 return dict(cursor) 112 113 def sync(self, feedback=None, clean=False): 114 db = self.env.get_db_cnx() 115 cursor = db.cursor() 116 if clean: 117 self.log.info('Cleaning cache') 118 cursor.execute("DELETE FROM revision WHERE repos=%s", (self.id,)) 119 cursor.execute("DELETE FROM node_change WHERE repos=%s", 120 (self.id,)) 121 cursor.executemany("DELETE FROM repository " 122 "WHERE id=%s AND name=%s", 123 [(self.id, k) for k in CACHE_METADATA_KEYS]) 124 cursor.executemany("INSERT INTO repository (id, name, value) " 125 "VALUES (%s, %s, %s)", 126 [(self.id, k, '') for k in CACHE_METADATA_KEYS]) 127 self.metadata.invalidate(db) 128 db.commit() 129 130 metadata = self.metadata.get(db) 131 do_commit = False 96 132 97 133 # -- check that we're populating the cache for the correct repository … … 103 139 % (repository_dir, self.name)) 104 140 raise TracError(_("The 'repository_dir' has changed, a " 105 "'trac-admin resync' operation is needed.")) 141 "'trac-admin $ENV repository resync' " 142 "operation is needed.")) 106 143 elif repository_dir is None: # 107 144 self.log.info('Storing initial "repository_dir": %s' % self.name) 108 cursor.execute("INSERT INTO system (name,value) VALUES (%s,%s)", 109 (CACHE_REPOSITORY_DIR, self.name,)) 145 cursor.execute("INSERT INTO repository (id,name,value) " 146 "VALUES (%s,%s,%s)", 147 (self.id, CACHE_REPOSITORY_DIR, self.name)) 148 do_commit = True 110 149 else: # 'repository_dir' cleared by a resync 111 150 self.log.info('Resetting "repository_dir": %s' % self.name) 112 cursor.execute("UPDATE system SET value=%s WHERE name=%s",113 (self.name, CACHE_REPOSITORY_DIR))114 115 db.commit() # save metadata changes made up to now151 cursor.execute("UPDATE repository SET value=%s " 152 "WHERE id=%s AND name=%s", 153 (self.name, self.id, CACHE_REPOSITORY_DIR)) 154 do_commit = True 116 155 117 156 # -- retrieve the youngest revision in the repository … … 120 159 121 160 # -- retrieve the youngest revision cached so far 122 if CACHE_YOUNGEST_REV not in metadata: 123 raise TracError(_('Missing "youngest_rev" in cache metadata')) 124 125 self.youngest = metadata[CACHE_YOUNGEST_REV] 126 127 if self.youngest: 128 self.youngest = self.repos.normalize_rev(self.youngest) 129 if not self.youngest: 161 youngest = metadata.get(CACHE_YOUNGEST_REV) 162 if youngest is None: 163 cursor.execute("INSERT INTO repository (id,name,value) " 164 "VALUES (%s,%s,%s)", 165 (self.id, CACHE_YOUNGEST_REV, '')) 166 do_commit = True 167 168 if do_commit: 169 self.metadata.invalidate(db) 170 db.commit() # save metadata changes made up to now 171 172 if youngest: 173 youngest = self.repos.normalize_rev(youngest) 174 if not youngest: 130 175 self.log.debug('normalize_rev failed (youngest_rev=%r)' % 131 176 self.youngest_rev) … … 133 178 self.log.debug('cache metadata undefined (youngest_rev=%r)' % 134 179 self.youngest_rev) 135 self.youngest = None180 youngest = None 136 181 137 182 # -- compare them and try to resync if different 138 if self.youngest != repos_youngest:183 if youngest != repos_youngest: 139 184 self.log.info("repos rev [%s] != cached rev [%s]" % 140 (repos_youngest, self.youngest))141 if self.youngest:142 next_youngest = self.repos.next_rev( self.youngest)185 (repos_youngest, youngest)) 186 if youngest: 187 next_youngest = self.repos.next_rev(youngest) 143 188 else: 144 189 next_youngest = None … … 153 198 next_youngest = self.repos.normalize_rev(next_youngest) 154 199 except TracError: 155 return # can't normalize oldest_rev: repository was empty 200 # can't normalize oldest_rev: repository was empty 201 return 156 202 157 203 if next_youngest is None: # nothing to cache yet … … 159 205 160 206 # 0. first check if there's no (obvious) resync in progress 161 cursor.execute("SELECT rev FROM revision WHERE rev=%s", 162 (str(next_youngest),)) 207 cursor.execute("SELECT rev FROM revision " 208 "WHERE repos=%s AND rev=%s", 209 (self.id, str(next_youngest))) 163 210 for rev, in cursor: 164 211 # already there, but in progress, so keep ''previous'' 165 212 # notion of 'youngest' 166 self.repos.clear(youngest_rev= self.youngest)213 self.repos.clear(youngest_rev=youngest) 167 214 return 168 215 … … 170 217 # (there still might be a race condition at this point) 171 218 172 authz = self.repos.authz173 self.repos.authz = Authorizer() # remove permission checking174 175 219 kindmap = dict(zip(_kindmap.values(), _kindmap.keys())) 176 220 actionmap = dict(zip(_actionmap.values(), _actionmap.keys())) 177 221 178 try: 179 while next_youngest is not None: 180 181 # 1.1 Attempt to resync the 'revision' table 182 self.log.info("Trying to sync revision [%s]" % 183 next_youngest) 184 cset = self.repos.get_changeset(next_youngest) 185 try: 186 cursor.execute("INSERT INTO revision " 187 " (rev,time,author,message) " 188 "VALUES (%s,%s,%s,%s)", 189 (str(next_youngest), 190 to_timestamp(cset.date), 191 cset.author, cset.message)) 192 except Exception, e: # *another* 1.1. resync attempt won 193 self.log.warning('Revision %s already cached: %s' % 194 (next_youngest, e)) 195 # also potentially in progress, so keep ''previous'' 196 # notion of 'youngest' 197 self.repos.clear(youngest_rev=self.youngest) 198 db.rollback() 199 return 200 201 # 1.2. now *only* one process was able to get there 202 # (i.e. there *shouldn't* be any race condition here) 203 204 for path, kind, action, bpath, brev in cset.get_changes(): 205 self.log.debug("Caching node change in [%s]: %s" 206 % (next_youngest, 207 (path,kind,action,bpath,brev))) 208 kind = kindmap[kind] 209 action = actionmap[action] 210 cursor.execute("INSERT INTO node_change " 211 " (rev,path,node_type,change_type, " 212 " base_path,base_rev) " 213 "VALUES (%s,%s,%s,%s,%s,%s)", 214 (str(next_youngest), 215 path, kind, action, bpath, brev)) 216 217 # 1.3. iterate (1.1 should always succeed now) 218 self.youngest = next_youngest 219 next_youngest = self.repos.next_rev(next_youngest) 220 221 # 1.4. update 'youngest_rev' metadata 222 # (minimize possibility of failures at point 0.) 223 cursor.execute("UPDATE system SET value=%s WHERE name=%s", 224 (str(self.youngest), CACHE_YOUNGEST_REV)) 225 db.commit() 226 227 # 1.5. provide some feedback 228 if feedback: 229 feedback(self.youngest) 230 finally: 231 # 3. restore permission checking (after 1.) 232 self.repos.authz = authz 222 while next_youngest is not None: 223 224 # 1.1 Attempt to resync the 'revision' table 225 self.log.info("Trying to sync revision [%s]" % 226 next_youngest) 227 cset = self.repos.get_changeset(next_youngest) 228 try: 229 cursor.execute("INSERT INTO revision " 230 " (repos,rev,time,author,message) " 231 "VALUES (%s,%s,%s,%s,%s)", 232 (self.id, str(next_youngest), 233 to_timestamp(cset.date), 234 cset.author, cset.message)) 235 except Exception, e: # *another* 1.1. resync attempt won 236 self.log.warning('Revision %s already cached: %s' % 237 (next_youngest, e)) 238 # also potentially in progress, so keep ''previous'' 239 # notion of 'youngest' 240 self.repos.clear(youngest_rev=youngest) 241 db.rollback() 242 return 243 244 # 1.2. now *only* one process was able to get there 245 # (i.e. there *shouldn't* be any race condition here) 246 247 for path, kind, action, bpath, brev in cset.get_changes(): 248 self.log.debug("Caching node change in [%s]: %s" 249 % (next_youngest, 250 (path,kind,action,bpath,brev))) 251 kind = kindmap[kind] 252 action = actionmap[action] 253 cursor.execute("INSERT INTO node_change " 254 " (repos,rev,path,node_type," 255 " change_type,base_path,base_rev) " 256 "VALUES (%s,%s,%s,%s,%s,%s,%s)", 257 (self.id, str(next_youngest), 258 path, kind, action, bpath, brev)) 259 260 # 1.3. iterate (1.1 should always succeed now) 261 youngest = next_youngest 262 next_youngest = self.repos.next_rev(next_youngest) 263 264 # 1.4. update 'youngest_rev' metadata 265 # (minimize possibility of failures at point 0.) 266 cursor.execute("UPDATE repository SET value=%s " 267 "WHERE id=%s AND name=%s", 268 (str(youngest), self.id, 269 CACHE_YOUNGEST_REV)) 270 self.metadata.invalidate(db) 271 db.commit() 272 273 # 1.5. provide some feedback 274 if feedback: 275 feedback(youngest) 233 276 234 277 def get_node(self, path, rev=None): 235 return self.repos.get_node(path, rev)278 return self.repos.get_node(path, self.normalize_rev(rev)) 236 279 237 280 def _get_node_revs(self, path, last=None, first=None): … … 241 284 last = self.normalize_rev(last) 242 285 node = self.get_node(path, last) # Check node existence and perms 243 db = self. getdb()286 db = self.env.get_db_cnx() 244 287 cursor = db.cursor() 245 288 rev_as_int = db.cast('rev', 'int') 246 289 if first is None: 247 290 cursor.execute("SELECT rev FROM node_change " 248 "WHERE %s <=%%s "249 " AND path =%%s "291 "WHERE repos=%%s AND %s<=%%s " 292 " AND path=%%s " 250 293 " AND change_type IN ('A', 'C', 'M') " 251 294 "ORDER BY %s DESC " 252 295 "LIMIT 1" % ((rev_as_int,) * 2), 253 ( last, path))296 (self.id, last, path)) 254 297 first = 0 255 298 for row in cursor: 256 299 first = int(row[0]) 257 300 cursor.execute("SELECT DISTINCT rev FROM node_change " 258 "WHERE %s >= %%s AND %s <=%%s "259 " AND (path =%%s OR path %s)" %301 "WHERE repos=%%s AND %s>=%%s AND %s<=%%s " 302 " AND (path=%%s OR path %s)" % 260 303 (rev_as_int, rev_as_int, db.like()), 261 (first, last, path, db.like_escape(path + '/') + '%')) 304 (self.id, first, last, path, 305 db.like_escape(path + '/') + '%')) 262 306 return [int(row[0]) for row in cursor] 263 307 264 308 def has_node(self, path, rev=None): 265 return self.repos.has_node(path, rev)309 return self.repos.has_node(path, self.normalize_rev(rev)) 266 310 267 311 def get_oldest_rev(self): … … 269 313 270 314 def get_youngest_rev(self): 271 if not hasattr(self, 'youngest'): 272 self.sync() 273 return self.youngest 315 return self.metadata.get().get(CACHE_YOUNGEST_REV) 274 316 275 317 def previous_rev(self, rev, path=''): … … 277 319 return self._next_prev_rev('<', rev, path) 278 320 else: 279 return self.repos.previous_rev( rev, path)321 return self.repos.previous_rev(self.normalize_rev(rev), path) 280 322 281 323 def next_rev(self, rev, path=''): … … 283 325 return self._next_prev_rev('>', rev, path) 284 326 else: 285 return self.repos.next_rev( rev, path)327 return self.repos.next_rev(self.normalize_rev(rev), path) 286 328 287 329 def _next_prev_rev(self, direction, rev, path=''): 288 db = self. getdb()330 db = self.env.get_db_cnx() 289 331 # the changeset revs are sequence of ints: 290 sql = "SELECT rev FROM node_change WHERE " + \291 db.cast('rev', 'int') + " " + direction + "%s"292 args = [ rev]332 sql = "SELECT rev FROM node_change WHERE repos=%s AND " + \ 333 db.cast('rev', 'int') + direction + "%s" 334 args = [self.id, rev] 293 335 294 336 if path: 295 337 path = path.lstrip('/') 296 338 # changes on path itself or its children 297 sql += " AND (path =%s OR path " + db.like()339 sql += " AND (path=%s OR path " + db.like() 298 340 args.extend((path, db.like_escape(path + '/') + '%')) 299 341 # deletion of path ancestors 300 342 components = path.lstrip('/').split('/') 301 343 parents = ','.join(('%s',) * len(components)) 302 sql += " OR (path IN (" + parents + ") AND change_type ='D'))"344 sql += " OR (path IN (" + parents + ") AND change_type='D'))" 303 345 for i in range(1, len(components) + 1): 304 346 args.append('/'.join(components[:i])) … … 313 355 314 356 def rev_older_than(self, rev1, rev2): 315 return self.repos.rev_older_than(rev1, rev2) 357 return self.repos.rev_older_than(self.normalize_rev(rev1), 358 self.normalize_rev(rev2)) 316 359 317 360 def get_path_history(self, path, rev=None, limit=None): 318 return self.repos.get_path_history(path, rev, limit) 361 return self.repos.get_path_history(path, self.normalize_rev(rev), 362 limit) 319 363 320 364 def normalize_path(self, path): … … 322 366 323 367 def normalize_rev(self, rev): 324 return self.repos.normalize_rev(rev) 368 if rev is None or isinstance(rev, basestring) and \ 369 rev.lower() in ('', 'head', 'latest', 'youngest'): 370 return self.youngest_rev 371 else: 372 try: 373 rev = int(rev) 374 if rev <= self.youngest_rev: 375 return rev 376 except (ValueError, TypeError): 377 pass 378 raise NoSuchChangeset(rev) 325 379 326 380 def get_changes(self, old_path, old_rev, new_path, new_rev, 327 ignore_ancestry=1): 328 return self.repos.get_changes(old_path, old_rev, new_path, new_rev, 329 ignore_ancestry) 381 ignore_ancestry=1): 382 return self.repos.get_changes(old_path, self.normalize_rev(old_rev), 383 new_path, self.normalize_rev(new_rev), 384 ignore_ancestry) 330 385 331 386 332 387 class CachedChangeset(Changeset): 333 388 334 def __init__(self, repos, rev, getdb, authz): 335 self.repos = repos 336 self.getdb = getdb 337 self.authz = authz 338 db = self.getdb() 389 def __init__(self, repos, rev, env): 390 self.env = env 391 db = self.env.get_db_cnx() 339 392 cursor = db.cursor() 340 393 cursor.execute("SELECT time,author,message FROM revision " 341 "WHERE rev=%s", (str(rev),)) 394 "WHERE repos=%s AND rev=%s", 395 (repos.id, str(rev))) 342 396 row = cursor.fetchone() 343 397 if row: 344 398 _date, author, message = row 345 399 date = datetime.fromtimestamp(_date, utc) 346 Changeset.__init__(self, re v, message, author, date)400 Changeset.__init__(self, repos, rev, message, author, date) 347 401 else: 348 402 raise NoSuchChangeset(rev) … … 350 404 351 405 def get_changes(self): 352 db = self. getdb()406 db = self.env.get_db_cnx() 353 407 cursor = db.cursor() 354 408 cursor.execute("SELECT path,node_type,change_type,base_path,base_rev " 355 "FROM node_change WHERE re v=%s "356 "ORDER BY path", (s tr(self.rev),))409 "FROM node_change WHERE repos=%s AND rev=%s " 410 "ORDER BY path", (self.repos.id, str(self.rev))) 357 411 for path, kind, change, base_path, base_rev in cursor: 358 if not self.authz.has_permission(posixpath.join(self.scope,359 path.strip('/'))):360 # FIXME: what about the base_path?361 continue362 412 kind = _kindmap[kind] 363 413 change = _actionmap[change] -
trunk/trac/versioncontrol/svn_authz.py
r7890 r9125 19 19 import os.path 20 20 21 from trac.config import Option 21 from trac.config import Option, PathOption 22 22 from trac.core import * 23 from trac.versioncontrol import Authorizer 24 25 26 class SvnAuthzOptions(Component): 27 28 authz_file = Option('trac', 'authz_file', '', 29 """Path to Subversion 30 [http://svnbook.red-bean.com/en/1.1/ch06s04.html#svn-ch-6-sect-4.4.2 authorization (authz) file] 31 """) 32 33 authz_module_name = Option('trac', 'authz_module_name', '', 34 """The module prefix used in the authz_file.""") 35 36 37 def SubversionAuthorizer(env, repos, authname): 38 authz_file = env.config.get('trac', 'authz_file') 39 if not authz_file: 40 return Authorizer() 41 if not os.path.isabs(authz_file): 42 authz_file = os.path.join(env.path, authz_file) 43 if not os.path.exists(authz_file): 44 env.log.error('[trac] authz_file (%s) does not exist.' % authz_file) 45 46 module_name = env.config.get('trac', 'authz_module_name') 47 return RealSubversionAuthorizer(repos, authname, module_name, authz_file) 23 from trac.perm import IPermissionPolicy 24 from trac.resource import Resource 25 from trac.util import read_file 26 from trac.util.compat import any 27 from trac.util.text import exception_to_unicode, to_unicode 28 from trac.util.translation import _ 29 from trac.versioncontrol.api import RepositoryManager 30 48 31 49 32 def parent_iter(path): … … 57 40 yield path 58 41 if path == '/': 59 r aise StopIteration()42 return 60 43 path = path[:-1] 61 44 yield path … … 64 47 65 48 66 class RealSubversionAuthorizer(Authorizer): 67 """FIXME: this should become a IPermissionPolicy, of course. 68 69 `check_permission(username, action, resource)` should be able to 70 replace `has_permission(path)` when resource is a `('source', path)` 71 and `has_permission_for_changeset` when resource is a `('changeset', rev)`. 49 class ParseError(Exception): 50 """Exception thrown for parse errors in authz files""" 51 52 53 def parse(authz): 54 """Parse a Subversion authorization file. 55 56 Return a dict of modules, each containing a dict of paths, each containing 57 a dict mapping users to permissions. 72 58 """ 73 74 auth_name = '' 75 module_name = '' 76 conf_authz = None 77 78 def __init__(self, repos, auth_name, module_name, cfg_file, cfg_fp=None): 79 self.repos = repos 80 self.auth_name = auth_name 81 self.module_name = module_name 82 83 from ConfigParser import ConfigParser 84 self.conf_authz = ConfigParser() 85 if cfg_fp: 86 self.conf_authz.readfp(cfg_fp, cfg_file) 87 elif cfg_file: 88 self.conf_authz.read(cfg_file) 89 90 self.groups = self._groups() 91 92 def has_permission(self, path): 93 if path is None: 94 return 1 95 96 for p in parent_iter(path): 97 if self.module_name: 98 for perm in self._get_section(self.module_name + ':' + p): 99 if perm is not None: 100 return perm 101 for perm in self._get_section(p): 102 if perm is not None: 103 return perm 104 105 return 0 106 107 def has_permission_for_changeset(self, rev): 108 changeset = self.repos.get_changeset(rev) 109 for change in changeset.get_changes(): 110 # the repository checks permissions for each change, so just check 111 # if any changes can be accessed 112 return 1 113 return 0 114 115 # Internal API 116 117 def _groups(self): 118 if not self.conf_authz.has_section('groups'): 119 return [] 120 121 grp_parents = {} 122 usr_grps = [] 123 124 for group in self.conf_authz.options('groups'): 125 for member in self.conf_authz.get('groups', group).split(','): 126 member = member.strip() 127 if member == self.auth_name: 128 usr_grps.append(group) 129 elif member.startswith('@'): 130 grp_parents.setdefault(member[1:], []).append(group) 131 132 expanded = {} 133 134 def expand_group(group): 135 if group in expanded: 136 return 137 expanded[group] = True 138 for parent in grp_parents.get(group, []): 139 expand_group(parent) 140 141 for g in usr_grps: 142 expand_group(g) 143 144 # expand groups 145 return expanded.keys() 146 147 def _get_section(self, section): 148 if not self.conf_authz.has_section(section): 149 return 150 151 yield self._get_permission(section, self.auth_name) 152 153 group_perm = None 154 for g in self.groups: 155 p = self._get_permission(section, '@' + g) 156 if p is not None: 157 group_perm = p 158 159 if group_perm: 160 yield 1 161 162 yield group_perm 163 164 yield self._get_permission(section, '*') 165 166 def _get_permission(self, section, subject): 167 if self.conf_authz.has_option(section, subject): 168 return 'r' in self.conf_authz.get(section, subject) 169 return None 59 groups = {} 60 aliases = {} 61 sections = {} 62 section = None 63 lineno = 0 64 for line in authz.splitlines(): 65 lineno += 1 66 line = to_unicode(line.strip()) 67 if not line or line.startswith('#') or line.startswith(';'): 68 continue 69 if line.startswith('[') and line.endswith(']'): 70 section = line[1:-1] 71 continue 72 if section is None: 73 raise ParseError(_('Line %(lineno)d: Entry before first ' 74 'section header', lineno=lineno)) 75 parts = line.split('=', 1) 76 if len(parts) != 2: 77 raise ParseError(_('Line %(lineno)d: Invalid entry', 78 lineno=lineno)) 79 name, value = parts 80 name = name.strip() 81 if section == 'groups': 82 group = groups.setdefault(name, set()) 83 group.update(each.strip() for each in value.split(',')) 84 elif section == 'aliases': 85 aliases[name] = value.strip() 86 else: 87 sections.setdefault(section, []).append((name.strip(), value)) 88 89 def resolve(subject, done): 90 if subject.startswith('@'): 91 done.add(subject) 92 for members in groups[subject[1:]] - done: 93 for each in resolve(members, done): 94 yield each 95 elif subject.startswith('&'): 96 yield aliases[subject[1:]] 97 else: 98 yield subject 99 100 authz = {} 101 for name, items in sections.iteritems(): 102 parts = name.split(':', 1) 103 module = authz.setdefault(len(parts) > 1 and parts[0] or '', {}) 104 section = module.setdefault(parts[-1], {}) 105 for subject, perms in items: 106 for user in resolve(subject, set()): 107 section.setdefault(user, 'r' in perms) # The first match wins 108 109 return authz 110 111 112 class AuthzSourcePolicy(Component): 113 """Permission policy for `source:` and `changeset:` resources using a 114 Subversion authz file. 115 116 `FILE_VIEW` and `BROWSER_VIEW` permissions are granted as specified in the 117 authz file. 118 119 `CHANGESET_VIEW` permission is granted for changesets where `FILE_VIEW` is 120 granted on at least one modified file, as well as empty for changesets. 121 """ 122 123 implements(IPermissionPolicy) 124 125 authz_file = PathOption('trac', 'authz_file', '', 126 """The path to the Subversion 127 [http://svnbook.red-bean.com/en/1.5/svn.serverconfig.pathbasedauthz.html authorization (authz) file]. 128 To enable authz permission checking, the `AuthzSourcePolicy` permission 129 policy must be added to `[trac] permission_policies`. 130 """) 131 132 authz_module_name = Option('trac', 'authz_module_name', '', 133 """The module prefix used in the `authz_file` for the default 134 repository. If left empty, the global sections will be used. 135 """) 136 137 _mtime = 0 138 _authz = {} 139 _users = set() 140 141 # IPermissionPolicy methods 142 143 def check_permission(self, action, username, resource, perm): 144 if action == 'FILE_VIEW' or action == 'BROWSER_VIEW': 145 authz, users = self._get_authz_info() 146 if authz is None: 147 return False 148 if resource is None: 149 return users is True or username in users 150 if resource.realm == 'source': 151 modules = [resource.parent.id or self.authz_module_name] 152 if modules[0]: 153 modules.append('') 154 for p in parent_iter(resource.id): 155 for module in modules: 156 section = authz.get(module, {}).get(p, {}) 157 result = section.get(username) 158 if result is not None: 159 return result 160 result = section.get('*') 161 if result is not None: 162 return result 163 return False 164 elif action == 'CHANGESET_VIEW': 165 authz, users = self._get_authz_info() 166 if authz is None: 167 return False 168 if resource is None: 169 return users is True or username in users 170 if resource.realm == 'changeset': 171 rm = RepositoryManager(self.env) 172 repos = rm.get_repository(resource.parent.id) 173 changes = list(repos.get_changeset(resource.id).get_changes()) 174 if not changes: 175 return True 176 source = Resource('source', version=resource.id, 177 parent=resource.parent) 178 return any('FILE_VIEW' in perm(source(id=change[0])) 179 for change in changes) 180 181 def _get_authz_info(self): 182 try: 183 mtime = os.path.getmtime(self.authz_file) 184 except OSError, e: 185 if self._authz is not None: 186 self.log.error('Error accessing authz file: %s', 187 exception_to_unicode(e)) 188 self._mtime = mtime = 0 189 self._authz = None 190 self._users = set() 191 if mtime > self._mtime: 192 self._mtime = mtime 193 self.log.info('Parsing authz file: %s' % self.authz_file) 194 try: 195 self._authz = parse(read_file(self.authz_file)) 196 users = set(user for module in self._authz.itervalues() 197 for path in module.itervalues() 198 for user, result in path.iteritems() if result) 199 self._users = '*' in users or users 200 except Exception, e: 201 self._authz = None 202 self._users = set() 203 self.log.error('Error parsing authz file: %s', 204 exception_to_unicode(e)) 205 return self._authz, self._users -
trunk/trac/versioncontrol/svn_fs.py
r8899 r9125 55 55 NoSuchChangeset, NoSuchNode 56 56 from trac.versioncontrol.cache import CachedRepository 57 from trac.versioncontrol.svn_authz import SubversionAuthorizer58 57 from trac.util import embedded_numbers 59 58 from trac.util.text import exception_to_unicode, to_unicode … … 271 270 yield ("svn", prio*2) 272 271 273 def get_repository(self, type, dir, authname):272 def get_repository(self, type, dir, params): 274 273 """Return a `SubversionRepository`. 275 274 … … 280 279 self._version = self._get_version() 281 280 self.env.systeminfo.append(('Subversion', self._version)) 282 fs_repos = SubversionRepository(dir, None, self.log, 283 {'tags': self.tags, 284 'branches': self.branches}) 281 params.update(tags=self.tags, branches=self.branches) 282 fs_repos = SubversionRepository(dir, params, self.log) 285 283 if type == 'direct-svnfs': 286 284 repos = fs_repos 287 285 else: 288 repos = CachedRepository(self.env.get_db_cnx, fs_repos, None, 289 self.log) 286 repos = CachedRepository(self.env, fs_repos, self.log) 290 287 repos.has_linear_changesets = True 291 if authname:292 authz = SubversionAuthorizer(self.env, weakref.proxy(repos),293 authname)294 repos.authz = fs_repos.authz = authz295 288 return repos 296 289 … … 307 300 """Repository implementation based on the svn.fs API.""" 308 301 309 def __init__(self, path, authz, log, options={}):302 def __init__(self, path, params, log): 310 303 self.log = log 311 self.options = options312 304 self.pool = Pool() 313 305 314 306 # Remove any trailing slash or else subversion might abort 315 307 if isinstance(path, unicode): 316 self.path = path317 308 path_utf8 = path.encode('utf-8') 318 309 else: # note that this should usually not happen (unicode arg expected) 319 self.path = to_unicode(path)320 path_utf8 = self.path.encode('utf-8') 310 path_utf8 = to_unicode(path).encode('utf-8') 311 321 312 path_utf8 = os.path.normpath(path_utf8).replace('\\', '/') 313 self.path = path_utf8.decode('utf-8') 314 322 315 root_path_utf8 = repos.svn_repos_find_root_path(path_utf8, self.pool()) 323 316 if root_path_utf8 is None: … … 333 326 self.fs_ptr = repos.svn_repos_fs(self.repos) 334 327 335 uuid = fs.get_uuid(self.fs_ptr, self.pool()) 336 name = 'svn:%s:%s' % (uuid, _from_svn(path_utf8)) 337 338 Repository.__init__(self, name, authz, log) 328 self.uuid = fs.get_uuid(self.fs_ptr, self.pool()) 329 self.base = 'svn:%s:%s' % (self.uuid, _from_svn(root_path_utf8)) 330 name = 'svn:%s:%s' % (self.uuid, self.path) 331 332 Repository.__init__(self, name, params, log) 339 333 340 334 # if root_path_utf8 is shorter than the path_utf8, the difference is … … 390 384 self.repos = self.fs_ptr = self.pool = None 391 385 386 def get_base(self): 387 return self.base 388 392 389 def _get_tags_or_branches(self, paths): 393 390 """Retrieve known branches or tags.""" 394 for path in self. options.get(paths, []):391 for path in self.params.get(paths, []): 395 392 if path.endswith('*'): 396 393 folder = posixpath.dirname(path) … … 419 416 yield 'tags', n.path, n.created_path, n.created_rev 420 417 418 def get_path_url(self, path, rev): 419 url = self.params.get('url', '').rstrip('/') 420 if url: 421 if not path or path == '/': 422 return url 423 return url + '/' + path.lstrip('/') 424 421 425 def get_changeset(self, rev): 422 426 rev = self.normalize_rev(rev) 423 return SubversionChangeset(rev, self.authz, self.scope, 424 self.fs_ptr, self.pool) 427 return SubversionChangeset(self, rev, self.scope, self.pool) 428 429 def get_changeset_uid(self, rev): 430 return (self.uuid, rev) 425 431 426 432 def get_node(self, path, rev=None): 427 433 path = path or '' 428 self.authz.assert_permission(posixpath.join(self.scope,429 path.strip('/')))430 434 if path and path[-1] == '/': 431 435 path = path[:-1] … … 478 482 break 479 483 path = _from_svn(path_utf8) 480 if not self.authz.has_permission(path):481 break482 484 yield path, rev 483 485 del tmp1 … … 655 657 656 658 def __init__(self, path, rev, repos, pool=None, parent_root=None): 657 self.repos = repos658 659 self.fs_ptr = repos.fs_ptr 659 self.authz = repos.authz660 660 self.scope = repos.scope 661 661 self._scoped_path_utf8 = _to_svn(self.scope, path) … … 688 688 self.rev = self.created_rev 689 689 # TODO: check node id 690 Node.__init__(self, path, self.rev, _kindmap[node_type])690 Node.__init__(self, repos, path, self.rev, _kindmap[node_type]) 691 691 692 692 def get_content(self): … … 707 707 for item in entries.keys(): 708 708 path = posixpath.join(self.path, _from_svn(item)) 709 if not self.authz.has_permission(posixpath.join(self.scope,710 path.strip('/'))):711 continue712 709 yield SubversionNode(path, self._requested_rev, self.repos, 713 710 self.pool, self.root) … … 834 831 class SubversionChangeset(Changeset): 835 832 836 def __init__(self, re v, authz, scope, fs_ptr, pool=None):833 def __init__(self, repos, rev, scope, pool=None): 837 834 self.rev = rev 838 self.authz = authz839 835 self.scope = scope 840 self.fs_ptr = fs_ptr836 self.fs_ptr = repos.fs_ptr 841 837 self.pool = Pool(pool) 842 838 try: … … 854 850 else: 855 851 date = None 856 Changeset.__init__(self, re v, message, author, date)852 Changeset.__init__(self, repos, rev, message, author, date) 857 853 858 854 def get_properties(self): … … 884 880 885 881 # Filtering on `path` 886 if not (_is_path_within_scope(self.scope, path) and 887 self.authz.has_permission(path)): 882 if not _is_path_within_scope(self.scope, path): 888 883 continue 889 884 … … 895 890 896 891 # Ensure `base_path` is within the scope 897 if not (_is_path_within_scope(self.scope, base_path) and 898 self.authz.has_permission(base_path)): 892 if not _is_path_within_scope(self.scope, base_path): 899 893 base_path, base_rev = None, -1 900 894 -
trunk/trac/versioncontrol/svn_prop.py
r8734 r9125 148 148 revs_label = (_('merged'), _('blocked'))[name.endswith('blocked')] 149 149 revs_cols = has_eligible and 2 or None 150 repo s = self.env.get_repository()150 reponame = context.resource.parent.id 151 151 target_path = context.resource.id 152 repos = self.env.get_repository(reponame) 152 153 target_rev = context.resource.version 153 154 if has_eligible: … … 171 172 try: 172 173 node = repos.get_node(spath, target_rev) 173 if 'LOG_VIEW' in context.perm('source', spath): 174 resource = context.resource.parent.child('source', spath) 175 if 'LOG_VIEW' in context.perm(resource): 174 176 row = [_get_source_link(spath, context), 175 177 _get_revs_link(revs_label, context, spath, revs)] … … 229 231 def _get_source_link(spath, context): 230 232 """Return a link to a merge source.""" 233 reponame = context.resource.parent.id 231 234 return tag.a('/' + spath, title=_('View merge source'), 232 href=context.href.browser( spath,235 href=context.href.browser(reponame or None, spath, 233 236 rev=context.resource.version)) 234 237 … … 238 241 with "no revision" for none. 239 242 """ 243 reponame = context.resource.parent.id 240 244 if not revs: 241 245 return tag.span(label, title=_('No revisions')) 242 246 elif ',' in revs or '-' in revs: 243 revs_href = context.href.log( spath, revs=revs)247 revs_href = context.href.log(reponame or None, spath, revs=revs) 244 248 else: 245 revs_href = context.href.changeset(revs, spath)249 revs_href = context.href.changeset(revs, reponame or None, spath) 246 250 return tag.a(label, title=revs.replace(',', ', '), href=revs_href) 247 251 … … 261 265 # || source || added revs || removed revs || 262 266 # || source || removed || 263 repos = self.env.get_repository( )267 repos = self.env.get_repository(old_context.resource.parent.id) 264 268 def parse_sources(props): 265 269 sources = {} -
trunk/trac/versioncontrol/templates/browser.html
r9103 r9125 9 9 <xi:include href="macros.html" /> 10 10 <head> 11 <title>$ path</title>11 <title>${'/'.join(part.name for part in path_links[1:]) or '/'}</title> 12 12 <meta py:if="file and file.annotate" name="ROBOTS" content="NOINDEX, NOFOLLOW" /> 13 13 <meta py:if="dir" name="ROBOTS" content="NOINDEX" /> … … 24 24 }); 25 25 26 <py:if test="dir ">26 <py:if test="dir or repo"> 27 27 /* browsers using old WebKits have issues with expandDir... */ 28 28 var webkit_rev = /AppleWebKit\/(\d+)/.exec(navigator.userAgent); 29 29 if ( !webkit_rev || (521 - webkit_rev[1]).toString()[0] == "-" ) 30 enableExpandDir(null, $(" #dirlist tr"), {30 enableExpandDir(null, $("table.dirlist tr"), { 31 31 action: 'inplace', 32 32 range_min_secs: '$dir.range_min_secs', … … 36 36 <py:if test="file"> 37 37 <py:if test="file.annotate"> 38 enableBlame("${href.changeset()}/", "${reponame}", "${path}"); 38 39 enableBlame("${href.changeset()}/", "${path}"); 39 40 </py:if> … … 47 48 <div id="content" class="browser"> 48 49 49 <h1>${browser_path_links(path_links, stickyrev)}</h1> 50 51 <div id="jumprev"> 52 <form action="" method="get"> 53 <div> 54 <label for="rev" title="${stickyrev and _('Hint: clear the field to view latest revision') or None}"> 55 View revision:</label> 56 <input type="text" id="rev" name="rev" value="$stickyrev" size="6" /> 57 </div> 58 </form> 59 </div> 60 61 <div py:if="quickjump_entries" id="jumploc"> 62 <form action="" method="get"> 63 <div class="buttons"> 64 <label for="preselected">Visit:</label> 65 <select id="preselected" name="preselected"> 66 <option selected="selected" /> 67 <optgroup py:for="category, locations in groupby(quickjump_entries, key=lambda q: q[0])" 68 label="${category}"> 69 <option py:for="_, name, path, rev in locations" value="${href.browser(path, rev=rev)}">$name</option> 70 </optgroup> 71 </select> 72 <input type="submit" value="${_('Go!')}" title="Jump to the chosen preselected path" /> 73 </div> 74 </form> 75 </div> 50 <py:if test="dir or file"> 51 <py:choose> 52 <h1 py:when="repo and repo.repositories">Default Repository</h1> 53 <h1 py:otherwise=""><xi:include href="path_links.html" /></h1> 54 </py:choose> 55 56 <div id="jumprev"> 57 <form action="" method="get"> 58 <div> 59 <label for="rev" title="${stickyrev and _('Hint: clear the field to view latest revision') or None}"> 60 View revision:</label> 61 <input type="text" id="rev" name="rev" value="$stickyrev" size="6" /> 62 </div> 63 </form> 64 </div> 65 66 <div py:if="quickjump_entries" id="jumploc"> 67 <form action="" method="get"> 68 <div class="buttons"> 69 <label for="preselected">Visit:</label> 70 <select id="preselected" name="preselected"> 71 <option selected="selected" /> 72 <optgroup py:for="category, locations in groupby(quickjump_entries, key=lambda q: q[0])" 73 label="${category}"> 74 <option py:for="_, name, path, rev in locations" value="${href.browser(reponame, path, rev=rev)}">$name</option> 75 </optgroup> 76 </select> 77 <input type="submit" value="${_('Go!')}" title="Jump to the chosen preselected path" /> 78 </div> 79 </form> 80 </div> 81 </py:if> 76 82 77 83 <py:if test="dir"> 78 <table class="listing" id="dirlist"> 79 <thead> 80 <tr> 81 <py:def function="sortable_th(order, desc, class_, title)"> 82 <th class="$class_${order == class_ and (desc and ' desc' or ' asc') or ''}"> 83 <a title="Sort by $class_${order == class_ and not desc and 84 ' (descending)' or ''}" 85 href="${href.browser(path, rev=stickyrev, order=(class_ != 'name' and class_ or None), 86 desc=(class_ == order and not desc and 1 or None))}">$title</a> 87 </th> 88 </py:def> 89 ${sortable_th(dir.order, dir.desc, 'name', 'Name')} 90 ${sortable_th(dir.order, dir.desc, 'size', 'Size')} 91 <th class="rev">Rev</th> 92 ${sortable_th(dir.order, dir.desc, 'date', 'Age')} 93 <th class="change">Last Change</th> 94 </tr> 95 </thead> 84 <table class="listing dirlist" id="dirlist"> 85 <xi:include href="dirlist_thead.html" /> 96 86 <tbody> 97 87 <py:if test="'up' in chrome.links"> … … 115 105 <tr py:if="file"> 116 106 <th scope="col" i18n:msg="rev, size, author, date"> 117 Revision <a href="${href.changeset(rev )}">$rev</a>, ${sizeinfo(file.size)}107 Revision <a href="${href.changeset(rev, reponame)}">$rev</a>, ${sizeinfo(file.size)} 118 108 checked in by ${authorinfo(file.changeset.author)}, ${dateinfo(file.changeset.date)} ago 119 109 (<a href="${href.changeset(rev, created_path)}">diff</a>) … … 123 113 <td class="message searchable" py:choose=""> 124 114 <py:when test="wiki_format_messages" xml:space="preserve"> 125 ${wiki_to_html(context('changeset', file.changeset.rev), file.changeset.message, escape_newlines=True)} 115 ${wiki_to_html(context('changeset', file.changeset.rev, parent=repos.resource), 116 file.changeset.message, escape_newlines=True)} 126 117 </py:when> 127 118 <py:otherwise>${file.changeset.message}</py:otherwise> … … 150 141 </table> 151 142 143 <div py:if="dir and path == '/' and repoinfo" class="description"> 144 ${wiki_to_html(context('source', '/', parent=repos.resource), repoinfo.description)} 145 </div> 146 147 <py:if test="repo and repo.repositories"> 148 <hr py:if="dir"/> 149 <h1>Repository Index</h1> 150 <py:with vars="repoindex = 'repoindex'"> 151 <xi:include href="repository_index.html" /> 152 </py:with> 153 </py:if> 154 152 155 <div py:if="file and file.preview" id="preview" class="searchable"> 153 156 ${preview_file(file.preview)} … … 162 165 <form action="${href.diff()}" method="get"> 163 166 <div class="buttons"> 164 <input type="hidden" name="new_path" value="$ path" />165 <input type="hidden" name="old_path" value="$ path" />167 <input type="hidden" name="new_path" value="${'/' + pathjoin(reponame, path)}" /> 168 <input type="hidden" name="old_path" value="${'/' + pathjoin(reponame, path)}" /> 166 169 <input type="hidden" name="new_rev" value="$stickyrev" /> 167 170 <input type="hidden" name="old_rev" value="$stickyrev" /> -
trunk/trac/versioncontrol/templates/changeset.html
r9039 r9125 25 25 <div id="title" py:choose=""> 26 26 <h1 py:when="changeset and restricted"> 27 Changeset <a title="Show full changeset" href="${href.changeset(new_rev)}">$new_rev</a> 28 for <a title="Show entry in browser" href="${href.browser(new_path, rev=new_rev)}">$new_path</a> 27 Changeset <a title="Show full changeset" 28 href="${href.changeset(new_rev, reponame)}">$new_rev</a><py:if test="reponame"> in $reponame</py:if> 29 for <a title="Show entry in browser" href="${href.browser(reponame, new_path, rev=new_rev)}">$new_path</a> 29 30 </h1> 30 31 <h1 py:when="not changeset and restricted"> 31 Changes in <a title="Show entry in browser" href="${href.browser( new_path, rev=new_rev)}">$new_path</a>32 <a title="Show revision log" href="${href.log( new_path, rev=new_rev, stop_rev=old_rev)}">33 [$old_rev:$new_rev]</a> 32 Changes in <a title="Show entry in browser" href="${href.browser(reponame, new_path, rev=new_rev)}">$new_path</a> 33 <a title="Show revision log" href="${href.log(reponame, new_path, rev=new_rev, stop_rev=old_rev)}"> 34 [$old_rev:$new_rev]</a><py:if test="reponame"> in $reponame</py:if> 34 35 </h1> 35 36 <h1 py:when="not changeset and not restricted"> 36 Changes from <a title="Show entry in browser" href="${href.browser(old_path, rev=old_rev)}">$old_path</a> 37 at <a title="Show full changeset" href="${href.changeset(old_rev)}">r$old_rev</a> 38 to <a title="Show entry in browser" href="${href.browser(new_path, rev=new_rev)}">$new_path</a> 39 at <a title="Show full changeset" href="${href.changeset(new_rev)}">r$new_rev</a> 37 Changes<py:if test="reponame"> in $reponame</py:if> 38 from <a title="Show entry in browser" href="${href.browser(reponame, old_path, rev=old_rev)}">$old_path</a> 39 at <a title="Show full changeset" href="${href.changeset(old_rev, reponame)}">r$old_rev</a> 40 to <a title="Show entry in browser" href="${href.browser(reponame, new_path, rev=new_rev)}">$new_path</a> 41 at <a title="Show full changeset" href="${href.changeset(new_rev, reponame)}">r$new_rev</a> 40 42 </h1> 41 <h1 py:otherwise="">Changeset <a py:strip="not annotated" title="Show full changeset" href="${href.changeset(new_rev)}">$new_rev</a></h1> 43 <h1 py:otherwise="">Changeset <a 44 py:strip="not annotated" 45 title="Show full changeset" 46 href="${href.changeset(new_rev, reponame)}">$new_rev</a><py:if test="reponame"> in $reponame</py:if></h1> 42 47 </div> 43 48 … … 47 52 <div> 48 53 <py:if test="not changeset"> 49 <input type="hidden" name="old_path" value="$ old_path" />50 <input type="hidden" name="new_path" value="$ new_path" />54 <input type="hidden" name="old_path" value="${'/' + pathjoin(reponame, old_path)}" /> 55 <input type="hidden" name="new_path" value="${'/' + pathjoin(reponame, new_path)}" /> 51 56 <input type="hidden" name="old" value="$old_rev" /> 52 57 <input type="hidden" name="new" value="$new_rev" /> 53 58 </py:if> 54 ${diff_options_fields(diff)}59 <xi:include href="diff_options.html" /> 55 60 </div> 56 61 </form> … … 118 123 </py:when> 119 124 <py:when test="wiki_format_messages"> 120 ${wiki_to_html(context ('changeset', changeset.rev), changeset.message, escape_newlines=True)}125 ${wiki_to_html(context, changeset.message, escape_newlines=True)} 121 126 </py:when> 122 127 <py:otherwise><pre>${changeset.message}</pre></py:otherwise> … … 125 130 <py:if test="location"> 126 131 <dt class="property location">Location:</dt> 127 <dd class="searchable"><a href="${req.href.browser( location, rev=new_rev)}">$location</a></dd>132 <dd class="searchable"><a href="${req.href.browser(reponame, location, rev=new_rev)}">$location</a></dd> 128 133 </py:if> 129 134 <dt class="property files">${files and 'Files:' or '(No files)'}</dt> -
trunk/trac/versioncontrol/templates/dir_entries.html
r6139 r9125 7 7 </py:if> 8 8 <py:for each="idx, entry in enumerate(dir.entries)"> 9 <py:with vars="change = dir.changes[entry.rev]"> 9 <py:with vars="change = dir.changes[entry.rev]; 10 chgset_context = change and context('changeset', change.rev, parent=repos.resource); 11 chgset_view = change and change.can_view(perm)"> 10 12 <tr class="${idx % 2 and 'even' or 'odd'}"> 11 13 <td class="name"> 12 14 <a class="$entry.kind" title="View ${entry.kind.capitalize()}" 13 href="${href.browser(entry.path, rev=stickyrev, order=(dir.order != 'name' and dir.order or None), desc=dir.desc)}">$entry.name</a> 15 href="${href.browser(reponame, entry.path, rev=stickyrev, 16 order=(order != 'name' and order or None), desc=desc)}">$entry.name</a> 14 17 </td> 15 18 <td class="size">${sizeinfo(entry.content_length)}</td> 16 19 <td class="rev"> 17 <a title="View Revision Log" href="${href.log(entry.path, rev=rev)}">$entry.rev</a> 20 <a title="View Revision Log" href="${href.log(reponame, entry.path, rev=rev)}">$entry.rev</a> 21 <a title="View Changeset" class="chgset" href="${href.changeset(change.rev, reponame)}"> </a> 18 22 </td> 19 <td class="age" style="${ch angeand dir.timerange and 'border-color: rgb(%s,%s,%s)' %20 dir.colorize_age(dir.timerange.relative(change.date)) or None}">21 ${ch ange and dateinfo(change.date) or '-'}23 <td class="age" style="${chgset_view and dir.timerange and 'border-color: rgb(%s,%s,%s)' % 24 dir.colorize_age(dir.timerange.relative(change.date)) or None}"> 25 ${chgset_view and dateinfo(change.date) or '–'} 22 26 </td> 23 <td class="change"> 24 <span class="author" py:if="change">${authorinfo(change.author)}:</span> 25 <span class="change" py:choose="" py:with="chgset_context = context('changeset', change.rev)"> 26 <py:when test="not change or 'CHANGESET_VIEW' not in perm(chgset_context.resource)">-</py:when> 27 <py:when test="wiki_format_messages"> 28 ${change and wiki_to_oneliner(chgset_context, change.message, shorten=True)} 29 </py:when> 30 <py:otherwise>${change and shorten_line(change.message)}</py:otherwise> 31 </span> 27 <td class="change" py:choose=""> 28 <py:when test="chgset_view"> 29 <span class="author">${authorinfo(change.author)}:</span> 30 <span class="change" py:choose=""> 31 <py:when test="wiki_format_messages"> 32 ${wiki_to_oneliner(chgset_context, change.message, shorten=True)} 33 </py:when> 34 <py:otherwise>${shorten_line(change.message)}</py:otherwise> 35 </span> 36 </py:when> 37 <py:otherwise>–</py:otherwise> 32 38 </td> 33 39 </tr> -
trunk/trac/versioncontrol/templates/revisionlog.html
r8730 r9125 9 9 <xi:include href="macros.html" /> 10 10 <head> 11 <title>$ path(log)</title>11 <title>${'/'.join(part.name for part in path_links[1:]) or '/'} (log)</title> 12 12 </head> 13 13 14 14 <body> 15 15 <div id="content" class="log"> 16 <h1> ${browser_path_links(path_links)}</h1>16 <h1><xi:include href="path_links.html" /></h1> 17 17 18 18 <form id="prefs" action="" method="get"> … … 82 82 </div> 83 83 84 85 84 <form py:for="items in item_ranges" class="printableform" action="${href.changeset()}" method="get"> 86 85 <div class="buttons"> 87 <input type="submit" value="${_('View changes')}" 88 title="${_('Diff from Old Revision to New Revision (select them below)')}" /> 86 <input type="hidden" name="reponame" value="$reponame" /> 87 <input type="submit" value="View changes" 88 title="${_('Diff from Old Revision to New Revision (as selected in the Diff column)')}" /> 89 89 </div> 90 90 <table class="listing chglist"> … … 94 94 <th class="change"></th> 95 95 <th class="rev">Rev</th> 96 <th class="chgset">Chgset</th> 97 <th class="date">Date</th> 96 <th class="age">Age</th> 98 97 <th class="author">Author</th> 99 98 <th class="summary"><py:if test="not verbose">Log Message</py:if></th> … … 112 111 <py:with vars="change = changes[item.rev]; 113 112 is_separator = item.change is None; 114 chgset_context = context('changeset', change.rev); 115 chgset_view = 'CHANGESET_VIEW' in perm(chgset_context.resource); 113 chgset_context = context('changeset', change.rev, parent=repos.resource); 116 114 odd_even = idx % 2 and 'odd' or 'even'"> 117 115 <!--! highlight copy or rename operations --> 118 116 <tr py:if="not is_separator and item.get('copyfrom_path')" class="$odd_even"> 119 117 <td /> 120 <td class="copyfrom_path" colspan=" 7" style="padding-left: ${item.depth - 1}em">121 copied from <a href="${href.browser( item.path, rev=item.rev)}">$item.copyfrom_path</a>:118 <td class="copyfrom_path" colspan="6" style="padding-left: ${item.depth - 1}em"> 119 copied from <a href="${href.browser(reponame, item.path, rev=item.rev)}">$item.copyfrom_path</a>: 122 120 </td> 123 121 </tr> 124 122 125 <tr class="$ odd_even" py:choose="">126 <td class="diff" rowspan="${verbose and 2 or None}">123 <tr class="${classes(odd_even,verbose=verbose)}" py:choose=""> 124 <td class="diff"> 127 125 <input type="radio" name="old" value="${item.rev}@${item.path}" 128 126 checked="${idx == (len(items) - 1) or None}" title="From r${item.rev}" /> … … 132 130 <py:when test="not is_separator"> 133 131 <td class="change" style="padding-left: ${item.depth}em"> 134 <a title="View log starting at this revision" href="${href.log( item.path, rev=item.rev)}">132 <a title="View log starting at this revision" href="${href.log(reponame, item.path, rev=item.rev)}"> 135 133 <span class="$item.change"></span> 136 134 <span class="comment">($item.change)</span> … … 138 136 </td> 139 137 <td class="rev"> 140 <a title="Browse at revision $item.existing_rev" href="${href.browser( item.path, rev=item.existing_rev)}">138 <a title="Browse at revision $item.existing_rev" href="${href.browser(reponame, item.path, rev=item.existing_rev)}"> 141 139 @$item.existing_rev</a> 140 <py:choose test="item.change"> 141 <a py:when="'delete'" title="View removal changeset [$item.rev]" class="chgset" 142 href="${href.changeset(item.rev)}"> </a> 143 <a py:otherwise="" title="View changeset [$item.rev] for $item.path" class="chgset" 144 href="${href.changeset(item.rev, reponame, item.path)}"> </a> 145 </py:choose> 142 146 </td> 143 <td class="chgset" py:choose="item.change"> 144 <a py:when="'delete'" title="View removal changeset [$item.rev]" href="${href.changeset(item.rev)}"> 145 [$item.rev]</a> 146 <a py:otherwise="" title="View changeset [$item.rev] for $item.path" href="${href.changeset(item.rev, item.path)}"> 147 [$item.rev]</a> 148 </td> 149 <td class="date" py:content="dateinfo(change.date)" /> 147 <td class="age" py:content="dateinfo(change.date)" /> 150 148 <td class="author" py:content="authorinfo(change.author)" /> 151 149 <td class="summary" py:choose=""> 152 <py:when test="verbose or not chgset_view"></py:when>150 <py:when test="verbose"></py:when> 153 151 <py:when test="wiki_format_messages"> 154 152 ${wiki_to_oneliner(chgset_context, change.message, shorten=True)} … … 157 155 </td> 158 156 </py:when> 159 <td colspan=" 6" class="separator" py:otherwise="" />157 <td colspan="5" py:otherwise="" /> 160 158 </tr> 161 159 162 <tr py:if="verbose and not is_separator" class="$odd_even verbose"> 163 <py:choose> 164 <td py:when="chgset_view" class="summary" colspan="6" 165 py:choose="" xml:space="preserve"> 166 <py:when test="wiki_format_messages"> 167 ${wiki_to_html(chgset_context, change.message, escape_newlines=True)} 168 </py:when> 169 <py:otherwise><pre>${change.message}</pre></py:otherwise> 170 </td> 171 </py:choose> 160 <tr py:if="verbose and not is_separator" class="summary verbose $odd_even"> 161 <td class="filler" colspan="2" /> 162 <td class="log" colspan="4" py:choose="" xml:space="preserve"> 163 <py:when test="wiki_format_messages"> 164 ${wiki_to_html(chgset_context, change.message, escape_newlines=True)} 165 </py:when> 166 <py:otherwise><pre>${change.message}</pre></py:otherwise> 167 </td> 172 168 </tr> 173 169 … … 179 175 <div py:if="len(items) > 10 and len(item_ranges) == 1" class="buttons"> 180 176 <input type="submit" value="${_('View changes')}" 181 title="${_('Diff from Old Revision to New Revision ( select them above)')}" />177 title="${_('Diff from Old Revision to New Revision (as selected in the Diff column)')}" /> 182 178 </div> 183 179 </form> -
trunk/trac/versioncontrol/templates/revisionlog.rss
r6384 r9125 4 4 xmlns:xi="http://www.w3.org/2001/XInclude"> 5 5 <xi:include href="macros.rss" /> 6 <channel py:with="log_href = abs_href.log( path, rev=rev)">6 <channel py:with="log_href = abs_href.log(reponame, path, rev=rev)"> 7 7 <title>Revisions of $path</title> 8 8 <link>$log_href</link> 9 <description>Trac Log - Revisions of $path< /description>9 <description>Trac Log - Revisions of $path<py:if test="reponame"> in $reponame</py:if></description> 10 10 <language>en-US</language> 11 11 <generator>Trac ${trac.version}</generator> … … 18 18 <item py:for="item in items" 19 19 py:with="change = changes[item.rev]; 20 item_context = context('changeset', change.rev )">20 item_context = context('changeset', change.rev, parent=repos.resource)"> 21 21 ${author_or_creator(change.author, email_map)} 22 22 <pubDate>${http_date(change.date)}</pubDate> 23 23 <title>Revision $item.rev: ${shorten_line(change.message)}</title> 24 <link>${abs_href.changeset(item.rev, item.path)}</link>25 <guid isPermaLink="false">${abs_href.changeset(item.rev, item.path)}</guid>24 <link>${abs_href.changeset(item.rev, reponame, item.path)}</link> 25 <guid isPermaLink="false">${abs_href.changeset(item.rev, reponame, item.path)}</guid> 26 26 <description py:with="m = change.message">${ 27 27 unicode(wiki_format_messages and (verbose and wiki_to_html(item_context, m) \ -
trunk/trac/versioncontrol/templates/revisionlog.txt
r8999 r9125 1 1 # 2 # ChangeLog for $path 2 # ChangeLog for $path${reponame and _(" in $(reponame)s", reponame=reponame) or None} 3 3 # 4 4 # Generated by Trac $trac.version … … 9 9 ${http_date(change.date)} ${format_author(change.author)} [$item.rev] 10 10 {% for idx, file in enumerate(extra.files) %}\ 11 {% if 'FILE_VIEW' in perm(repos.resource.child('source', file, version=change.rev)) %}\ 11 12 * $file (${dict(edit='modified', add='added', delete='deleted', 12 13 copy='copied', move='moved')[extra.actions[idx]]}) 14 {% end %}\ 13 15 {% end %}\ 14 16 -
trunk/trac/versioncontrol/tests/api.py
r6038 r9125 22 22 23 23 def setUp(self): 24 self.repo_base = Repository('testrepo', None, None) 24 self.repo_base = Repository('testrepo', {'name': 'testrepo', 'id': 1}, 25 None) 25 26 26 27 def test_raise_NotImplementedError_close(self): 27 28 self.failUnlessRaises(NotImplementedError, self.repo_base.close) 28 29 def test_raise_NotImplementedError_sync_changeset(self):30 self.failUnlessRaises(NotImplementedError, self.repo_base.sync_changeset, 1)31 29 32 30 def test_raise_NotImplementedError_get_changeset(self): -
trunk/trac/versioncontrol/tests/cache.py
r8734 r9125 17 17 from datetime import datetime 18 18 19 from trac.log import logger_factory 20 from trac.test import Mock, InMemoryDatabase 19 from trac.test import EnvironmentStub, Mock 21 20 from trac.util.datefmt import to_timestamp, utc 22 21 from trac.versioncontrol import Repository, Changeset, Node, NoSuchChangeset … … 29 28 30 29 def setUp(self): 31 self.db = InMemoryDatabase() 32 self.log = logger_factory('test') 30 self.env = EnvironmentStub() 31 self.db = self.env.get_db_cnx() 32 self.log = self.env.log 33 33 cursor = self.db.cursor() 34 cursor.execute("INSERT INTO system (name, value) VALUES (%s,%s)", 35 ('youngest_rev', '')) 34 cursor.executemany("INSERT INTO repository (id,name,value) " 35 "VALUES (%s,%s,%s)", 36 [(1, 'name', 'test-repos'), 37 (1, 'youngest_rev', '')]) 38 39 def tearDown(self): 40 self.env.reset_db() 36 41 37 42 def test_initial_sync_with_empty_repos(self): … … 39 44 raise NoSuchChangeset(rev) 40 45 41 repos = Mock(Repository, 'test-repos', None, self.log, 46 repos = Mock(Repository, 'test-repos', {'name': 'test-repos', 'id': 1}, 47 self.log, 42 48 get_changeset=no_changeset, 43 49 get_oldest_rev=lambda: 1, … … 45 51 normalize_rev=no_changeset, 46 52 next_rev=lambda x: None) 47 cache = CachedRepository(self. db, repos, None, self.log)53 cache = CachedRepository(self.env, repos, self.log) 48 54 cache.sync() 49 55 … … 59 65 changes = [('trunk', Node.DIRECTORY, Changeset.ADD, None, None), 60 66 ('trunk/README', Node.FILE, Changeset.ADD, None, None)] 61 changesets = [Mock(Changeset, 0, '', '', t1, 62 get_changes=lambda: []), 63 Mock(Changeset, 1, 'Import', 'joe', t2, 64 get_changes=lambda: iter(changes))] 65 repos = Mock(Repository, 'test-repos', None, self.log, 67 repos = Mock(Repository, 'test-repos', {'name': 'test-repos', 'id': 1}, 68 self.log, 66 69 get_changeset=lambda x: changesets[int(x)], 67 70 get_oldest_rev=lambda: 0, … … 69 72 normalize_rev=lambda x: x, 70 73 next_rev=lambda x: int(x) == 0 and 1 or None) 71 cache = CachedRepository(self.db, repos, None, self.log) 74 changesets = [Mock(Changeset, repos, 0, '', '', t1, 75 get_changes=lambda: []), 76 Mock(Changeset, repos, 1, 'Import', 'joe', t2, 77 get_changes=lambda: iter(changes))] 78 cache = CachedRepository(self.env, repos, self.log) 72 79 cache.sync() 73 80 … … 90 97 t3 = datetime(2003, 1, 1, 1, 1, 1, 0, utc) 91 98 cursor = self.db.cursor() 92 cursor.execute("INSERT INTO revision (rev,time,author,message) " 93 "VALUES (0,%s,'','')", (to_timestamp(t1),)) 94 cursor.execute("INSERT INTO revision (rev,time,author,message) " 95 "VALUES (1,%s,'joe','Import')", (to_timestamp(t2),)) 96 cursor.executemany("INSERT INTO node_change (rev,path,node_type," 97 "change_type,base_path,base_rev) " 98 "VALUES ('1',%s,%s,%s,%s,%s)", 99 cursor.execute("INSERT INTO revision (repos,rev,time,author,message) " 100 "VALUES (1,0,%s,'','')", 101 (to_timestamp(t1),)) 102 cursor.execute("INSERT INTO revision (repos,rev,time,author,message) " 103 "VALUES (1,1,%s,'joe','Import')", 104 (to_timestamp(t2),)) 105 cursor.executemany("INSERT INTO node_change (repos,rev,path," 106 "node_type,change_type,base_path,base_rev) " 107 "VALUES (1,'1',%s,%s,%s,%s,%s)", 99 108 [('trunk', 'D', 'A', None, None), 100 109 ('trunk/README', 'F', 'A', None, None)]) 101 cursor.execute("UPDATE system SET value='1' WHERE name='youngest_rev'") 110 cursor.execute("UPDATE repository SET value='1' " 111 "WHERE id=1 AND name='youngest_rev'") 102 112 103 113 changes = [('trunk/README', Node.FILE, Changeset.EDIT, 'trunk/README', 1)] 104 changeset = Mock(Changeset, 2, 'Update', 'joe', t3, 105 get_changes=lambda: iter(changes)) 106 repos = Mock(Repository, 'test-repos', None, self.log, 114 repos = Mock(Repository, 'test-repos', {'name': 'test-repos', 'id': 1}, 115 self.log, 107 116 get_changeset=lambda x: changeset, 108 117 get_youngest_rev=lambda: 2, … … 110 119 normalize_rev=lambda x: x, 111 120 next_rev=lambda x: x and int(x) == 1 and 2 or None) 112 cache = CachedRepository(self.db, repos, None, self.log) 121 changeset = Mock(Changeset, repos, 2, 'Update', 'joe', t3, 122 get_changes=lambda: iter(changes)) 123 cache = CachedRepository(self.env, repos, self.log) 113 124 cache.sync() 114 125 … … 127 138 t2 = datetime(2002, 1, 1, 1, 1, 1, 0, utc) 128 139 cursor = self.db.cursor() 129 cursor.execute("INSERT INTO revision (rev,time,author,message) " 130 "VALUES (0,%s,'','')", (to_timestamp(t1),)) 131 cursor.execute("INSERT INTO revision (rev,time,author,message) " 132 "VALUES (1,%s,'joe','Import')", (to_timestamp(t2),)) 133 cursor.executemany("INSERT INTO node_change (rev,path,node_type," 134 "change_type,base_path,base_rev) " 135 "VALUES ('1',%s,%s,%s,%s,%s)", 140 cursor.execute("INSERT INTO revision (repos,rev,time,author,message) " 141 "VALUES (1,0,%s,'','')", 142 (to_timestamp(t1),)) 143 cursor.execute("INSERT INTO revision (repos,rev,time,author,message) " 144 "VALUES (1,1,%s,'joe','Import')", 145 (to_timestamp(t2),)) 146 cursor.executemany("INSERT INTO node_change (repos,rev,path," 147 "node_type,change_type,base_path,base_rev) " 148 "VALUES (1,'1',%s,%s,%s,%s,%s)", 136 149 [('trunk', 'D', 'A', None, None), 137 150 ('trunk/README', 'F', 'A', None, None)]) 138 cursor.execute("UPDATE system SET value='1' WHERE name='youngest_rev'") 151 cursor.execute("UPDATE repository SET value='1' " 152 "WHERE id=1 AND name='youngest_rev'") 139 153 140 repos = Mock(Repository, 'test-repos', None, self.log, 154 repos = Mock(Repository, 'test-repos', {'name': 'test-repos', 'id': 1}, 155 self.log, 141 156 get_changeset=lambda x: None, 142 157 get_youngest_rev=lambda: 1, … … 144 159 next_rev=lambda x: None, 145 160 normalize_rev=lambda rev: rev) 146 cache = CachedRepository(self. db, repos, None, self.log)161 cache = CachedRepository(self.env, repos, self.log) 147 162 self.assertEqual('1', cache.youngest_rev) 148 163 changeset = cache.get_changeset(1) -
trunk/trac/versioncontrol/tests/svn_authz.py
r8734 r9125 1 # -*- coding: utf-8 -*- 2 # 3 # Copyright (C) 2010 Edgewall Software 4 # All rights reserved. 5 # 6 # This software is licensed as described in the file COPYING, which 7 # you should have received as part of this distribution. The terms 8 # are also available at http://trac.edgewall.org/wiki/TracLicense. 9 # 10 # This software consists of voluntary contributions made by many 11 # individuals. For the exact contribution history, see the revision 12 # history and logs, available at http://trac.edgewall.org/log/. 13 14 import os.path 15 import tempfile 1 16 import unittest 2 import sys 3 4 def tests(): 5 """ 6 Subversion Authz File Permissions 7 ================================= 8 9 Setup code 10 ---------- 11 We'll use the ``make_auth`` method to create Authorizer objects 12 for testing the use of authz files. ``make_auth`` takes a module name 13 and a string for the authz configuration contents. 14 15 >>> from trac.versioncontrol.svn_authz import RealSubversionAuthorizer 16 >>> from StringIO import StringIO 17 >>> make_auth = lambda mod, cfg: RealSubversionAuthorizer(None, 18 ... 'user', mod, None, StringIO(cfg)) 19 20 21 Simple operation 22 ---------------- 23 Returns 1 if no path is given: 24 >>> int(make_auth('', '').has_permission(None)) 25 1 26 27 By default read permission is not enabled: 28 >>> int(make_auth('', '').has_permission('/')) 29 0 30 31 Read and Write Permissions 32 ---------------------- 33 Trac is only concerned about read permissions. 34 >>> a = make_auth('', ''' 35 ... [/readonly] 36 ... user = r 37 ... [/writeonly] 38 ... user = w 39 ... [/readwrite] 40 ... user = rw 41 ... [/empty] 42 ... user = 43 ... ''') 44 45 Permissions of 'r' or 'rw' will allow access: 46 >>> int(a.has_permission('/readonly')) 47 1 48 >>> int(a.has_permission('/readwrite')) 49 1 50 51 If only 'w' permission is given, Trac does not allow access: 52 >>> int(a.has_permission('/writeonly')) 53 0 54 55 And an empty permission does not give access: 56 >>> int(a.has_permission('/empty')) 57 0 58 59 Trailing Slashes 60 ---------------- 61 Checks all combinations of trailing slashes in the configuration 62 or in the path parameter: 63 >>> a = make_auth('', ''' 64 ... [/a] 65 ... user = r 66 ... [/b/] 67 ... user = r 68 ... ''') 69 >>> int(a.has_permission('/a')) 70 1 71 >>> int(a.has_permission('/a/')) 72 1 73 >>> int(a.has_permission('/b')) 74 1 75 >>> int(a.has_permission('/b/')) 76 1 77 78 79 Module Usage 80 ------------ 81 If a module name is specified, the rules used are specific to the module. 82 >>> a = make_auth('module', ''' 83 ... [module:/a] 84 ... user = r 85 ... [other:/b] 86 ... user = r 87 ... ''') 88 >>> int(a.has_permission('/a')) 89 1 90 >>> int(a.has_permission('/b')) 91 0 92 93 If a module is specified, but the configuration contains a non-module 94 path, the non-module path can still apply: 95 >>> int(make_auth('module', ''' 96 ... [/a] 97 ... user = r 98 ... ''').has_permission('/a')) 99 1 100 101 However, the module-specific rule will take precedence if both exist: 102 >>> int(make_auth('module', ''' 103 ... [module:/a] 104 ... user = 105 ... [/a] 106 ... user = r 107 ... ''').has_permission('/a')) 108 0 109 110 111 Groups and Wildcards 112 -------------------- 113 Authz provides a * wildcard for matching any user: 114 >>> int(make_auth('', ''' 115 ... [/a] 116 ... * = r 117 ... ''').has_permission('/a')) 118 1 119 120 Groups are specified in a separate section and used with an @ prefix: 121 >>> int(make_auth('', ''' 122 ... [groups] 123 ... grp = user 124 ... [/a] 125 ... @grp = r 126 ... ''').has_permission('/a')) 127 1 128 129 Groups can also be members of other groups: 130 >>> int(make_auth('', ''' 131 ... [groups] 132 ... grp1 = user 133 ... grp2 = @grp1 134 ... [/a] 135 ... @grp2 = r 136 ... ''').has_permission('/a')) 137 1 138 139 Groups should not be defined cyclically, but they are handled appropriately 140 to avoid infinite loops: 141 >>> int(make_auth('', ''' 142 ... [groups] 143 ... grp1 = @grp2 144 ... grp2 = @grp3 145 ... grp3 = @grp1, user 146 ... [/a] 147 ... @grp1 = r 148 ... ''').has_permission('/a')) 149 1 150 151 If more than one group matches at the specific path, access is granted 152 if any of the group rules allow access. 153 >>> a = make_auth('', ''' 154 ... [groups] 155 ... grp1 = user 156 ... grp2 = user 157 ... [/a] 158 ... @grp1 = r 159 ... @grp2 = 160 ... [/b] 161 ... @grp1 = 162 ... @grp2 = r 163 ... ''') 164 >>> int(a.has_permission('/a')) 165 1 166 >>> int(a.has_permission('/b')) 167 1 168 169 170 Precedence 171 ---------- 172 Precedence is user, group, then *: 173 >>> a = make_auth('', ''' 174 ... [groups] 175 ... grp = user 176 ... [/a] 177 ... @grp = r 178 ... user = 179 ... [/b] 180 ... * = r 181 ... @grp = 182 ... ''') 183 184 User specific permission overrides the group permission: 185 >>> int(a.has_permission('/a')) 186 0 187 188 And group permission overrides the * permission: 189 >>> int(a.has_permission('/b')) 190 0 191 192 The most specific matching path takes precedence: 193 >>> a = make_auth('', ''' 194 ... [/] 195 ... * = r 196 ... [/b] 197 ... user = 198 ... ''') 199 >>> int(a.has_permission('/')) 200 1 201 >>> int(a.has_permission('/a')) 202 1 203 >>> int(a.has_permission('/b')) 204 0 205 206 Changeset Permissions 207 --------------------- 208 A test should go here for the changeset permissions. 209 """ 17 18 from trac.resource import Resource 19 from trac.test import EnvironmentStub 20 from trac.util import create_file 21 from trac.versioncontrol.svn_authz import AuthzSourcePolicy, ParseError, \ 22 parse 23 24 25 class AuthzParserTestCase(unittest.TestCase): 26 27 def test_parse_file(self): 28 authz = parse("""\ 29 [groups] 30 developers = foo, bar 31 users = @developers, &baz 32 33 [aliases] 34 baz = CN=Hàröld Hacker,OU=Enginéers,DC=red-bean,DC=com 35 36 # Applies to all repositories 37 [/] 38 * = r 39 40 [/trunk] 41 @developers = rw 42 &baz = 43 @users = r 44 45 [/branches] 46 bar = rw 47 48 ; Applies only to module 49 [module:/trunk] 50 foo = rw 51 &baz = r 52 """) 53 self.assertEqual({ 54 '': { 55 '/': { 56 '*': True, 57 }, 58 '/trunk': { 59 'foo': True, 60 'bar': True, 61 u'CN=Hàröld Hacker,OU=Enginéers,DC=red-bean,DC=com': False, 62 }, 63 '/branches': { 64 'bar': True, 65 }, 66 }, 67 'module': { 68 '/trunk': { 69 'foo': True, 70 u'CN=Hàröld Hacker,OU=Enginéers,DC=red-bean,DC=com': True, 71 }, 72 }, 73 }, authz) 74 75 def test_parse_errors(self): 76 self.assertRaises(ParseError, parse, """\ 77 user = r 78 79 [module:/trunk] 80 user = r 81 """) 82 self.assertRaises(ParseError, parse, """\ 83 [module:/trunk] 84 user 85 """) 86 87 88 class AuthzSourcePolicyTestCase(unittest.TestCase): 89 90 def setUp(self): 91 tmpdir = os.path.realpath(tempfile.gettempdir()) 92 self.authz = os.path.join(tmpdir, 'trac-authz') 93 create_file(self.authz, """\ 94 [groups] 95 group1 = user 96 group2 = @group1 97 98 cycle1 = @cycle2 99 cycle2 = @cycle3 100 cycle3 = @cycle1, user 101 102 alias1 = &jekyll 103 alias2 = @alias1 104 105 [aliases] 106 jekyll = Mr Hyde 107 108 # Read / write permissions 109 [/readonly] 110 user = r 111 [/writeonly] 112 user = w 113 [/readwrite] 114 user = rw 115 [/empty] 116 user = 117 118 # Trailing slashes 119 [/trailing_a] 120 user = r 121 [/trailing_b/] 122 user = r 123 124 # Sub-paths 125 [/sub/path] 126 user = r 127 128 # Module usage 129 [module:/module_a] 130 user = r 131 [other:/module_b] 132 user = r 133 [/module_c] 134 user = r 135 [module:/module_d] 136 user = 137 [/module_d] 138 user = r 139 140 # Wildcards 141 [/wildcard] 142 * = r 143 144 # Groups 145 [/groups_a] 146 @group1 = r 147 [/groups_b] 148 @group2 = r 149 [/cyclic] 150 @cycle1 = r 151 152 # Precedence 153 [module:/precedence_a] 154 user = 155 [/precedence_a] 156 user = r 157 [/precedence_b] 158 user = r 159 [/precedence_b/sub] 160 user = 161 [/precedence_b/sub/test] 162 user = r 163 [/precedence_c] 164 user = 165 @group1 = r 166 [/precedence_d] 167 @group1 = r 168 user = 169 170 # Aliases 171 [/aliases_a] 172 &jekyll = r 173 [/aliases_b] 174 @alias2 = r 175 """) 176 self.env = EnvironmentStub(enable=[AuthzSourcePolicy]) 177 self.env.config.set('trac', 'authz_file', self.authz) 178 self.policy = AuthzSourcePolicy(self.env) 179 180 def tearDown(self): 181 self.env.reset_db() 182 os.remove(self.authz) 183 184 def assertPermission(self, result, user, reponame, path): 185 """Assert that `user` is granted access `result` to `path` within 186 the repository `reponame`. 187 """ 188 resource = Resource('source', path, 189 parent=Resource('repository', reponame)) 190 check = self.policy.check_permission('FILE_VIEW', user, resource, None) 191 self.assertEqual(result, check) 192 193 def test_default_permission(self): 194 # By default, no permission is granted 195 self.assertPermission(False, 'joe', '', '/not_defined') 196 self.assertPermission(False, 'jane', 'repo', '/not/defined/either') 197 198 def test_read_write(self): 199 # Allow 'r' and 'rw' entries, deny 'w' and empty entries 200 self.assertPermission(True, 'user', '', '/readonly') 201 self.assertPermission(True, 'user', '', '/readwrite') 202 self.assertPermission(False, 'user', '', '/writeonly') 203 self.assertPermission(False, 'user', '', '/empty') 204 205 def test_trailing_slashes(self): 206 # Combinations of trailing slashes in the file and in the path 207 self.assertPermission(True, 'user', '', '/trailing_a') 208 self.assertPermission(True, 'user', '', '/trailing_a/') 209 self.assertPermission(True, 'user', '', '/trailing_b') 210 self.assertPermission(True, 'user', '', '/trailing_b/') 211 212 def test_sub_path(self): 213 # Permissions are inherited from containing directories 214 self.assertPermission(True, 'user', '', '/sub/path') 215 self.assertPermission(True, 'user', '', '/sub/path/test') 216 self.assertPermission(True, 'user', '', '/sub/path/other/sub') 217 218 def test_module_usage(self): 219 # If a module name is specified, the rules are specific to the module 220 self.assertPermission(True, 'user', 'module', '/module_a') 221 self.assertPermission(False, 'user', 'module', '/module_b') 222 # If a module is specified, but the configuration contains a non-module 223 # path, the non-module path can still apply 224 self.assertPermission(True, 'user', 'module', '/module_c') 225 # The module-specific rule takes precedence 226 self.assertPermission(False, 'user', 'module', '/module_d') 227 228 def test_wildcard(self): 229 # The * wildcard matches all users 230 self.assertPermission(True, 'joe', '', '/wildcard') 231 self.assertPermission(True, 'jane', '', '/wildcard') 232 233 def test_groups(self): 234 # Groups are specified in a separate section and used with an @ prefix 235 self.assertPermission(True, 'user', '', '/groups_a') 236 # Groups can also be members of other groups 237 self.assertPermission(True, 'user', '', '/groups_b') 238 # Groups should not be defined cyclically, but they are still handled 239 # correctly to avoid infinite loops 240 self.assertPermission(True, 'user', '', '/cyclic') 241 242 def test_precedence(self): 243 # Module-specific sections take precedence over non-module sections 244 self.assertPermission(False, 'user', 'module', '/precedence_a') 245 # The most specific section applies 246 self.assertPermission(True, 'user', '', '/precedence_b/sub/test') 247 self.assertPermission(False, 'user', '', '/precedence_b/sub') 248 self.assertPermission(True, 'user', '', '/precedence_b') 249 # Within a section, the first matching rule applies 250 self.assertPermission(False, 'user', '', '/precedence_c') 251 self.assertPermission(True, 'user', '', '/precedence_d') 252 253 def test_aliases(self): 254 # Aliases are specified in a separate section and used with an & prefix 255 self.assertPermission(True, 'Mr Hyde', '', '/aliases_a') 256 # Aliases can also be used in groups 257 self.assertPermission(True, 'Mr Hyde', '', '/aliases_b') 258 210 259 211 260 def suite(): 212 try: 213 from doctest import DocTestSuite 214 return DocTestSuite(sys.modules[__name__]) 215 except ImportError: 216 print >> sys.stderr, "WARNING: DocTestSuite required to run these " \ 217 "tests" 218 return unittest.TestSuite() 261 suite = unittest.TestSuite() 262 suite.addTest(unittest.makeSuite(AuthzParserTestCase, 'test')) 263 suite.addTest(unittest.makeSuite(AuthzSourcePolicyTestCase, 'test')) 264 return suite 265 219 266 220 267 if __name__ == '__main__': -
trunk/trac/versioncontrol/tests/svn_fs.py
r8734 r9125 86 86 87 87 def setUp(self): 88 self.repos = SubversionRepository(REPOS_PATH, None, 88 self.repos = SubversionRepository(REPOS_PATH, 89 {'name': 'repo', 'id': 1}, 89 90 logger_factory('test')) 90 91 … … 508 509 509 510 def setUp(self): 510 self.repos = SubversionRepository(REPOS_PATH + u'/tête', None, 511 self.repos = SubversionRepository(REPOS_PATH + u'/tête', 512 {'name': 'repo', 'id': 1}, 511 513 logger_factory('test')) 512 514 … … 757 759 758 760 def setUp(self): 759 self.repos = SubversionRepository(REPOS_PATH + u'/tête/dir1', None, 761 self.repos = SubversionRepository(REPOS_PATH + u'/tête/dir1', 762 {'name': 'repo', 'id': 1}, 760 763 logger_factory('test')) 761 764 … … 777 780 778 781 def setUp(self): 779 self.repos = SubversionRepository(REPOS_PATH + '/tags/v1', None, 782 self.repos = SubversionRepository(REPOS_PATH + '/tags/v1', 783 {'name': 'repo', 'id': 1}, 780 784 logger_factory('test')) 781 785 … … 795 799 796 800 def setUp(self): 797 self.repos = SubversionRepository(REPOS_PATH + '/branches', None, 801 self.repos = SubversionRepository(REPOS_PATH + '/branches', 802 {'name': 'repo', 'id': 1}, 798 803 logger_factory('test')) 799 804 -
trunk/trac/versioncontrol/web_ui/browser.py
r9105 r9125 22 22 from genshi.builder import tag 23 23 24 from trac.config import ListOption, BoolOption, Option 24 from trac.config import ListOption, BoolOption, Option, _TRUE_VALUES 25 25 from trac.core import * 26 26 from trac.mimeview.api import Mimeview, is_binary, \ … … 29 29 from trac.resource import ResourceNotFound 30 30 from trac.util import embedded_numbers 31 from trac.util.compat import any 31 32 from trac.util.datefmt import http_date, utc 32 33 from trac.util.html import escape, Markup … … 36 37 from trac.web.chrome import add_ctxtnav, add_link, add_script, add_stylesheet, \ 37 38 prevnext_nav, INavigationContributor 38 from trac.wiki.api import IWikiSyntaxProvider 39 from trac.wiki.api import IWikiSyntaxProvider, IWikiMacroProvider, parse_args 39 40 from trac.wiki.formatter import format_to_html, format_to_oneliner 40 from trac.versioncontrol.api import NoSuchChangeset41 from trac.versioncontrol.api import RepositoryManager, NoSuchChangeset 41 42 from trac.versioncontrol.web_ui.util import * 42 43 … … 175 176 176 177 implements(INavigationContributor, IPermissionRequestor, IRequestHandler, 177 IWikiSyntaxProvider, IHTMLPreviewAnnotator) 178 IWikiSyntaxProvider, IHTMLPreviewAnnotator, 179 IWikiMacroProvider) 178 180 179 181 property_renderers = ExtensionPoint(IPropertyRenderer) … … 289 291 290 292 def get_navigation_items(self, req): 291 if 'BROWSER_VIEW' in req.perm: 293 rm = RepositoryManager(self.env) 294 if any(repos and repos.params.get('hidden') not in _TRUE_VALUES 295 and repos.can_view(req.perm) 296 for repos in rm.get_real_repositories()): 292 297 yield ('mainnav', 'browser', 293 298 tag.a(_('Browse Source'), href=req.href.browser())) … … 323 328 path = req.args.get('path', '/') 324 329 rev = req.args.get('rev', None) 325 order = req.args.get('order', None)326 desc = req.args. get('desc', None)330 order = req.args.get('order', 'name').lower() 331 desc = req.args.has_key('desc') 327 332 xhr = req.get_header('X-Requested-With') == 'XMLHttpRequest' 328 333 334 rm = RepositoryManager(self.env) 335 reponame, repos, path = rm.get_repository_by_path(path) 336 337 # Repository index 338 all_repositories, repoinfo = None, None 339 if not reponame and path == '/': 340 all_repositories = rm.get_all_repositories() 341 repoinfo = all_repositories.get(reponame) 342 if repos and (all_repositories[''].get('hidden') in _TRUE_VALUES 343 or not repos.can_view(req.perm)): 344 repos = None 345 346 if not repos and reponame: 347 raise ResourceNotFound(_("No repository '%(repo)s' found", 348 repo=reponame)) 349 350 if reponame and reponame != repos.reponame: # Redirect alias 351 qs = req.query_string 352 req.redirect(req.href.browser(repos.reponame or None, path) 353 + (qs and '?' + qs or '')) 354 reponame = repos and repos.reponame or None 355 329 356 # Find node for the requested path/rev 330 repos = self.env.get_repository(req.authname) 331 332 try: 333 if rev: 334 rev = repos.normalize_rev(rev) 335 # If `rev` is `None`, we'll try to reuse `None` consistently, 336 # as a special shortcut to the latest revision. 337 rev_or_latest = rev or repos.youngest_rev 338 node = get_existing_node(req, repos, path, rev_or_latest) 339 except NoSuchChangeset, e: 340 raise ResourceNotFound(e.message, _('Invalid Changeset Number')) 341 342 context = Context.from_request(req, 'source', path, rev_or_latest) 343 344 path_links = get_path_links(req.href, path, rev, order, desc) 357 context = Context.from_request(req) 358 node = None 359 if repos: 360 try: 361 if rev: 362 rev = repos.normalize_rev(rev) 363 # If `rev` is `None`, we'll try to reuse `None` consistently, 364 # as a special shortcut to the latest revision. 365 rev_or_latest = rev or repos.youngest_rev 366 node = get_existing_node(req, repos, path, rev_or_latest) 367 except NoSuchChangeset, e: 368 raise ResourceNotFound(e.message, 369 _('Invalid changeset number')) 370 371 context = context(repos.resource.child('source', path, 372 version=node.created_rev)) 373 374 # Prepare template data 375 path_links = get_path_links(req.href, reponame, path, rev, 376 order, desc) 377 378 repo_data = dir_data = file_data = None 379 if all_repositories: 380 repo_data = self._render_repository_index( 381 context, all_repositories, order, desc) 382 if node: 383 if node.isdir: 384 dir_data = self._render_dir(req, repos, node, rev, order, desc) 385 elif node.isfile: 386 file_data = self._render_file(req, context, repos, node, rev) 387 388 quickjump_data = properties_data = None 389 if node and not xhr: 390 properties_data = self.render_properties( 391 'browser', context, node.get_properties()) 392 quickjump_data = list(repos.get_quickjump_entries(rev)) 345 393 346 394 data = { 347 'context': context, 348 ' path': path, 'rev': node.rev, 'stickyrev': rev,349 ' created_path': node.created_path,350 'created_ rev': node.created_rev,351 ' properties': xhr or self.render_properties('browser', context,352 node.get_properties()),395 'context': context, 'reponame': reponame, 'repos': repos, 396 'repoinfo': repoinfo, 397 'path': path, 'rev': node and node.rev, 'stickyrev': rev, 398 'created_path': node and node.created_path, 399 'created_rev': node and node.created_rev, 400 'properties': properties_data, 353 401 'path_links': path_links, 354 ' dir': node.isdir and self._render_dir(req, repos, node, rev),355 ' file': node.isfile and self._render_file(req, context, repos,356 node, rev),357 ' quickjump_entries': xhr or list(repos.get_quickjump_entries(rev)),358 'wiki_format_messages':359 self.config['changeset'].getbool('wiki_format_messages')360 } 402 'order': order, 'desc': desc and 1 or None, 403 'repo': repo_data, 'dir': dir_data, 'file': file_data, 404 'quickjump_entries': quickjump_data, 405 'wiki_format_messages': \ 406 self.config['changeset'].getbool('wiki_format_messages'), 407 'xhr': xhr, 408 } 361 409 if xhr: # render and return the content only 362 data['xhr'] = True363 410 return 'dir_entries.html', data, None 364 411 412 if dir_data or repo_data: 413 add_script(req, 'common/js/expand_dir.js') 414 add_script(req, 'common/js/keyboard_nav.js') 415 365 416 # Links for contextual navigation 366 if node.isfile: 367 prev_rev = repos.previous_rev(rev=node.rev, 417 if node: 418 if node.isfile: 419 prev_rev = repos.previous_rev(rev=node.rev, 420 path=node.created_path) 421 if prev_rev: 422 href = req.href.browser(reponame, 423 node.created_path, rev=prev_rev) 424 add_link(req, 'prev', href, 425 _('Revision %(num)s', num=prev_rev)) 426 if rev is not None: 427 add_link(req, 'up', req.href.browser(reponame, 428 node.created_path)) 429 next_rev = repos.next_rev(rev=node.rev, 368 430 path=node.created_path) 369 if prev_rev: 370 href = req.href.browser(node.created_path, rev=prev_rev) 371 add_link(req, 'prev', href, 372 _('Revision %(num)s', num=prev_rev)) 373 if rev is not None: 374 add_link(req, 'up', req.href.browser(node.created_path)) 375 next_rev = repos.next_rev(rev=node.rev, 376 path=node.created_path) 377 if next_rev: 378 href = req.href.browser(node.created_path, rev=next_rev) 379 add_link(req, 'next', href, 380 _('Revision %(num)s', num=next_rev)) 381 prevnext_nav(req, _('Previous Revision'), _('Next Revision'), 382 _('Latest Revision')) 383 else: 384 if len(path_links) > 1: 385 add_link(req, 'up', path_links[-2]['href'], 386 _('Parent directory')) 387 add_ctxtnav(req, tag.a(_('Last Change'), 388 href=req.href.changeset(node.rev, node.created_path))) 389 390 if node.isfile: 391 if data['file']['annotate']: 392 add_ctxtnav(req, _('Normal'), 393 title=_('View file without annotations'), 394 href=req.href.browser(node.created_path, 395 rev=node.rev)) 431 if next_rev: 432 href = req.href.browser(reponame, node.created_path, 433 rev=next_rev) 434 add_link(req, 'next', href, 435 _('Revision %(num)s', num=next_rev)) 436 prevnext_nav(req, _('Previous Revision'), _('Next Revision'), 437 _('Latest Revision')) 396 438 else: 397 add_ctxtnav(req, _('Annotate'), 398 title=_('Annotate each line with the last ' 399 'changed revision ' 400 '(this can be time consuming...)'), 401 href=req.href.browser(node.created_path, 402 rev=node.rev, 403 annotate='blame')) 404 add_ctxtnav(req, _('Revision Log'), 405 href=req.href.log(path, rev=rev)) 439 if path != '/': 440 add_link(req, 'up', path_links[-2]['href'], 441 _('Parent directory')) 442 add_ctxtnav(req, tag.a(_('Last Change'), 443 href=req.href.changeset(node.rev, reponame, 444 node.created_path))) 445 if node.isfile: 446 if data['file']['annotate']: 447 add_ctxtnav(req, _('Normal'), 448 title=_('View file without annotations'), 449 href=req.href.browser(reponame, 450 node.created_path, 451 rev=node.rev)) 452 else: 453 add_ctxtnav(req, _('Annotate'), 454 title=_('Annotate each line with the last ' 455 'changed revision ' 456 '(this can be time consuming...)'), 457 href=req.href.browser(reponame, 458 node.created_path, 459 rev=node.rev, 460 annotate='blame')) 461 add_ctxtnav(req, _('Revision Log'), 462 href=req.href.log(reponame, path, rev=rev)) 463 path_url = repos.get_path_url(path, rev) 464 if path_url: 465 if path_url.startswith('//'): 466 path_url = req.scheme + ':' + path_url 467 add_ctxtnav(req, _('Repository URL'), href=path_url) 406 468 407 469 add_stylesheet(req, 'common/css/browser.css') … … 410 472 # Internal methods 411 473 412 def _render_dir(self, req, repos, node, rev=None): 413 req.perm.require('BROWSER_VIEW') 474 def _render_repository_index(self, context, all_repositories, order, desc): 475 # Color scale for the age column 476 timerange = custom_colorizer = None 477 if self.color_scale: 478 custom_colorizer = self.get_custom_colorizer() 479 480 rm = RepositoryManager(self.env) 481 repositories = [] 482 for reponame, repoinfo in all_repositories.items(): 483 if not reponame or repoinfo.get('hidden') in _TRUE_VALUES: 484 continue 485 try: 486 repos = rm.get_repository(reponame) 487 if repos: 488 if not repos.can_view(context.perm): 489 continue 490 youngest = repos.get_changeset(repos.youngest_rev) 491 if self.color_scale and youngest: 492 if not timerange: 493 timerange = TimeRange(youngest.date) 494 else: 495 timerange.insert(youngest.date) 496 entry = (reponame, repoinfo, repos, youngest, None) 497 else: 498 entry = (reponame, repoinfo, None, None, "XXX") 499 except TracError, err: 500 entry = (reponame, repoinfo, None, None, 501 exception_to_unicode(err)) 502 repositories.append(entry) 503 504 # Ordering of repositories 505 if order == 'date': 506 def repo_order((reponame, repoinfo, repos, youngest, err)): 507 return youngest and youngest.date 508 else: 509 def repo_order((reponame, repoinfo, repos, youngest, err)): 510 return embedded_numbers(reponame.lower()) 511 512 repositories = sorted(repositories, key=repo_order, reverse=desc) 513 514 return {'repositories' : repositories, 515 'timerange': timerange, 'colorize_age': custom_colorizer} 516 517 def _render_dir(self, req, repos, node, rev, order, desc): 518 req.perm(node.resource).require('BROWSER_VIEW') 414 519 415 520 # Entries metadata … … 420 525 setattr(self, f, getattr(node, f)) 421 526 422 entries = [entry(n) for n in node.get_entries()] 527 entries = [entry(n) for n in node.get_entries() 528 if n.can_view(req.perm)] 423 529 changes = get_changes(repos, [i.rev for i in entries]) 424 530 … … 442 548 443 549 # Ordering of entries 444 order = req.args.get('order', 'name').lower()445 desc = req.args.has_key('desc')446 447 550 if order == 'date': 448 551 def file_order(a): … … 466 569 if node.path and patterns and \ 467 570 filter(None, [fnmatchcase(node.path, p) for p in patterns]): 468 zip_href = req.href.changeset(rev or repos.youngest_rev, node.path, 469 old=rev, old_path='/', format='zip') 571 zip_href = req.href.changeset(rev or repos.youngest_rev, 572 repos.reponame or None, node.path, 573 old=rev, 574 old_path=repos.reponame or '/', 575 format='zip') 470 576 add_link(req, 'alternate', zip_href, _('Zip Archive'), 471 577 'application/zip', 'zip') 472 578 473 add_script(req, 'common/js/expand_dir.js') 474 add_script(req, 'common/js/keyboard_nav.js') 475 476 return {'order': order, 'desc': desc and 1 or None, 477 'entries': entries, 'changes': changes, 579 return {'entries': entries, 'changes': changes, 478 580 'timerange': timerange, 'colorize_age': custom_colorizer, 479 581 'range_max_secs': (timerange and … … 484 586 485 587 def _render_file(self, req, context, repos, node, rev=None): 486 req.perm( context.resource).require('FILE_VIEW')588 req.perm(node.resource).require('FILE_VIEW') 487 589 488 590 mimeview = Mimeview(self.env) … … 523 625 # add ''Plain Text'' alternate link if needed 524 626 if not is_binary(chunk) and mime_type != 'text/plain': 525 plain_href = req.href.browser(node.path, rev=rev, format='txt') 627 plain_href = req.href.browser(repos.reponame or None, 628 node.path, rev=rev, format='txt') 526 629 add_link(req, 'alternate', plain_href, _('Plain Text'), 527 630 'text/plain') 528 631 529 632 # add ''Original Format'' alternate link (always) 530 raw_href = req.href.export(rev or repos.youngest_rev, node.path) 633 raw_href = req.href.export(rev or repos.youngest_rev, 634 repos.reponame or None, node.path) 531 635 add_link(req, 'alternate', raw_href, _('Original Format'), 532 636 mime_type) … … 621 725 path, rev = export.split('@', 1) 622 726 else: 623 rev, path = self.env.get_repository().youngest_rev, export727 rev, path = '', export 624 728 return tag.a(label, class_='export', 625 729 href=formatter.href.export(rev, path) + fragment) … … 652 756 blame_annotator.annotate(row, lineno) 653 757 758 # IWikiMacroProvider methods 759 760 def get_macros(self): 761 yield "RepositoryIndex" 762 763 def get_macro_description(self, name): 764 return """ 765 Display the list of available repositories. 766 767 Can be given a ''format'' argument (defaults to ''compact'') 768 - ''compact'' will produce a comma-separated list of 769 repository prefix names 770 - ''list'' will produce a description list of 771 repository prefix names 772 - ''table'' will produce a table view, similar to the 773 one visible in the ''Browse View'' page 774 775 Can be given a ''glob'' argument, which will do a glob-style 776 filtering on the repository names (defaults to '*') 777 778 (since 0.12) 779 """ 780 781 def expand_macro(self, formatter, name, content): 782 args, kwargs = parse_args(content) 783 format = kwargs.get('format', 'compact') 784 glob = kwargs.get('glob', '*') 785 order = kwargs.get('order') 786 desc = kwargs.get('desc', 0) 787 788 rm = RepositoryManager(self.env) 789 all_repos = dict(rdata for rdata in rm.get_all_repositories().items() 790 if fnmatchcase(rdata[0], glob)) 791 792 if format == 'table': 793 repo = self._render_repository_index(formatter.context, all_repos, 794 order, desc) 795 796 add_stylesheet(formatter.req, 'common/css/browser.css') 797 data = {'repo': repo, 'desc': desc and 1 or None, 798 'reponame': None, 'path': '/', 'stickyrev': None} 799 from trac.web.chrome import Chrome 800 return Chrome(self.env).render_template( 801 formatter.req, 'repository_index.html', data, None, 802 fragment=True) 803 804 def repolink(reponame, repos): 805 label = reponame or _('(default)') 806 return Markup(tag.a(label, 807 title=_('View repository %(repo)s', repo=label), 808 href=formatter.href.browser(repos.reponame or None))) 809 810 all_repos = dict((reponame, rm.get_repository(reponame)) 811 for reponame in all_repos) 812 all_repos = sorted((reponame, repos) for reponame, repos in all_repos 813 if repos 814 and repos.params.get('hidden') not in _TRUE_VALUE 815 and repos.can_view(formatter.perm)) 816 817 if format == 'list': 818 return tag.dl([ 819 tag(tag.dt(repolink(reponame, repos)), 820 tag.dd(repos.params.get('description'))) 821 for reponame, repos in all_repos]) 822 else: # compact 823 return Markup(', ').join([repolink(reponame, repos) 824 for reponame, repos in all_repos]) 825 826 654 827 655 828 class BlameAnnotator(object): … … 657 830 def __init__(self, env, context): 658 831 self.env = env 659 # `context`'s resource is ('source', path, version=rev)660 832 self.context = context 661 self.resource = context.resource 662 self.repos = env.get_repository() 833 self.repos = env.get_repository(context.resource.parent.id) 834 self.path = context.resource.id 835 self.rev = context.resource.version 663 836 # maintain state 664 837 self.prev_chgset = None … … 670 843 671 844 def reset(self): 672 rev = self.re source.version673 node = self.repos.get_node(self. resource.id, rev)845 rev = self.rev 846 node = self.repos.get_node(self.path, rev) 674 847 # FIXME: get_annotations() should be in the Resource API 675 848 # -- get revision numbers for each line … … 713 886 # -- compute anchor and style once per revision 714 887 if rev not in self.chgset_data: 715 chgset_href = self.context.href.changeset(rev, path) 888 chgset_href = \ 889 self.context.href.changeset(rev, self.repos.reponame or None, 890 path) 716 891 short_author = chgset.author.split(' ', 1)[0] 717 892 title = shorten_line('%s: %s' % (short_author, chgset.message)) … … 727 902 self.prev_style = style 728 903 # optimize away the path if there's no copy/rename info 729 if not path or path == self. resource.id:904 if not path or path == self.path: 730 905 path = '' 731 906 # -- produce blame column, eventually with an anchor -
trunk/trac/versioncontrol/web_ui/changeset.py
r8899 r9125 28 28 from genshi.builder import tag 29 29 30 from trac.config import Option, BoolOption, IntOption 30 from trac.config import Option, BoolOption, IntOption, _TRUE_VALUES 31 31 from trac.core import * 32 32 from trac.mimeview import Context, Mimeview … … 35 35 from trac.search import ISearchSource, search_to_sql, shorten_result 36 36 from trac.timeline.api import ITimelineEventProvider 37 from trac.util import embedded_numbers, content_disposition38 from trac.util.compat import any 37 from trac.util import content_disposition, embedded_numbers, pathjoin 38 from trac.util.compat import any, set 39 39 from trac.util.datefmt import pretty_timedelta, utc 40 from trac.util.text import exception_to_unicode, unicode_urlencode, \ 41 shorten_line, CRLF 42 from trac.util.translation import _ 43 from trac.versioncontrol import Changeset, Node, NoSuchChangeset 40 from trac.util.text import exception_to_unicode, to_unicode, \ 41 unicode_urlencode, shorten_line, CRLF 42 from trac.util.translation import _, ngettext 43 from trac.versioncontrol.api import RepositoryManager, Changeset, Node, \ 44 NoSuchChangeset 44 45 from trac.versioncontrol.diff import get_diff_options, diff_blocks, \ 45 46 unified_diff … … 210 211 req.perm.require('CHANGESET_VIEW') 211 212 212 repos = self.env.get_repository(req.authname)213 214 213 # -- retrieve arguments 215 new_path = req.args.get('new_path')214 full_new_path = new_path = req.args.get('new_path') 216 215 new = req.args.get('new') 217 old_path = req.args.get('old_path')216 full_old_path = old_path = req.args.get('old_path') 218 217 old = req.args.get('old') 218 reponame = req.args.get('reponame') 219 219 220 220 xhr = req.get_header('X-Requested-With') == 'XMLHttpRequest' … … 227 227 new, new_path = new.split('@', 1) 228 228 229 rm = RepositoryManager(self.env) 230 if reponame: 231 repos = rm.get_repository(reponame) 232 else: 233 reponame, repos, new_path = rm.get_repository_by_path(new_path) 234 235 if old_path: 236 old_reponame, old_repos, old_path = \ 237 rm.get_repository_by_path(old_path) 238 if old_repos != repos: 239 raise TracError(_("Can't compare across different " 240 "repositories: %(old)s vs. %(new)s", 241 old=old_reponame, new=reponame)) 242 243 if not repos: 244 if reponame or (new_path and new_path != '/'): 245 raise TracError(_("No repository found for '%(reponame)s'", 246 reponame=reponame or new_path.strip('/'))) 247 else: 248 raise TracError(_("No repository specified and no default " 249 "repository configured.")) 250 229 251 # -- normalize and check for special case 230 252 try: 231 253 new_path = repos.normalize_path(new_path) 232 254 new = repos.normalize_rev(new) 233 234 repos.authz.assert_permission_for_changeset(new) 235 255 full_new_path = '/' + pathjoin(repos.reponame, new_path) 236 256 old_path = repos.normalize_path(old_path or new_path) 237 257 old = repos.normalize_rev(old or new) 258 full_old_path = '/' + pathjoin(repos.reponame, old_path) 238 259 except NoSuchChangeset, e: 239 260 raise ResourceNotFound(e.message, _('Invalid Changeset Number')) … … 251 272 restricted = old_path == new_path # (same path or not) 252 273 253 # -- redirect if changing the diff options 254 if req.args.has_key('update'): 274 # -- redirect if changing the diff options or alias requested 275 if req.args.has_key('update') or reponame != repos.reponame: 276 reponame = repos.reponame or None 255 277 if chgset: 256 278 if restricted: 257 req.redirect(req.href.changeset(new, new_path))279 req.redirect(req.href.changeset(new, reponame, new_path)) 258 280 else: 259 req.redirect(req.href.changeset(new)) 260 else: 261 req.redirect(req.href.changeset(new, new_path, old=old, 262 old_path=old_path)) 281 req.redirect(req.href.changeset(new, reponame)) 282 else: 283 req.redirect(req.href.changeset(new, reponame, 284 new_path, old=old, 285 old_path=full_old_path)) 263 286 264 287 # -- preparing the data … … 280 303 data = {'old_path': old_path, 'old_rev': old, 281 304 'new_path': new_path, 'new_rev': new} 282 data['diff'] = diff_data 283 data['wiki_format_messages'] = self.wiki_format_messages 305 data.update({'repos': repos, 'reponame': repos.reponame or None, 306 'diff': diff_data, 307 'wiki_format_messages': self.wiki_format_messages}) 284 308 285 309 if chgset: 286 req.perm('changeset', new).require('CHANGESET_VIEW')287 310 chgset = repos.get_changeset(new) 311 req.perm(chgset.resource).require('CHANGESET_VIEW') 288 312 289 313 # TODO: find a cheaper way to reimplement r2636 … … 297 321 298 322 if format in ['diff', 'zip']: 299 req.perm.require('FILE_VIEW')300 323 # choosing an appropriate filename 301 324 rpath = new_path.replace('/','_') … … 325 348 diff_params = 'new=%s' % new 326 349 else: 327 diff_params = unicode_urlencode({'new_path': new_path, 328 'new': new, 329 'old_path': old_path, 330 'old': old}) 331 add_link(req, 'alternate', '?format=diff&'+diff_params, 350 diff_params = unicode_urlencode({'new_path': new_path, 'new': new, 351 'old_path': old_path, 'old': old}) 352 add_link(req, 'alternate', '?format=diff&' + diff_params, 332 353 _('Unified Diff'), 'text/plain', 'diff') 333 add_link(req, 'alternate', '?format=zip&' +diff_params, _('Zip Archive'),334 'application/zip', 'zip')354 add_link(req, 'alternate', '?format=zip&' + diff_params, 355 _('Zip Archive'), 'application/zip', 'zip') 335 356 add_script(req, 'common/js/diff.js') 336 357 add_stylesheet(req, 'common/css/changeset.css') … … 343 364 prevnext_nav(req, _('Previous Changeset'), _('Next Changeset')) 344 365 else: 345 rev_href = req.href.changeset(old, old_path, old=new,346 old _path=new_path)366 rev_href = req.href.changeset(old, full_old_path, 367 old=new, old_path=full_new_path) 347 368 add_ctxtnav(req, _('Reverse Diff'), href=rev_href) 348 369 … … 355 376 data['restricted'] = restricted 356 377 browser = BrowserModule(self.env) 378 reponame = repos.reponame or None 357 379 358 380 if chgset: # Changeset Mode (possibly restricted on a path) … … 385 407 386 408 # Support for revision properties (#2545) 387 context = Context.from_request(req, 'changeset', chgset.rev) 409 context = Context.from_request(req, 'changeset', chgset.rev, 410 parent=repos.resource) 411 data['context'] = context 388 412 revprops = chgset.get_properties() 389 413 data['properties'] = browser.render_properties('revprop', context, … … 396 420 prev_path, prev_rev = prev[:2] 397 421 if prev_rev: 398 prev_href = req.href.changeset(prev_rev, prev_path) 422 prev_href = req.href.changeset(prev_rev, reponame, 423 prev_path) 399 424 else: 400 425 prev_path = prev_rev = None 401 426 else: 402 add_link(req, 'first', req.href.changeset(oldest_rev), 427 add_link(req, 'first', 428 req.href.changeset(oldest_rev, reponame), 403 429 _('Changeset %(id)s', id=oldest_rev)) 404 430 prev_path = data['old_path'] 405 431 prev_rev = repos.previous_rev(chgset.rev) 406 432 if prev_rev: 407 prev_href = req.href.changeset(prev_rev )433 prev_href = req.href.changeset(prev_rev, reponame) 408 434 if prev_rev: 409 435 add_link(req, 'prev', prev_href, _changeset_title(prev_rev)) … … 414 440 if next_rev: 415 441 if repos.has_node(path, next_rev): 416 next_href = req.href.changeset(next_rev, path) 442 next_href = req.href.changeset(next_rev, reponame, 443 path) 417 444 else: # must be a 'D'elete or 'R'ename, show full cset 418 next_href = req.href.changeset(next_rev )445 next_href = req.href.changeset(next_rev, reponame) 419 446 else: 420 add_link(req, 'last', req.href.changeset(youngest_rev), 447 add_link(req, 'last', 448 req.href.changeset(youngest_rev, reponame), 421 449 _('Changeset %(id)s', id=youngest_rev)) 422 450 next_rev = repos.next_rev(chgset.rev) 423 451 if next_rev: 424 next_href = req.href.changeset(next_rev )452 next_href = req.href.changeset(next_rev, reponame) 425 453 if next_rev: 426 454 add_link(req, 'next', next_href, _changeset_title(next_rev)) … … 445 473 'rev': node.rev, 446 474 'shortrev': repos.short_rev(node.rev), 447 'href': req.href.browser(node.created_path, 475 'href': req.href.browser(reponame, 476 node.created_path, 448 477 rev=node.created_rev, 449 478 annotate=annotated and 'blame' or \ … … 459 488 460 489 def _prop_changes(old_node, new_node): 461 old_source = Resource('source', old_node.created_path, 462 version=old_node.created_rev) 463 new_source = Resource('source', new_node.created_path, 464 version=new_node.created_rev) 465 old_props = new_props = [] 466 if 'FILE_VIEW' in req.perm(old_source): 467 old_props = old_node.get_properties() 468 if 'FILE_VIEW' in req.perm(new_source): 469 new_props = new_node.get_properties() 470 old_ctx = Context.from_request(req, old_source) 471 new_ctx = Context.from_request(req, new_source) 490 old_props = old_node.get_properties() 491 new_props = new_node.get_properties() 492 old_ctx = Context.from_request(req, old_node.resource) 493 new_ctx = Context.from_request(req, new_node.resource) 472 494 changed_properties = [] 473 495 if old_props != new_props: … … 540 562 return [] 541 563 542 if 'FILE_VIEW' in req.perm: 543 diff_bytes = diff_files = 0 544 if self.max_diff_bytes or self.max_diff_files: 545 for old_node, new_node, kind, change in get_changes(): 546 if change in Changeset.DIFF_CHANGES and kind == Node.FILE: 547 diff_files += 1 548 diff_bytes += _estimate_changes(old_node, new_node) 549 show_diffs = (not self.max_diff_files or \ 550 diff_files <= self.max_diff_files) and \ 551 (not self.max_diff_bytes or \ 552 diff_bytes <= self.max_diff_bytes or \ 553 diff_files == 1) 554 else: 555 show_diffs = False 564 diff_bytes = diff_files = 0 565 if self.max_diff_bytes or self.max_diff_files: 566 for old_node, new_node, kind, change in get_changes(): 567 if change in Changeset.DIFF_CHANGES and kind == Node.FILE \ 568 and old_node.can_view(req.perm) \ 569 and new_node.can_view(req.perm): 570 diff_files += 1 571 diff_bytes += _estimate_changes(old_node, new_node) 572 show_diffs = (not self.max_diff_files or \ 573 0 < diff_files <= self.max_diff_files) and \ 574 (not self.max_diff_bytes or \ 575 diff_bytes <= self.max_diff_bytes or \ 576 diff_files == 1) 556 577 557 578 # XHR is used for blame support: display the changeset view without … … 569 590 props = [] 570 591 diffs = [] 592 show_old = old_node and old_node.can_view(req.perm) 593 show_new = new_node and new_node.can_view(req.perm) 571 594 show_entry = change != Changeset.EDIT 572 595 show_diff = show_diffs or (new_node and new_node.path == annotated) 573 596 574 if change in Changeset.DIFF_CHANGES and 'FILE_VIEW' in req.perm:597 if change in Changeset.DIFF_CHANGES and show_old and show_new: 575 598 assert old_node and new_node 576 599 props = _prop_changes(old_node, new_node) … … 584 607 # elif None (means: manually compare to (previous)) 585 608 show_entry = True 586 if show_entry or not show_diff:609 if (show_old or show_new) and (show_entry or not show_diff): 587 610 info = {'change': change, 588 611 'old': old_node and node_info(old_node, annotated), … … 595 618 if change in Changeset.DIFF_CHANGES: 596 619 if chgset: 597 href = req.href.changeset(new_node.rev, new_node.path) 620 href = req.href.changeset(new_node.rev, reponame, 621 new_node.path) 598 622 title = _('Show the changeset %(id)s restricted to ' 599 623 '%(path)s', id=new_node.rev, … … 601 625 else: 602 626 href = req.href.changeset( 603 new_node.created_rev, new_node.created_path, 627 new_node.created_rev, reponame, 628 new_node.created_path, 604 629 old=old_node.created_rev, 605 old_path=old_node.created_path) 630 old_path=pathjoin(repos.reponame, 631 old_node.created_path)) 606 632 title = _('Show the %(range)s differences restricted ' 607 633 'to %(path)s', … … 644 670 645 671 for old_node, new_node, kind, change in repos.get_changes( 646 new_path=data['new_path'], new_rev=data['new_rev'],647 old_path=data['old_path'], old_rev=data['old_rev']):672 new_path=data['new_path'], new_rev=data['new_rev'], 673 old_path=data['old_path'], old_rev=data['old_rev']): 648 674 # TODO: Property changes 649 675 … … 654 680 new_content = old_content = '' 655 681 new_node_info = old_node_info = ('','') 656 mimeview = Mimeview(self.env)657 682 658 683 if old_node: 684 if not old_node.can_view(req.perm): 685 continue 659 686 if mimeview.is_binary(old_node.content_type, old_node.path): 660 687 continue … … 666 693 old_node.content_type) 667 694 if new_node: 695 if not new_node.can_view(req.perm): 696 continue 668 697 if mimeview.is_binary(new_node.content_type, new_node.path): 669 698 continue … … 678 707 old_node_path = repos.normalize_path(old_node.path) 679 708 diff_old_path = repos.normalize_path(data['old_path']) 680 new_path = p osixpath.join(data['new_path'],681 old_node_path[len(diff_old_path)+1:])709 new_path = pathjoin(data['new_path'], 710 old_node_path[len(diff_old_path) + 1:]) 682 711 683 712 if old_content != new_content: … … 722 751 new_path=data['new_path'], new_rev=data['new_rev'], 723 752 old_path=data['old_path'], old_rev=data['old_rev']): 724 if kind == Node.FILE and change != Changeset.DELETE :725 assert new_node753 if kind == Node.FILE and change != Changeset.DELETE \ 754 and new_node.can_view(req.perm): 726 755 zipinfo = ZipInfo() 727 756 zipinfo.filename = new_node.path.strip('/').encode('utf-8') … … 802 831 def get_timeline_filters(self, req): 803 832 if 'CHANGESET_VIEW' in req.perm: 804 yield ('changeset', _('Repository checkins')) 833 # non-'hidden' repositories will be listed as additional 834 # repository filters. 835 # '(default)' will be shown for the default repository, 836 # unless it has a visible alias, or when it is itself an 837 # alias to a visible repository, or when it would be the 838 # only repository filter. 839 filters = [] 840 rm = RepositoryManager(self.env) 841 repositories = rm.get_real_repositories() 842 if len(repositories) > 1: 843 filters = [ 844 ('repo-' + repos.reponame, 845 u"\xa0\xa0-\xa0" + (repos.reponame or _('(default)'))) 846 for repos in repositories 847 if repos.params.get('hidden') not in _TRUE_VALUES 848 and repos.can_view(req.perm)] 849 filters.sort() 850 add_script(req, 'common/js/timeline_multirepos.js') 851 changeset_label = _('Changesets in all repositories') 852 else: 853 changeset_label = _('Repository changesets') 854 filters.insert(0, ('changeset', changeset_label)) 855 return filters 805 856 806 857 def get_timeline_events(self, req, start, stop, filters): 807 if 'changeset' in filters: 858 all_repos = 'changeset' in filters 859 repo_filters = set(f for f in filters if f.startswith('repo-')) 860 if all_repos or repo_filters: 808 861 show_files = self.timeline_show_files 809 862 show_location = show_files == 'location' … … 815 868 show_files = 0 # disabled 816 869 817 repos = self.env.get_repository(req.authname)818 819 870 if self.timeline_collapse: 820 871 collapse_changesets = lambda c: (c.author, c.message) … … 822 873 collapse_changesets = lambda c: c.rev 823 874 824 for _, changesets in groupby(repos.get_changesets(start, stop), 825 key=collapse_changesets): 826 permitted_changesets = [] 827 for chgset in changesets: 828 if 'CHANGESET_VIEW' in req.perm('changeset', chgset.rev): 829 permitted_changesets.append(chgset) 830 if permitted_changesets: 831 chgset = permitted_changesets[-1] 832 yield ('changeset', chgset.date, chgset.author, 833 (permitted_changesets, chgset.message or '', 834 show_location, show_files)) 875 uids_seen = {} 876 def generate_changesets(repos): 877 for _, changesets in groupby(repos.get_changesets(start, stop), 878 key=collapse_changesets): 879 viewable_changesets = [] 880 for cset in changesets: 881 cset_resource = Resource('changeset', cset.rev, 882 parent=repos.resource) 883 if cset.can_view(req.perm): 884 repos_for_uid = [repos.reponame] 885 uid = repos.get_changeset_uid(cset.rev) 886 if uid: 887 # uid can be seen in multiple repositories 888 if uid in uids_seen: 889 uids_seen[uid].append(repos.reponame) 890 continue # already viewable, simply append 891 uids_seen[uid] = repos_for_uid 892 viewable_changesets.append((cset, cset_resource, 893 repos_for_uid)) 894 if viewable_changesets: 895 cset = viewable_changesets[-1][0] 896 yield ('changeset', cset.date, cset.author, 897 (viewable_changesets, 898 show_location, show_files)) 899 900 rm = RepositoryManager(self.env) 901 for repos in sorted(rm.get_real_repositories(), 902 key=lambda repos: repos.reponame): 903 if all_repos or ('repo-' + repos.reponame) in repo_filters: 904 try: 905 for event in generate_changesets(repos): 906 yield event 907 except TracError, e: 908 self.log.error("Timeline event provider for repository" 909 " '%s' failed: %r", 910 repos.reponame, exception_to_unicode(e)) 835 911 836 912 def render_timeline_event(self, context, field, event): 837 changesets, message, show_location, show_files = event[3] 838 rev_b, rev_a = changesets[0].rev, changesets[-1].rev 839 913 changesets, show_location, show_files = event[3] 914 cset, cset_resource, repos_for_uid = changesets[0] 915 message = cset.message or '' 916 reponame = cset_resource.parent.id 917 rev_b, rev_a = cset.rev, cset.rev 918 840 919 if field == 'url': 841 920 if rev_a == rev_b: 842 return context.href.changeset(rev_a) 843 else: 844 return context.href.log(rev=rev_b, stop_rev=rev_a) 921 return context.href.changeset(rev_a, reponame or None) 922 else: 923 return context.href.log(reponame or None, rev=rev_b, 924 stop_rev=rev_a) 845 925 846 926 elif field == 'description': … … 858 938 if show_location: 859 939 filestats = self._prepare_filestats() 860 for c in changesets:940 for c, r, repos_for_c in changesets: 861 941 for chg in c.get_changes(): 942 resource = c.resource.parent.child('source', 943 chg[0] or '/', r.id) 944 if not 'FILE_VIEW' in context.perm(resource): 945 continue 862 946 filestats[chg[2]] += 1 863 947 files.append(chg[0]) … … 874 958 markup, class_="changes") 875 959 elif show_files: 876 for c in changesets:960 for c, r, repos_for_c in changesets: 877 961 for chg in c.get_changes(): 962 resource = c.resource.parent.child('source', 963 chg[0] or '/', r.id) 964 if not 'FILE_VIEW' in context.perm(resource): 965 continue 878 966 if show_files > 0 and len(files) > show_files: 879 967 break … … 884 972 markup = tag(tag.ul(files, class_="changes"), markup) 885 973 if message: 886 markup += format_to(self.env, None, context, message) 974 markup += format_to(self.env, None, context(cset_resource), 975 message) 887 976 return markup 888 977 889 if rev_a == rev_b: 890 title = tag('Changeset ', tag.em('[%s]' % rev_a)) 978 single = rev_a == rev_b 979 if not repos_for_uid[0]: 980 repos_for_uid[0] = _('(default)') 981 if reponame or len(repos_for_uid) > 1: 982 title = ngettext('Changeset in %(repo)s ', 983 'Changesets in %(repo)s ', 984 single and 1 or 2, repo=', '.join(repos_for_uid)) 891 985 else: 892 title = tag('Changesets ', tag.em('[', rev_a, '-', rev_b, ']')) 893 986 title = ngettext('Changeset ', 'Changesets ', single and 1 or 2) 987 if single: 988 title = tag(title, tag.em('[%s]' % rev_a)) 989 else: 990 title = tag(title, tag.em('[%s-%s]' % (rev_a, rev_b))) 894 991 if field == 'title': 895 992 return title … … 925 1022 if intertrac: 926 1023 return intertrac 1024 1025 # identifying repository 1026 rm = RepositoryManager(self.env) 927 1027 chgset, params, fragment = formatter.split_link(chgset) 928 1028 sep = chgset.find('/') … … 930 1030 rev, path = chgset[:sep], chgset[sep:] 931 1031 else: 932 rev, path = chgset, None 933 if 'CHANGESET_VIEW' in formatter.perm('changeset', rev): 1032 rev, path = chgset, '/' 1033 reponame = rm.get_default_repository(formatter.context) 1034 if reponame is not None: 1035 repos = rm.get_repository(reponame) 1036 else: 1037 reponame, repos, path = rm.get_repository_by_path(path) 1038 if path == '/': 1039 path = None 1040 1041 # rendering changeset link 1042 if repos: 934 1043 try: 935 changeset = self.env.get_repository().get_changeset(rev) 936 return tag.a(label, class_="changeset", 937 title=shorten_line(changeset.message), 938 href=(formatter.href.changeset(rev, path) + 939 params + fragment)) 1044 changeset = repos.get_changeset(rev) 1045 if changeset.can_view(formatter.perm): 1046 href = formatter.href.changeset(rev, 1047 repos.reponame or None, 1048 path) 1049 return tag.a(label, class_="changeset", 1050 title=shorten_line(changeset.message), 1051 href=href + params + fragment) 1052 errmsg = _("No permission to view changset %(rev)s " 1053 "on %(repos)s", rev=rev, 1054 repos=reponame or _('(default)')) 940 1055 except TracError, e: 941 return tag.a(label, class_="missing changeset", 942 title=unicode(e)) 943 return tag.a(label, class_="missing changeset") 1056 errmsg = to_unicode(e) 1057 elif reponame: 1058 errmsg = _("Repository %(repos)s not found", repos=reponame) 1059 else: 1060 errmsg = _("No default repository defined") 1061 return tag.a(label, class_="missing changeset", title=errmsg) 944 1062 945 1063 def _format_diff_link(self, formatter, ns, target, label): … … 973 1091 return tag.a(label, class_="changeset", title=title, href=href) 974 1092 975 # ISearchSource methods 1093 # ISearchSource methods 1094 1095 ### FIXME: move this specific implementation into cache.py 976 1096 977 1097 def get_search_filters(self, req): … … 982 1102 if not 'changeset' in filters: 983 1103 return 984 repos = self.env.get_repository(req.authname) 1104 rm = RepositoryManager(self.env) 1105 repositories = dict((repos.params['id'], repos) 1106 for repos in rm.get_real_repositories()) 985 1107 db = self.env.get_db_cnx() 986 1108 sql, args = search_to_sql(db, ['rev', 'message', 'author'], terms) 987 1109 cursor = db.cursor() 988 cursor.execute("SELECT re v,time,author,message "1110 cursor.execute("SELECT repos,rev,time,author,message " 989 1111 "FROM revision WHERE " + sql, args) 990 for rev, ts, author, log in cursor: 991 if not repos.authz.has_permission_for_changeset(rev): 992 continue 993 yield (req.href.changeset(rev), 994 '[%s]: %s' % (rev, shorten_line(log)), 995 datetime.fromtimestamp(ts, utc), author, 996 shorten_result(log, terms)) 1112 for id, rev, ts, author, log in cursor: 1113 repos = repositories.get(id) 1114 if not repos: 1115 continue # revisions for a no longer active repository 1116 cset = repos.resource.child('changeset', rev) 1117 if 'CHANGESET_VIEW' in req.perm(cset): 1118 yield (req.href.changeset(rev, repos.reponame or None), 1119 '[%s]: %s' % (rev, shorten_line(log)), 1120 datetime.fromtimestamp(ts, utc), author, 1121 shorten_result(log, terms)) 997 1122 998 1123 … … 1007 1132 1008 1133 def process_request(self, req): 1009 r epos = self.env.get_repository(req.authname)1134 rm = RepositoryManager(self.env) 1010 1135 1011 1136 if req.get_header('X-Requested-With') == 'XMLHttpRequest': 1012 1137 dirname, prefix = posixpath.split(req.args.get('q')) 1013 1138 prefix = prefix.lower() 1014 node = repos.get_node(dirname)1015 1139 reponame, repos, path = rm.get_repository_by_path(dirname) 1140 # an entry is a (isdir, name, path) tuple 1016 1141 def kind_order(entry): 1017 def name_order(entry): 1018 return embedded_numbers(entry.name) 1019 return entry.isfile, name_order(entry) 1142 return (not entry[0], embedded_numbers(entry[1])) 1143 1144 if repos: 1145 entries = [(e.isdir, e.name, 1146 '/' + pathjoin(repos.reponame, e.path)) 1147 for e in repos.get_node(path).get_entries() 1148 if e.can_view(req.perm)] 1149 if not reponame: 1150 entries.extend((True, repos.reponame, '/' + repos.reponame) 1151 for repos in rm.get_real_repositories() 1152 if repos.can_view(req.perm)) 1020 1153 1021 1154 elem = tag.ul( 1022 [tag.li(is_dir and tag.b(path) or path) 1023 for e in sorted(node.get_entries(), key=kind_order) 1024 for is_dir, path in [(e.isdir, '/' + e.path.lstrip('/'))] 1025 if e.name.lower().startswith(prefix)] 1026 ) 1155 [tag.li(isdir and tag.b(path) or path) 1156 for (isdir, name, path) in sorted(entries, key=kind_order) 1157 if name.lower().startswith(prefix)]) 1027 1158 1028 1159 xhtml = elem.generate().render('xhtml') … … 1038 1169 1039 1170 # -- normalize 1040 new_path = repos.normalize_path(new_path) 1041 if not new_path.startswith('/'): 1042 new_path = '/' + new_path 1043 new_rev = repos.normalize_rev(new_rev) 1044 old_path = repos.normalize_path(old_path) 1045 if not old_path.startswith('/'): 1046 old_path = '/' + old_path 1047 old_rev = repos.normalize_rev(old_rev) 1048 1049 repos.authz.assert_permission_for_changeset(new_rev) 1050 repos.authz.assert_permission_for_changeset(old_rev) 1171 new_reponame, new_repos, new_path = \ 1172 rm.get_repository_by_path(new_path) 1173 old_reponame, old_repos, old_path = \ 1174 rm.get_repository_by_path(old_path) 1175 new_rev = new_repos.normalize_rev(new_rev) 1176 old_rev = old_repos.normalize_rev(old_rev) 1051 1177 1052 1178 # -- prepare rendering 1053 data = {'new_path': new_path, 'new_rev': new_rev, 1054 'old_path': old_path, 'old_rev': old_rev} 1179 data = {'new_path': '/' + pathjoin(new_repos.reponame, new_path), 1180 'new_rev': new_rev, 1181 'old_path': '/' + pathjoin(old_repos.reponame, old_path), 1182 'old_rev': old_rev} 1055 1183 1056 1184 add_script(req, 'common/js/suggest.js') -
trunk/trac/versioncontrol/web_ui/log.py
r8853 r9125 31 31 from trac.util.text import wrap 32 32 from trac.util.translation import _ 33 from trac.versioncontrol.api import Changeset, NoSuchChangeset 33 from trac.versioncontrol.api import RepositoryManager, Changeset, \ 34 NoSuchChangeset 34 35 from trac.versioncontrol.web_ui.changeset import ChangesetModule 35 36 from trac.versioncontrol.web_ui.util import * … … 70 71 71 72 def process_request(self, req): 72 req.perm. assert_permission('LOG_VIEW')73 req.perm.require('LOG_VIEW') 73 74 74 75 mode = req.args.get('mode', 'stop_on_copy') … … 81 82 limit = int(req.args.get('limit') or self.default_log_limit) 82 83 83 repos = self.env.get_repository(req.authname) 84 rm = RepositoryManager(self.env) 85 reponame, repos, path = rm.get_repository_by_path(path) 86 87 if not repos: 88 raise ResourceNotFound(_("No repository '%(repo)s' found", 89 repo=reponame)) 90 91 if reponame != repos.reponame: # Redirect alias 92 qs = req.query_string 93 req.redirect(req.href.log(repos.reponame or None, path) 94 + (qs and '?' + qs or '')) 95 84 96 normpath = repos.normalize_path(path) 85 97 # if `revs` parameter is given, then we're restricted to the … … 101 113 # * for ''show only add, delete'' we're using 102 114 # `Repository.get_path_history()` 115 cset_resource = repos.resource.child('changeset') 103 116 if mode == 'path_history': 104 def history(limit): 105 for h in repos.get_path_history(path, rev, limit): 106 yield h 117 def history(): 118 for h in repos.get_path_history(path, rev): 119 if 'CHANGESET_VIEW' in req.perm(cset_resource(id=h[1])): 120 yield h 107 121 elif revranges: 108 def history( limit):122 def history(): 109 123 prevpath = path 110 124 expected_next_item = None … … 119 133 if rev < a: 120 134 break # simply skip, no separator 121 if expected_next_item: 122 # check whether we're continuing previous range 123 np, nrev, nchg = expected_next_item 124 if rev != nrev: # no, we need a separator 125 yield (np, nrev, None) 126 yield node_history[0] 135 if 'CHANGESET_VIEW' in req.perm(cset_resource(id=rev)): 136 if expected_next_item: 137 # check whether we're continuing previous range 138 np, nrev, nchg = expected_next_item 139 if rev != nrev: # no, we need a separator 140 yield (np, nrev, None) 141 yield node_history[0] 127 142 prevpath = node_history[-1][0] # follow copy 128 b = rev -1143 b = rev - 1 129 144 if len(node_history) > 1: 130 145 expected_next_item = node_history[-1] … … 134 149 yield (expected_next_item[0], expected_next_item[1], None) 135 150 else: 136 history = get_existing_node(req, repos, path, rev).get_history 151 def history(): 152 node = get_existing_node(req, repos, path, rev) 153 for h in node.get_history(): 154 if 'CHANGESET_VIEW' in req.perm(cset_resource(id=h[1])): 155 yield h 137 156 138 157 # -- retrieve history, asking for limit+1 results … … 141 160 previous_path = normpath 142 161 count = 0 143 for old_path, old_rev, old_chg in history( limit+1):162 for old_path, old_rev, old_chg in history(): 144 163 if stop_rev and repos.rev_older_than(old_rev, stop_rev): 145 164 break … … 188 207 if verbose: 189 208 params['verbose'] = verbose 190 return req.href.log( path, **params)209 return req.href.log(repos.reponame or None, path, **params) 191 210 192 211 if format in ('rss', 'changelog'): … … 238 257 239 258 data = { 240 'context': Context.from_request(req, 'source', path), 241 'path': path, 'rev': rev, 'stop_rev': stop_rev, 259 'context': Context.from_request(req, 'source', path, 260 parent=repos.resource), 261 'reponame': repos.reponame or None, 'repos': repos, 242 262 'path': path, 'rev': rev, 'stop_rev': stop_rev, 243 263 'revranges': revranges, … … 252 272 return 'revisionlog.txt', data, 'text/plain' 253 273 elif req.args.get('format') == 'rss': 254 data['context'] = Context.from_request(req, 'source', path, 274 data['context'] = Context.from_request(req, 'source', 275 path, parent=repos.resource, 255 276 absurls=True) 256 277 return 'revisionlog.rss', data, 'application/rss+xml' … … 273 294 add_stylesheet(req, 'common/css/browser.css') 274 295 275 path_links = get_path_links(req.href, path, rev)296 path_links = get_path_links(req.href, repos.reponame, path, rev) 276 297 if path_links: 277 298 data['path_links'] = path_links 278 if len(path_links) > 1:299 if path != '/': 279 300 add_link(req, 'up', path_links[-2]['href'], _('Parent directory')) 280 301 … … 288 309 289 310 add_ctxtnav(req, _('View Latest Revision'), 290 href=req.href.browser( path))311 href=req.href.browser(repos.reponame or None, path)) 291 312 if 'next' in req.chrome['links']: 292 313 next = req.chrome['links']['next'][0] … … 341 362 idx = min([i for i in indexes if i is not False]) 342 363 path, revs = match[:idx], match[idx+1:] 364 365 rm = RepositoryManager(self.env) 366 reponame = rm.get_default_repository(formatter.context) 367 if reponame is not None: 368 repos = rm.get_repository(reponame) 369 else: 370 reponame, repos, path = rm.get_repository_by_path(path) 371 343 372 revranges = None 344 373 if any(c for c in ':-,' if c in revs): 345 revranges = self._normalize_ranges( formatter.req, revs)374 revranges = self._normalize_ranges(repos, path, revs) 346 375 revs = None 347 376 if 'LOG_VIEW' in formatter.perm: 348 377 if revranges: 349 href = formatter.href.log(path or '/', revs=str(revranges)) 378 href = formatter.href.log(repos.reponame or None, path or '/', 379 revs=str(revranges)) 350 380 else: 351 repos = self.env.get_repository(formatter.req.authname)352 381 try: 353 382 rev = repos.normalize_rev(revs) 354 383 except NoSuchChangeset: 355 384 rev = None 356 href = formatter.href.log(path or '/', rev=rev) 385 href = formatter.href.log(repos.reponame or None, path or '/', 386 rev=rev) 357 387 if query and (revranges or revs): 358 388 query = '&' + query[1:] … … 363 393 LOG_LINK_RE = re.compile(r"([^@:]*)[@:]%s?" % REV_RANGE) 364 394 365 def _normalize_ranges(self, re q, revs):395 def _normalize_ranges(self, repos, path, revs): 366 396 ranges = revs.replace(':', '-') 367 397 try: … … 370 400 except ValueError: 371 401 # slow path, normalize each rev 372 repos = self.env.get_repository(req.authname)373 402 splitted_ranges = re.split(r'([-,])', ranges) 374 403 try: … … 376 405 except NoSuchChangeset: 377 406 return None 378 seps = splitted_ranges[1::2] +['']407 seps = splitted_ranges[1::2] + [''] 379 408 ranges = ''.join([str(rev)+sep for rev, sep in zip(revs, seps)]) 380 409 return Ranges(ranges) -
trunk/trac/versioncontrol/web_ui/tests/wikisyntax.py
r8734 r9125 4 4 5 5 from trac.test import Mock 6 from trac.versioncontrol import NoSuchChangeset 7 from trac.versioncontrol.api import * 8 from trac.versioncontrol.web_ui import * 6 9 from trac.wiki.tests import formatter 7 from trac.versioncontrol import NoSuchChangeset8 from trac.versioncontrol.web_ui import *9 10 10 11 11 12 def _get_changeset(rev): 12 13 if rev == '1': 13 return Mock(message="start" )14 return Mock(message="start", can_view=lambda perm: True) 14 15 else: 15 16 raise NoSuchChangeset(rev) … … 24 25 raise NoSuchChangeset(rev) 25 26 26 def _get_repository(authname=None): 27 return Mock(get_changeset=_get_changeset, youngest_rev='200', 27 def _get_repository(reponame): 28 return Mock(reponame=reponame, youngest_rev='200', 29 get_changeset=_get_changeset, 28 30 normalize_rev=_normalize_rev) 29 31 30 32 def repository_setup(tc): 31 33 setattr(tc.env, 'get_repository', _get_repository) 34 setattr(RepositoryManager(tc.env), 'get_repository', _get_repository) 32 35 33 36 … … 304 307 ------------------------------ 305 308 <p> 306 <a class="export" href="/export/ 200/foo/bar.html">export:/foo/bar.html</a>309 <a class="export" href="/export//foo/bar.html">export:/foo/bar.html</a> 307 310 <a class="export" href="/export/123/foo/pict.gif">export:123:/foo/pict.gif</a> 308 311 <a class="export" href="/export/123/foo/pict.gif">export:/foo/pict.gif@123</a> … … 313 316 ------------------------------ 314 317 <p> 315 <a class="export" href="/export/ 200/foo/bar.html#header">export:/foo/bar.html#header</a>318 <a class="export" href="/export//foo/bar.html#header">export:/foo/bar.html#header</a> 316 319 </p> 317 320 ------------------------------ -
trunk/trac/versioncontrol/web_ui/util.py
r8734 r9125 39 39 return changes 40 40 41 def get_path_links(href, fullpath, rev, order=None, desc=None): 42 links = [{'name': 'root', 43 'href': href.browser(rev=rev, order=order, desc=desc)}] 44 path = '' 45 for part in [p for p in fullpath.split('/') if p]: 46 path += part + '/' 41 def get_path_links(href, reponame, path, rev, order=None, desc=None): 42 desc = desc or None 43 links = [{'name': 'source:', 44 'href': href.browser(rev=reponame == '' and rev or None, 45 order=order, desc=desc)}] 46 if reponame: 47 links.append({ 48 'name': reponame, 49 'href': href.browser(reponame, rev=rev, order=order, desc=desc)}) 50 partial_path = '' 51 for part in [p for p in path.split('/') if p]: 52 partial_path += part + '/' 47 53 links.append({ 48 54 'name': part, 49 'href': href.browser(path, rev=rev, order=order, desc=desc) 55 'href': href.browser(reponame or None, partial_path, rev=rev, 56 order=order, desc=desc) 50 57 }) 51 58 return links -
trunk/trac/web/chrome.py
r8999 r9125 59 59 from trac.resource import * 60 60 from trac.util import compat, get_reporter_id, presentation, get_pkginfo, \ 61 translation61 pathjoin, translation 62 62 from trac.util.compat import any, partial 63 63 from trac.util.html import escape, plaintext … … 367 367 'paginate': presentation.paginate, 368 368 'partial': partial, 369 'pathjoin': pathjoin, 369 370 'plaintext': plaintext, 370 371 'pprint': pprint.pformat, … … 729 730 d.update({ 730 731 'context': req and Context.from_request(req) or None, 732 'Resource': Resource, 731 733 'url_of': get_rel_url, 732 734 'abs_url_of': get_abs_url, … … 742 744 'show_email_addresses': show_email_addresses, 743 745 'show_ip_addresses': self.show_ip_addresses, 746 'authorinfo': partial(self.authorinfo, req), 744 747 'format_author': partial(self.format_author, req), 745 748 'format_emails': self.format_emails, … … 893 896 return sep.join(all_cc) 894 897 898 def authorinfo(self, req, author, email_map=None): 899 if author: 900 return self.format_author(req, 901 email_map and '@' not in author and 902 email_map.get(author) or author) 903 else: 904 return 'anonymous' 905 895 906 def format_author(self, req, author): 896 907 if self.show_email_addresses or not req or 'EMAIL_VIEW' in req.perm: -
trunk/trac/wiki/default-pages/TracEnvironment
r8847 r9125 10 10 }}} 11 11 12 [wiki:TracAdmin trac-admin] will ask you for the name of the project, the 13 database connection string (explained below), and the type and path to 14 your source code repository. 12 [wiki:TracAdmin trac-admin] will ask you for the name of the project and the 13 database connection string (explained below). 15 14 16 15 ''Note: The web server user will require file system write permission to … … 97 96 == Source Code Repository == 98 97 99 You'll first have to provide the ''type'' of your repository (e.g. `svn` for Subversion, 100 which is the default), then the ''path'' where the repository is located. 98 Since version 0.12, one Trac environment can be tied to more than one repository. There are many different ways to add repositories to an environment, see TracRepositoryAdmin. This page also details the various attributes that can be set on a repository (like `type`, `url`, `description`). 101 99 102 If you don't want to use Trac with a source code repository, simply leave the ''path'' empty 103 (the ''type'' information doesn't matter, then). 100 If you don't want to use Trac with any source code repository, at creation time simply leave the ''path'' empty 101 (the ''type'' information doesn't matter in this case). Then you also need to disable the `trac.versioncontrol.*` components: 102 {{{ 103 [components] 104 trac.versioncontrol.* = disabled 105 }}} 104 106 105 107 For some systems, it is possible to specify not only the path to the repository, … … 108 110 Trac supports this; for other types, check the corresponding plugin's documentation. 109 111 110 Example of a configuration for a Subversion repository :112 Example of a configuration for a Subversion repository used as the default repository: 111 113 {{{ 112 114 [trac] … … 135 137 * `htdocs` - directory containing web resources, which can be referenced in Genshi templates. '''''(since 0.11)''''' 136 138 * `log` - default directory for log files, if logging is turned on and a relative path is given. 137 * `plugins` - Environment-specific [wiki:TracPlugins plugins] (Python eggs , since [trac:milestone:0.10 0.10])139 * `plugins` - Environment-specific [wiki:TracPlugins plugins] (Python eggs or single file plugins, since [trac:milestone:0.10 0.10]) 138 140 * `templates` - Custom Genshi environment-specific templates. '''''(since 0.11)''''' 139 141 * `site.html` - method to customize header, footer, and style, described in TracInterfaceCustomization#SiteAppearance 140 142 141 '''Note: don't confuse a Trac environment directory with the source code repository directory. 142 It happens that the above structure is loosely modelled after the Subversion repository directory 143 structure, but they are not and ''must not'' be located at the same place.''' 143 '''Caveat:''' ''don't confuse a Trac environment directory with the source code repository directory.'' 144 145 This is a common beginners' mistake. 146 It happens that the structure for a Trac environment is loosely modelled after the Subversion repository directory 147 structure, but those are two disjoint entities and they are not and ''must not'' be located at the same place. 144 148 145 149 ---- -
trunk/trac/wiki/default-pages/TracIni
r8847 r9125 19 19 20 20 21 == Reference ==21 == Reference for settings 22 22 23 23 This is a brief reference of available configuration options. … … 25 25 ''Note that the [bitten], [spam-filter] and [vote] sections below are added by plugins enabled on this Trac, and therefore won't be part of a default installation.'' 26 26 27 [[TracIni()]] 27 ''Note that the [bitten], [spam-filter] and [vote] sections below are added by plugins enabled on this Trac, and therefore won't be part of a default installation.'' 28 28 29 [[TracIni]] 29 30 30 == [components] == #components-section 31 == Reference for special sections 32 [[PageOutline(3,,inline)]] 33 34 === [components] === #components-section 31 35 This section is used to enable or disable components provided by plugins, as well as by Trac itself. The component to enable/disable is specified via the name of the option. Whether its enabled is determined by the option value; setting the value to `enabled` or `on` will enable the component, any other value (typically `disabled` or `off`) will disable the component. 32 36 … … 46 50 See also: TracPlugins 47 51 48 == [ticket-custom] == #ticket-custom-section49 52 50 In this section, you can define additional fields for tickets. See TracTicketsCustomFields for more details. 51 52 == [ticket-workflow] == #ticket-workflow-section 53 ''(since 0.11)'' 54 55 The workflow for tickets is controlled by plugins. 56 By default, there's only a `ConfigurableTicketWorkflow` component in charge. 57 That component allows the workflow to be configured via this section in the trac.ini file. 58 See TracWorkflow for more details. 59 60 == [milestone-groups] == #milestone-groups-section 53 === [milestone-groups] === #milestone-groups-section 61 54 ''(since 0.11)'' 62 55 … … 72 65 closed.order = 0 73 66 # optional extra param for the query (two additional columns: created and modified and sort on created) 74 group=resolution,order=time,col=id,col=summary,col=owner,col=type,col=priority,col=component,col=severity,col=time,col=changetime67 closed.query_args = group=resolution,order=time,col=id,col=summary,col=owner,col=type,col=priority,col=component,col=severity,col=time,col=changetime 75 68 # indicates groups that count for overall completion 76 69 closed.overall_completion = truepercentage … … 98 91 selector: `table.progress td.<class>` 99 92 100 == [svn:externals] == #svn:externals-section 93 === [repositories] === #repositories-section 94 95 (''since 0.12'' multirepos) 96 97 One of the alternatives for registering new repositories is to populate the `[repositories]` section of the trac.ini. 98 99 This is especially suited for setting up convenience aliases, short-lived repositories, or during the initial phases of an installation. 100 101 See [TracRepositoryAdmin#Intrac.ini TracRepositoryAdmin] for details about the format adopted for this section and the rest of that page for the other alternatives. 102 103 === [svn:externals] === #svn:externals-section 101 104 ''(since 0.11)'' 102 105 103 The TracBrowser for Subversion can interpret the `svn:externals` property of folders out of the box. 104 However, if those externals are ''not'' using the `http:` or `https:` protocol, or if a link to a different repository browser such as another Trac or [http://www.viewvc.org/ ViewVC] is desired, then Trac needs to be able to map an external prefix to this other URL. 106 The TracBrowser for Subversion can interpret the `svn:externals` property of folders. 107 By default, it only turns the URLs into links as Trac can't browse remote repositories. 108 109 However, if you have another Trac instance (or an other repository browser like [http://www.viewvc.org/ ViewVC]) configured to browse the target repository, then you can instruct Trac which other repository browser to use for which external URL. 105 110 106 111 This mapping is done in the `[svn:externals]` section of the TracIni … … 109 114 {{{ 110 115 [svn:externals] 111 1 = svn://server/repos1 http://trac/proj1/browser/$path?rev=$rev112 2 = svn://server/repos2 http://trac/proj2/browser/$path?rev=$rev116 1 = svn://server/repos1 http://trac/proj1/browser/$path?rev=$rev 117 2 = svn://server/repos2 http://trac/proj2/browser/$path?rev=$rev 113 118 3 = http://theirserver.org/svn/eng-soft http://ourserver/viewvc/svn/$path/?pathrev=25914 114 119 4 = svn://anotherserver.com/tools_repository http://ourserver/tracs/tools/browser/$path?rev=$rev … … 120 125 Finally, the relative URLs introduced in [http://subversion.tigris.org/svn_1.5_releasenotes.html#externals Subversion 1.5] are not yet supported. 121 126 127 === [ticket-custom] === #ticket-custom-section 128 129 In this section, you can define additional fields for tickets. See TracTicketsCustomFields for more details. 130 131 === [ticket-workflow] === #ticket-workflow-section 132 ''(since 0.11)'' 133 134 The workflow for tickets is controlled by plugins. 135 By default, there's only a `ConfigurableTicketWorkflow` component in charge. 136 That component allows the workflow to be configured via this section in the trac.ini file. 137 See TracWorkflow for more details. 138 139 122 140 ---- 123 141 See also: TracGuide, TracAdmin, TracEnvironment -
trunk/trac/wiki/default-pages/TracInstall
r8848 r9125 1 1 = Trac Installation Guide for 0.12dev = 2 {{{ 3 #!div style="margin-top: .5em; padding: 0 1em; background-color: #ffd; border:1px outset #ddc;" 4 5 '''NOTE: this page is for 0.12dev (trunk), the version currently in development. For installing previous Trac versions, please refer to TracInstall (0.11)''' 6 }}} 2 7 3 [[TracGuideToc]] 8 4 9 5 Trac is written in the Python programming language and needs a database, [http://sqlite.org/ SQLite], [http://www.postgresql.org/ PostgreSQL], or [http://mysql.com/ MySQL]. For HTML rendering, Trac uses the [http://genshi.edgewall.org Genshi] templating system. 10 6 11 Since version 0.12, Trac can also be localized, and there's probably a translation available for your language. If you want to be able to use the Trac interface in other languages, then make sure you have installed the optional package [#OtherPythonPackages Babel]. Pay attention to the extra steps for localization support in the [#InstallingTrac Installing Trac] section below. Note Trac can still be installed without Babel, but thenyou will only get the default english version, as usual.7 Since version 0.12, Trac can also be localized, and there's probably a translation available for your language. If you want to be able to use the Trac interface in other languages, then make sure you have installed the optional package [#OtherPythonPackages Babel]. Pay attention to the extra steps for localization support in the [#InstallingTrac Installing Trac] section below. Lacking Babel, you will only get the default english version, as usual. 12 8 13 9 If you're interested in contributing new translations for other languages or enhance the existing translations, then please have a look at ["TracL10N"]. … … 17 13 18 14 == Prerequisites == 19 15 === Mandatory Prerequisites 20 16 To install Trac, the following software packages must be installed: 21 17 22 18 * [http://www.python.org/ Python], version >= 2.4 (we dropped the support for Python 2.3 in this release) 23 19 * [http://peak.telecommunity.com/DevCenter/setuptools setuptools], version >= 0.6 24 * [http://genshi.edgewall.org/wiki/Download Genshi], [genshi:source:trunk trunk], from svn, minimum required revision is [G1072]. 25 * You also need a database system and the corresponding python drivers for it. 26 The database can be either SQLite, PostgreSQL or MySQL. 27 28 === For SQLite === 20 * [http://genshi.edgewall.org/wiki/Download Genshi], [genshi:source:trunk trunk], from svn, minimum required revision is [G1072], current [G1092] works fine as well. 21 22 You also need a database system and the corresponding python bindings. 23 The database can be either SQLite, PostgreSQL or MySQL. 24 25 ==== For the SQLite database #ForSQLite 29 26 30 27 If you're using Python 2.5 or 2.6, you already have everything you need. … … 47 44 See additional information in [trac:PySqlite PySqlite]. 48 45 49 === For PostgreSQL ===46 ==== For the PostgreSQL database #ForPostgreSQL 50 47 51 48 You need to install the database and its Python bindings: 52 49 * [http://www.postgresql.org/ PostgreSQL] 53 * [http:// initd.org/projects/psycopg2 psycopg2]54 55 See [trac:DatabaseBackend#Postgresql DatabaseBackend] for details 56 57 58 === For MySQL ===50 * [http://pypi.python.org/pypi/psycopg2 psycopg2] 51 52 See [trac:DatabaseBackend#Postgresql DatabaseBackend] for details. 53 54 55 ==== For the MySQL database #ForMySQL 59 56 60 57 Trac can now work quite well with MySQL, provided you follow the guidelines. … … 63 60 * [http://sf.net/projects/mysql-python MySQLdb], version 1.2.2 or later 64 61 65 See [trac:MySqlDb MySqlDb] for more detailed information. 66 It is ''very'' important to read carefully that page before creating the database. 67 68 === Optional Requirements === 62 It is '''very''' important to read carefully the [trac:MySqlDb MySqlDb] page before creating the database. 63 64 === Optional Prerequisites 69 65 70 66 ==== Version Control System ==== 71 67 72 68 ===== Subversion ===== 73 '''Please note:''' if using Subversion, Trac must be installed on the '''same machine'''. Remote repositories are currently not supported. 74 75 * [http://subversion.tigris.org/ Subversion], version >= 1.0. (versions recommended: 1.2.4, 1.3.2 or 1.4.2) and the '''''corresponding''''' Python bindings. For troubleshooting, check [trac:TracSubversion TracSubversion] 76 [[br]] ''FIXME'' upgrade the requirements; it makes no sense to still support 1.0 or even 1.3, as Subversion itself only still supports 1.4.x 77 * Trac uses the [http://svnbook.red-bean.com/svnbook-1.1/ch08s02.html#svn-ch-8-sect-2.3 SWIG] bindings included in the Subversion distribution, '''not''' [http://pysvn.tigris.org/ PySVN] (which is sometimes confused with the standard SWIG bindings), neither does it support the newer `ctype`-style bindings 78 * If Subversion was already installed without the SWIG bindings, on Unix you'll need to re-`configure` Subversion and `make swig-py`, `make install-swig-py`.79 * There are [http://subversion.tigris.org/servlets/ProjectDocumentList?folderID=91 pre-compiled bindings] available for win32. 69 * [http://subversion.apache.org/ Subversion], 1.5.x or 1.6.x and the '''''corresponding''''' Python bindings. Actually older versions starting from 1.0, like 1.2.4, 1.3.2 or 1.4.2, etc. should still work. For troubleshooting information, check the [trac:TracSubversion#Troubleshooting TracSubversion] page/ 70 71 There are [http://subversion.apache.org/packages.html pre-compiled SWIG bindings] available for various platforms. Note that Trac '''doesn't''' use [http://pysvn.tigris.org/ PySVN], neither does it work yet with the newer `ctype`-style bindings 72 73 74 '''Please note:''' if using Subversion, Trac must be installed on the '''same machine'''. Remote repositories are currently [trac:ticket:493 not supported]. 75 80 76 81 77 ===== Others ===== … … 90 86 server (see [trac:TracOnWindowsIisAjp TracOnWindowsIisAjp]), or 91 87 * [http://httpd.apache.org/ Apache] with 92 - [http://code.google.com/p/modwsgi/ mod_wsgi], see [wiki:TracModWSGI] or88 - [http://code.google.com/p/modwsgi/ mod_wsgi], see [wiki:TracModWSGI] and 93 89 http://code.google.com/p/modwsgi/wiki/IntegrationWithTrac 94 90 - [http://modpython.org/ mod_python 3.3.1], see TracModPython) … … 162 158 easy_install --prefix=/usr/local --install-dir=/Library/Python/2.5/site-packages 163 159 }}} 160 Note: If installing on Mac OS X 10.6 running {{{ easy_install http://svn.edgewall.org/repos/trac/trunk }}} will install into /usr/local and /Library/Python/2.6/site-packages by default 164 161 165 162 The above will place your `tracd` and `trac-admin` commands into `/usr/local/bin` and will install the Trac libraries and dependencies into `/Library/Python/2.5/site-packages`, which is Apple's preferred location for third-party Python application installations. … … 168 165 == Creating a Project Environment == 169 166 170 A [ wiki:TracEnvironment Trac environment] is the backend storage where Trac stores information like wiki pages, tickets, reports, settings, etc. An environment is basically a directory that contains a human-readable configuration fileand various other files and directories.167 A [TracEnvironment Trac environment] is the backend storage where Trac stores information like wiki pages, tickets, reports, settings, etc. An environment is basically a directory that contains a human-readable [TracIni configuration file], and various other files and directories. 171 168 172 169 A new environment is created using [wiki:TracAdmin trac-admin]: … … 175 172 }}} 176 173 177 [wiki:TracAdmin trac-admin] will prompt you for the information it needs to create the environment, such as the name of the project, the type and the path to an existing [wiki:TracEnvironment#SourceCodeRepository source code repository], the [wiki:TracEnvironment#DatabaseConnectionStrings database connection string], and so on. If you're not sure what to specify for one of these options, just leave it blank to use the default value. The database connection string in particular will always work as long as you have SQLite installed. Leaving the path to the source code repository empty will disable any functionality related to version control, but you can always add that back when the basic system is running. 178 179 Also note that the values you specify here can be changed later by directly editing the [wiki:TracIni] configuration file. 180 181 ''Note: The user account under which the web server runs will require write permissions to the environment directory and all the files inside. On Linux, with the web server running as user apache and group apache, enter:'' 182 183 chown -R apache.apache /path/to/myproject 174 [TracAdmin trac-admin] will prompt you for the information it needs to create the environment, such as the name of the project and the [TracEnvironment#DatabaseConnectionStrings database connection string]. If you're not sure what to specify for one of these options, just press `<Enter>` to use the default value. 175 176 Leaving the database connection string empty in particular will always work as long as you have SQLite installed. 177 For the other [DatabaseBackend database backends] you should plan ahead and already have a database ready to use at this point. 178 179 Since 0.12, Trac doesn't ask for a [TracEnvironment#SourceCodeRepository source code repository] anymore when creating an environment. Repositories can be [TracRepositoryAdmin added] afterward, or the version control support can be disabled completely if you don't need it. 180 181 Also note that the values you specify here can be changed later by directly editing the [TracIni conf/trac.ini] configuration file. 182 183 Finally, make sure the user account under which the web front-end runs will have '''write permissions''' to the environment directory and all the files inside. This will be the case if you run `trac-admin ... initenv` as this user. If not, you should set the correct user afterwards. For example on Linux, with the web server running as user `apache` and group `apache`, enter: 184 {{{ 185 # chown -R apache.apache /path/to/myproject 186 }}} 184 187 185 188 == Running the Standalone Server == … … 239 242 ---- 240 243 See also: [trac:TracInstallPlatforms TracInstallPlatforms], TracGuide, TracCgi, TracFastCgi, TracModPython, [wiki:TracModWSGI], TracUpgrade, TracPermissions 241 -
trunk/trac/wiki/default-pages/TracUpgrade
r8848 r9125 70 70 SQLite v2.x is no longer supported, if you happen to still use a Trac database using this format, you'll need to convert it to SQLite v3.x first. See [trac:PySqlite#UpgradingSQLitefrom2.xto3.x] for details. 71 71 72 If you plan to add more repositories to your Trac instance, as this is now possible with the newly introduced multiple repository support, please refer to TracRepositoryAdmin#Migration. 73 74 This can be of interest even if you only have one repository, as there's now a way to avoid the potentially costly resync check at every request. 75 72 76 ==== Upgrading to Trac 0.11 ==== 73 77 ===== Site Templates ===== -
trunk/trac/wiki/default-pages/WikiFormatting
r8848 r9125 5 5 6 6 Trac has a built in small and powerful wiki rendering engine. This wiki engine implements an ever growing subset of the commands from other popular Wikis, 7 especially [http://moinmo in.wikiwikiweb.de/ MoinMoin].7 especially [http://moinmo.in/ MoinMoin] and [trac:WikiCreole]. 8 8 9 9 … … 11 11 12 12 13 [[PageOutline(2,Markup Categories,inline)]] 14 15 13 16 == Font Styles == 14 17 15 18 The Trac wiki supports the following font styles: 16 {{{ 17 * '''bold''', '''!''' can be bold too''', and '''! ''' 19 ||= Wiki Markup =||= Display =|| 20 {{{#!td 21 {{{ 22 * '''bold''', 23 ''' triple quotes !''' 24 can be bold too if prefixed by ! ''', 25 * ''italic'' 26 * '''''bold italic''''' or ''italic and 27 ''' italic bold ''' '' 28 * __underline__ 29 * {{{monospace}}} or `monospace` 30 (hence `{{{` or {{{`}}} quoting) 31 * ~~strike-through~~ 32 * ^superscript^ 33 * ,,subscript,, 34 }}} 35 }}} 36 {{{#!td 37 * '''bold''', 38 ''' triple quotes !''' 39 can be bold too if prefixed by ! ''', 18 40 * ''italic'' 19 * '''''bold italic''''' 41 * '''''bold italic''''' or ''italic and 42 ''' italic bold ''' '' 20 43 * __underline__ 21 44 * {{{monospace}}} or `monospace` 45 (hence `{{{` or {{{`}}} quoting) 22 46 * ~~strike-through~~ 23 47 * ^superscript^ … … 25 49 }}} 26 50 27 Display:28 * '''bold''', '''!''' can be bold too''', and '''! '''29 * ''italic''30 * '''''bold italic'''''31 * __underline__32 * {{{monospace}}} or `monospace`33 * ~~strike-through~~34 * ^superscript^35 * ,,subscript,,36 37 51 Notes: 38 52 * `{{{...}}}` and {{{`...`}}} commands not only select a monospace font, but also treat their content as verbatim text, meaning that no further wiki processing is done on this text. 39 53 * {{{ ! }}} tells wiki parser to not take the following characters as wiki format, so pay attention to put a space after !, e.g. when ending bold. 54 * all the font styles marks have to be used in opening/closing pairs, 55 and they must nest properly 40 56 41 57 == Headings == 42 58 43 You can create heading by starting a line with one up to five''equal'' characters ("=")44 followed by a single space and the headline text. The line should end with a space45 followed by the same number of ''='' characters .59 You can create heading by starting a line with one up to six ''equal'' characters ("=") 60 followed by a single space and the headline text. The headline text can be 61 followed by the same number of ''='' characters, but this is no longer mandatory. 46 62 The heading might optionally be followed by an explicit id. If not, an implicit but nevertheless readable id will be generated. 47 63 64 ||= Wiki Markup =||= Display =|| 65 {{{#!td 66 {{{ 67 = Heading = 68 == Subheading 69 === About ''this'' === 70 === Explicit id === #using-explicit-id-in-heading 71 == Subheading #sub2 72 }}} 73 }}} 74 {{{#!td style="padding: 1em;" 75 {{{ 76 #!div 77 == Subheading 78 === About ''this'' === 79 === Explicit id === #using-explicit-id-in-heading 80 == Subheading #sub2 81 }}} 82 }}} 83 84 == Paragraphs == 85 86 A new text paragraph is created whenever two blocks of text are separated by one or more empty lines. 87 88 A forced line break can also be inserted, using: 89 ||= Wiki Markup =||= Display =|| 90 {{{#!td 91 {{{ 92 Line 1[[BR]]Line 2 93 }}} 94 {{{ 95 Paragraph 96 one 97 98 Paragraph 99 two 100 }}} 101 }}} 102 {{{#!td 103 Line 1[[BR]]Line 2 104 105 Paragraph 106 one 107 108 Paragraph 109 two 110 }}} 111 112 == Lists == 113 114 The wiki supports both ordered/numbered and unordered lists. 115 48 116 Example: 49 {{{ 50 = Heading = 51 == Subheading == 52 === About ''this'' === 53 === Explicit id === #using-explicit-id-in-heading 54 }}} 55 56 Display: 57 = Heading = 58 == Subheading == 59 === About ''this'' === 60 === Explicit id === #using-explicit-id-in-heading 61 62 == Paragraphs == 63 64 A new text paragraph is created whenever two blocks of text are separated by one or more empty lines. 65 66 A forced line break can also be inserted, using: 67 {{{ 68 Line 1[[BR]]Line 2 69 }}} 70 Display: 71 72 Line 1[[BR]]Line 2 73 74 75 == Lists == 76 77 The wiki supports both ordered/numbered and unordered lists. 78 79 Example: 80 {{{ 117 ||= Wiki Markup =||= Display =|| 118 {{{#!td 119 {{{ 120 * Item 1 121 * Item 1.1 122 * Item 1.1.1 123 * Item 1.1.2 124 * Item 1.1.3 125 * Item 1.2 126 * Item 2 127 - items can start at the beginning of a line 128 and they can span multiple lines 129 - be careful though to continue the line 130 with the appropriate indentation, otherwise 131 that will start a new paragraph... 132 133 1. Item 1 134 a. Item 1.a 135 a. Item 1.b 136 i. Item 1.b.i 137 i. Item 1.b.ii 138 1. Item 2 139 And numbered lists can also be restarted 140 with an explicit number: 141 3. Item 3 142 }}} 143 }}} 144 {{{#!td 81 145 * Item 1 82 146 * Item 1.1 … … 86 150 * Item 1.2 87 151 * Item 2 152 - items can start at the beginning of a line 153 and they can span multiple lines 154 - be careful though to continue the line 155 with the appropriate indentation, otherwise 156 that will start a new paragraph... 88 157 89 158 1. Item 1 … … 93 162 i. Item 1.b.ii 94 163 1. Item 2 95 And numbered lists can also be givenan explicit number:164 And numbered lists can also be restarted with an explicit number: 96 165 3. Item 3 97 166 }}} 98 167 99 Display:100 * Item 1101 * Item 1.1102 * Item 1.1.1103 * Item 1.1.2104 * Item 1.1.3105 * Item 1.2106 * Item 2107 108 1. Item 1109 a. Item 1.a110 a. Item 1.b111 i. Item 1.b.i112 i. Item 1.b.ii113 1. Item 2114 And numbered lists can also be given an explicit number:115 3. Item 3116 117 Note that there must be one or more spaces preceding the list item markers, otherwise the list will be treated as a normal paragraph.118 119 168 120 169 == Definition Lists == 121 170 122 123 171 The wiki also supports definition lists. 124 172 125 Example: 126 {{{ 173 ||= Wiki Markup =||= Display =|| 174 {{{#!td 175 {{{ 176 llama:: 177 some kind of mammal, with hair 178 ppython:: 179 some kind of reptile, without hair 180 (can you spot the typo?) 181 }}} 182 }}} 183 {{{#!td 127 184 llama:: 128 185 some kind of mammal, with hair … … 132 189 }}} 133 190 134 Display:135 llama::136 some kind of mammal, with hair137 ppython::138 some kind of reptile, without hair139 (can you spot the typo?)140 141 191 Note that you need a space in front of the defined term. 142 192 … … 146 196 Block containing preformatted text are suitable for source code snippets, notes and examples. Use three ''curly braces'' wrapped around the text to define a block quote. The curly braces need to be on a separate line. 147 197 148 Example: 198 ||= Wiki Markup =||= Display =|| 199 {{{#!td 200 {{{ 201 {{{ 202 def HelloWorld(): 203 print '''Hello World''' 204 }}} 205 }}} 206 }}} 207 {{{#!td 208 {{{ 209 def HelloWorld(): 210 print '''Hello World''' 211 }}} 212 }}} 213 214 Note that this kind of block is also used for selecting lines that should be processed through WikiProcessors. 215 216 == Blockquotes == 217 218 In order to mark a paragraph as blockquote, indent that paragraph with two spaces. 219 220 ||= Wiki Markup =||= Display =|| 221 {{{#!td 149 222 {{{ 150 {{{ 151 def HelloWorld(): 152 print "Hello World" 153 }}} 154 }}} 155 156 Display: 157 {{{ 158 def HelloWorld(): 159 print "Hello World" 160 }}} 161 162 163 == Blockquotes == 164 165 In order to mark a paragraph as blockquote, indent that paragraph with two spaces. 166 167 Example: 168 {{{ 223 Paragraph 169 224 This text is a quote from someone else. 170 225 }}} 171 172 Display: 226 }}} 227 {{{#!td 228 Paragraph 173 229 This text is a quote from someone else. 230 }}} 174 231 175 232 == Discussion Citations == … … 177 234 To delineate a citation in an ongoing discussion thread, such as the ticket comment area, e-mail-like citation marks (">", ">>", etc.) may be used. 178 235 179 Example: 180 {{{ 236 ||= Wiki Markup =||= Display =|| 237 {{{#!td 238 {{{ 239 >> Someone's original text 240 > Someone else's reply text 241 > - which can be any kind of Wiki markup 242 My reply text 243 }}} 244 }}} 245 {{{#!td 181 246 >> Someone's original text 182 247 > Someone else's reply text 248 > - which can be any kind of Wiki markup 183 249 My reply text 184 250 }}} 185 251 186 Display:187 >> Someone's original text188 > Someone else's reply text189 My reply text190 191 ''Note: Some WikiFormatting elements, such as lists and preformatted text, are lost in the citation area. Some reformatting may be necessary to create a clear citation.''192 252 193 253 == Tables == 194 254 === Simple Tables === 195 255 Simple tables can be created like this: 196 {{{ 256 ||= Wiki Markup =||= Display =|| 257 {{{#!td 258 {{{ 259 ||Cell 1||Cell 2||Cell 3|| 260 ||Cell 4||Cell 5||Cell 6|| 261 }}} 262 }}} 263 {{{#!td style="padding: 2em;" 197 264 ||Cell 1||Cell 2||Cell 3|| 198 265 ||Cell 4||Cell 5||Cell 6|| 199 266 }}} 200 267 201 Display:202 ||Cell 1||Cell 2||Cell 3||203 ||Cell 4||Cell 5||Cell 6||204 205 206 268 Cell headings can be specified by wrapping the content in a pair of '=' characters. 207 269 Note that the '=' characters have to stick to the cell separators, like this: 208 {{{ 270 ||= Wiki Markup =||= Display =|| 271 {{{#!td 272 {{{ 273 || ||= stable =||= latest =|| 274 ||= 0.10 =|| 0.10.5 || 0.10.6dev|| 275 ||= 0.11 =|| 0.11.6 || 0.11.7dev|| 276 }}} 277 }}} 278 {{{#!td style="padding: 2em;" 209 279 || ||= stable =||= latest =|| 210 280 ||= 0.10 =|| 0.10.5 || 0.10.6dev|| … … 212 282 }}} 213 283 214 Display:215 || ||= stable =||= latest =||216 ||= 0.10 =|| 0.10.5 || 0.10.6dev||217 ||= 0.11 =|| 0.11.6 || 0.11.7dev||218 219 284 Finally, specifying an empty cell means that the next non empty cell will span the empty cells. For example: 220 {{{ 285 ||= Wiki Markup =||= Display =|| 286 {{{#!td 287 {{{ 288 || 1 || 2 || 3 || 289 |||| 1-2 || 3 || 290 || 1 |||| 2-3 || 291 |||||| 1-2-3 || 292 }}} 293 }}} 294 {{{#!td style="padding: 2em;" 221 295 || 1 || 2 || 3 || 222 |||| 1 2 || 3 || 223 || 1 |||| 2 3 || 224 |||||| 1 2 3 || 225 }}} 226 227 Display: 228 || 1 || 2 || 3 || 229 |||| 1 2 || 3 || 230 || 1 |||| 2 3 || 231 |||||| 1 2 3 || 232 233 Note that more complex tables can be created using 234 [wiki:WikiRestructuredText#BiggerReSTExample reStructuredText]. 296 |||| 1-2 || 3 || 297 || 1 |||| 2-3 || 298 |||||| 1-2-3 || 299 }}} 300 301 Note that if the content of a cell "sticks" to one side of the cell and only one, then the text will be aligned on that side. Example: 302 ||= Wiki Markup =||= Display =|| 303 {{{#!td 304 {{{ 305 ||=Text =||= Numbers =|| 306 ||left align || 1.0|| 307 || center || 4.5|| 308 || right align|| 4.5|| 309 || default alignment || 2.5|| 310 ||default|| 2.5|| 311 || default || 2.5|| 312 || default || 2.5|| 313 }}} 314 }}} 315 {{{#!td style="padding: 2em;" 316 ||=Text =||= Numbers =|| 317 ||left align || 1.0|| 318 || center || 4.5|| 319 || right align|| 4.5|| 320 || default alignment || 2.5|| 321 ||default|| 2.5|| 322 || default || 2.5|| 323 || default || 2.5|| 324 }}} 325 326 If contrary to the example above, the cells in your table contain more text, it might be convenient to spread a table row over multiple lines of markup. The `\` character placed at the end of a line after a cell separator tells Trac to not start a new row for the cells on the next line. 327 328 ||= Wiki Markup =|| 329 {{{#!td 330 {{{ 331 || this is column 1 [http://trac.edgewall.org/newticket new ticket] || \ 332 || this is column 2 [http://trac.edgewall.org/roadmap the road ahead] || \ 333 || that's column 3 and last one || 334 }}} 335 }}} 336 |------------- 337 ||= Display =|| 338 {{{#!td style="padding: 2em;" 339 || this is column 1 [http://trac.edgewall.org/newticket new ticket] || \ 340 || this is column 2 [http://trac.edgewall.org/roadmap the road ahead] || \ 341 || that's column 3 and last one || 342 }}} 343 344 === Complex Tables === 345 346 If the possibilities offered by the simple "pipe"-based markup for tables described above are not enough for your needs, you can create more elaborated tables by using [#Processors-example-tables WikiProcessor based tables]. 235 347 236 348 … … 239 351 Hyperlinks are automatically created for WikiPageNames and URLs. !WikiPageLinks can be disabled by prepending an exclamation mark "!" character, such as {{{!WikiPageLink}}}. 240 352 241 Example: 242 {{{ 243 TitleIndex, http://www.edgewall.com/, !NotAlink 244 }}} 245 246 Display: 247 TitleIndex, http://www.edgewall.com/, !NotAlink 353 ||= Wiki Markup =||= Display =|| 354 {{{#!td 355 {{{ 356 TitleIndex, http://www.edgewall.com/, !NotAlink 357 }}} 358 }}} 359 {{{#!td 360 TitleIndex, http://www.edgewall.com/, !NotAlink 361 }}} 248 362 249 363 Links can be given a more descriptive title by writing the link followed by a space and a title and all this inside square brackets. If the descriptive title is omitted, then the explicit prefix is discarded, unless the link is an external link. This can be useful for wiki pages not adhering to the WikiPageNames convention. 250 364 251 Example: 252 {{{ 365 ||= Wiki Markup =||= Display =|| 366 {{{#!td 367 {{{ 368 * [http://www.edgewall.com/ Edgewall Software] 369 * [wiki:TitleIndex Title Index] 370 * [wiki:ISO9000] 371 }}} 372 }}} 373 {{{#!td 253 374 * [http://www.edgewall.com/ Edgewall Software] 254 375 * [wiki:TitleIndex Title Index] … … 256 377 }}} 257 378 258 Display:259 * [http://www.edgewall.com/ Edgewall Software]260 * [wiki:TitleIndex Title Index]261 * [wiki:ISO9000]262 263 379 == Trac Links == 264 380 265 381 Wiki pages can link directly to other parts of the Trac system. Pages can refer to tickets, reports, changesets, milestones, source files and other Wiki pages using the following notations: 266 {{{ 267 * Tickets: #1 or ticket:1 268 * Reports: {1} or report:1 269 * Changesets: r1, [1] or changeset:1 270 * ... 271 }}} 272 273 Display: 382 383 ||= Wiki Markup =||= Display =|| 384 {{{#!td 385 {{{ 386 * Tickets: #1 or ticket:1 387 * Reports: {1} or report:1 388 * Changesets: r1, [1] or changeset:1 389 * ... 390 }}} 391 }}} 392 {{{#!td 274 393 * Tickets: #1 or ticket:1 275 394 * Reports: {1} or report:1 276 395 * Changesets: r1, [1] or changeset:1 277 396 * ... 397 }}} 278 398 279 399 There are many more flavors of Trac links, see TracLinks for more in-depth information. … … 284 404 You may avoid making hyperlinks out of TracLinks by preceding an expression with a single "!" (exclamation mark). 285 405 286 Example: 287 {{{ 406 ||= Wiki Markup =||= Display =|| 407 {{{#!td 408 {{{ 409 !NoHyperLink 410 !#42 is not a link 411 }}} 412 }}} 413 {{{#!td 288 414 !NoHyperLink 289 415 !#42 is not a link 290 416 }}} 291 292 Display:293 !NoHyperLink294 !#42 is not a link295 296 417 297 418 == Images == … … 307 428 * `[[Image(source:/trunk/trac/htdocs/trac_logo_mini.png)]]` (a file in repository) 308 429 309 Example display: [[Image(htdocs:../common/trac_logo_mini.png)]] 430 ||= Wiki Markup =||= Display =|| 431 {{{#!td 432 {{{ 433 [[Image(htdocs:../common/trac_logo_mini.png)]] 434 }}} 435 }}} 436 {{{#!td 437 [[Image(htdocs:../common/trac_logo_mini.png)]] 438 }}} 310 439 311 440 See WikiMacros for further documentation on the `[[Image()]]` macro. … … 316 445 Macros are ''custom functions'' to insert dynamic content in a page. 317 446 318 Example: 319 {{{ 320 [[RecentChanges(Trac,3)]] 321 }}} 322 323 Display: 324 [[RecentChanges(Trac,3)]] 447 ||= Wiki Markup =||= Display =|| 448 {{{#!td 449 {{{ 450 [[RecentChanges(Trac,3)]] 451 }}} 452 }}} 453 {{{#!td style="padding-left: 2em" 454 [[RecentChanges(Trac,3)]] 455 }}} 325 456 326 457 See WikiMacros for more information, and a list of installed macros. 458 459 The detailed help for a specific macro can also be obtained more directly by appending a "?" to the macro name. 460 461 ||= Wiki Markup =||= Display =|| 462 {{{#!td 463 {{{ 464 [[MacroList?]] 465 }}} 466 }}} 467 {{{#!td style="padding-left: 2em" 468 [[MacroList?]] 469 }}} 327 470 328 471 … … 332 475 [wiki:WikiRestructuredText reStructuredText] or [wiki:WikiHtml HTML]. 333 476 334 Example 1: 335 {{{ 336 #!html 337 <pre class="wiki">{{{ 338 #!html 339 <h1 style="text-align: right; color: blue">HTML Test</h1> 340 }}}</pre> 341 }}} 342 343 Display: 477 ||= Wiki Markup =||= Display =|| 478 |-------------------------------------------------------- 479 {{{#!td align="center" colspan=2 style="border: 0px; font-size: 90%" 480 481 [=#Processors-example-html Example 1:] HTML 482 483 }}} 484 |-------------------------------------------------------- 485 {{{#!td style="border: 0px" 486 {{{ 487 {{{ 488 #!html 489 <h1 style="text-align: right; color: blue"> 490 HTML Test 491 </h1> 492 }}} 493 }}} 494 }}} 495 {{{#!td valign="top" style="border: 0px" 496 344 497 {{{ 345 498 #!html … … 347 500 }}} 348 501 349 Example: 502 }}} 503 |-------------------------------------------------------- 504 {{{#!td align="center" colspan=2 style="border: 0px; font-size: 90%" 505 506 [=#Processors-example-highlight Example 2:] Code Highlighting 507 508 }}} 509 |-------------------------------------------------------- 510 {{{#!td style="border: 0px" 511 {{{ 512 {{{ 513 #!python 514 class Test: 515 516 def __init__(self): 517 print "Hello World" 518 if __name__ == '__main__': 519 Test() 520 }}} 521 }}} 522 }}} 350 523 {{{ 351 #!html 352 <pre class="wiki">{{{ 353 #!python 354 class Test: 355 356 def __init__(self): 357 print "Hello World" 358 if __name__ == '__main__': 359 Test() 360 }}}</pre> 361 }}} 362 363 Display: 524 #!td valign="top" style="border: 0px" 525 364 526 {{{ 365 527 #!python … … 371 533 }}} 372 534 373 Perl: 535 }}} 536 |-------------------------------------------------------- 537 {{{#!td align="center" colspan=2 style="border: 0px; font-size: 90%" 538 539 [=#Processors-example-tables Example 3:] Complex Tables 540 541 }}} 542 |-------------------------------------------------------- 543 {{{#!td style="border: 0px" 544 {{{ 545 {{{#!th rowspan=4 align=justify 546 With the `#td` and `#th` processors, 547 table cells can contain any content: 548 }}} 549 |---------------- 550 {{{#!td 551 - lists 552 - embedded tables 553 - simple multiline content 554 }}} 555 |---------------- 556 {{{#!td 557 As processors can be easily nested, 558 so can be tables: 559 {{{#!th 560 Example: 561 }}} 562 {{{#!td style="background: #eef" 563 || must be at the third level now... || 564 }}} 565 }}} 566 |---------------- 567 {{{#!td 568 Even when you don't have complex markup, 569 this form of table cells can be convenient 570 to write content on multiple lines. 571 }}} 572 }}} 573 }}} 374 574 {{{ 375 #!perl 376 my ($test) = 0; 377 if ($test > 0) { 378 print "hello"; 379 } 575 #!td valign="top" style="border: 0px" 576 577 {{{#!th rowspan=4 align=justify 578 With the `#td` and `#th` processors, 579 table cells can contain any content: 580 }}} 581 |---------------- 582 {{{#!td 583 - lists 584 - embedded tables 585 - simple multiline content 586 }}} 587 |---------------- 588 {{{#!td 589 As processors can be easily nested, 590 so can be tables: 591 {{{#!th 592 Example: 593 }}} 594 {{{#!td style="background: #eef" 595 || must be at the third level now... || 596 }}} 597 }}} 598 |---------------- 599 {{{#!td 600 Even when you don't have complex markup, 601 this form of table cells can be convenient 602 to write content on multiple lines. 603 }}} 604 380 605 }}} 381 606 … … 386 611 387 612 Comments can be added to the plain text. These will not be rendered and will not display in any other format than plain text. 388 {{{ 389 {{{ 390 #!comment 391 Your comment here 392 }}} 393 }}} 394 613 614 ||= Wiki Markup =||= Display =|| 615 {{{#!td 616 {{{ 617 Nothing to 618 {{{ 619 #!comment 620 Your comment for editors here 621 }}} 622 see ;-) 623 }}} 624 }}} 625 {{{#!td 626 Nothing to 627 {{{ 628 #!comment 629 Your comment for editors here 630 }}} 631 see ;-) 632 }}} 395 633 396 634 == Miscellaneous == 397 635 398 Four or more dashes will be replaced by a horizontal line (<HR>) 399 400 Example: 401 {{{ 402 ---- 403 }}} 404 405 Display: 636 An horizontal line can be used to separated different parts of your page: 637 638 ||= Wiki Markup =||= Display =|| 639 {{{#!td 640 {{{ 641 Four or more dashes will be replaced 642 by a horizontal line (<HR>) 643 ---- 644 See? 645 }}} 646 }}} 647 {{{#!td 648 Four or more dashes will be replaced 649 by a horizontal line (<HR>) 406 650 ---- 407 651 See? 652 }}} 408 653 409 654 -
trunk/trac/wiki/default-pages/WikiMacros
r8847 r9125 8 8 9 9 == Using Macros == 10 10 11 Macro calls are enclosed in two ''square brackets''. Like Python functions, macros can also have arguments, a comma separated list within parentheses. 11 12 12 Trac macros can also be written as TracPlugins. This gives them some capabilities that macros do not have, such as being able to directly access the HTTP request. 13 === Getting Detailed Help === 14 The list of available macros and the full help can be obtained using the !MacroList macro, as seen [#AvailableMacros below]. 15 16 A brief list can be obtained via ![[MacroList(*)]] or ![[?]]. 17 18 Detailed help on a specific macro can be obtained by passing it as an argument to !MacroList, e.g. ![[MacroList(MacroList)]], or, more conveniently, by appending a question mark (?) to the macro's name, like in ![[MacroList?]]. 19 20 13 21 14 22 === Example === … … 16 24 A list of 3 most recently changed wiki pages starting with 'Trac': 17 25 18 {{{ 19 [[RecentChanges(Trac,3)]] 26 ||= Wiki Markup =||= Display =|| 27 {{{#!td 28 {{{ 29 [[RecentChanges(Trac,3)]] 30 }}} 20 31 }}} 21 22 Display: 23 [[RecentChanges(Trac,3)]] 32 {{{#!td style="padding-left: 2em;" 33 [[RecentChanges(Trac,3)]] 34 }}} 35 |----------------------------------- 36 {{{#!td 37 {{{ 38 [[RecentChanges?(Trac,3)]] 39 }}} 40 }}} 41 {{{#!td style="padding-left: 2em;" 42 [[RecentChanges?(Trac,3)]] 43 }}} 44 |----------------------------------- 45 {{{#!td 46 {{{ 47 [[?]] 48 }}} 49 }}} 50 {{{#!td style="padding-left: 2em; font-size: 80%" 51 [[?]] 52 }}} 24 53 25 54 == Available Macros == … … 34 63 35 64 == Developing Custom Macros == 36 Macros, like Trac itself, are written in the [http://python.org/ Python programming language] .65 Macros, like Trac itself, are written in the [http://python.org/ Python programming language] and are developed as part of TracPlugins. 37 66 38 67 For more information about developing macros, see the [trac:TracDev development resources] on the main project site. 39 68 40 41 == Implementation ==42 69 43 70 Here are 2 simple examples showing how to create a Macro with Trac 0.11. … … 46 73 47 74 === Macro without arguments === 48 It should be saved as `TimeStamp.py` (in the TracEnvironment's `plugins/` directory) as Trac will use the module name as the Macro name.75 To test the following code, you should saved it in a `timestamp_sample.py` file located in the TracEnvironment's `plugins/` directory. 49 76 {{{ 50 77 #!python … … 63 90 url = "$URL$" 64 91 65 def expand_macro(self, formatter, name, args):92 def expand_macro(self, formatter, name, text): 66 93 t = datetime.now(utc) 67 94 return tag.b(format_datetime(t, '%c')) … … 69 96 70 97 === Macro with arguments === 71 It should be saved as `HelloWorld.py` (in the TracEnvironment's `plugins/` directory) as Trac will use the module name as the Macro name.98 To test the following code, you should saved it in a `helloworld_sample.py` file located in the TracEnvironment's `plugins/` directory. 72 99 {{{ 73 100 #!python 101 from genshi.core import Markup 102 74 103 from trac.wiki.macros import WikiMacroBase 75 104 … … 89 118 url = "$URL$" 90 119 91 def expand_macro(self, formatter, name, args):120 def expand_macro(self, formatter, name, text, args): 92 121 """Return some output that will be displayed in the Wiki content. 93 122 94 123 `name` is the actual name of the macro (no surprise, here it'll be 95 124 `'HelloWorld'`), 96 ` args` is the text enclosed in parenthesis at the call of the macro.125 `text` is the text enclosed in parenthesis at the call of the macro. 97 126 Note that if there are ''no'' parenthesis (like in, e.g. 98 [[HelloWorld]]), then `args` is `None`. 127 [[HelloWorld]]), then `text` is `None`. 128 `args` are the arguments passed when HelloWorld is called using a 129 `#!HelloWorld` code block. 99 130 """ 100 return 'Hello World, args = ' + unicode(args) 101 102 # Note that there's no need to HTML escape the returned data, 103 # as the template engine (Genshi) will do it for us. 131 return 'Hello World, text = %s, args = %s' % \ 132 (Markup.escape(text), Markup.escape(repr(args))) 133 104 134 }}} 105 135 136 Note that `expand_macro` optionally takes a 4^th^ parameter ''`args`''. When the macro is called as a [WikiProcessors WikiProcessor], it's also possible to pass `key=value` [WikiProcessors#UsingProcessors processor parameters]. If given, those are stored in a dictionary and passed in this extra `args` parameter (''since 0.12''). 106 137 107 === {{{expand_macro}}} details === 108 {{{expand_macro}}} should return either a simple Python string which will be interpreted as HTML, or preferably a Markup object (use {{{from trac.util.html import Markup}}}). {{{Markup(string)}}} just annotates the string so the renderer will render the HTML string as-is with no escaping. You will also need to import Formatter using {{{from trac.wiki import Formatter}}}. 138 '''FIXME''' when called as a macro, `args` should be `None`. 109 139 110 If your macro creates wiki markup instead of HTML, you can convert it to HTML like this: 140 For example, when writing: 141 {{{ 142 {{{#!HelloWorld style="polite" 143 <Hello World!> 144 }}} 145 146 {{{#!HelloWorld 147 <Hello World!> 148 }}} 149 150 [[HelloWorld(<Hello World!>)]] 151 }}} 152 One should get: 153 {{{ 154 Hello World, text = <Hello World!> , args = {'style': u'polite'} 155 Hello World, text = <Hello World!> , args = {} 156 Hello World, text = <Hello World!> , args = None 157 }}} 158 159 Note that the return value of `expand_macro` is '''not''' HTML escaped. Depending on the expected result, you should escape it by yourself (using `return Markup.escape(result)`) or, if this is indeed HTML, wrap it in a Markup object (`return Markup(result)`) with `Markup` coming from Genshi, (`from genshi.core import Markup`). 160 161 You can also recursively use a wiki Formatter (`from trac.wiki import Formatter`) to process the `text` as wiki markup, for example by doing: 111 162 112 163 {{{ 113 164 #!python 114 text = "whatever wiki markup you want, even containing other macros"115 # Convert Wiki markup to HTML, new style116 out = StringIO()117 Formatter(self.env, formatter.context).format(text, out)118 return Markup(out.getvalue())165 text = "whatever wiki markup you want, even containing other macros" 166 # Convert Wiki markup to HTML, new style 167 out = StringIO() 168 Formatter(self.env, formatter.context).format(text, out) 169 return Markup(out.getvalue()) 119 170 }}} -
trunk/trac/wiki/default-pages/WikiProcessors
r8847 r9125 3 3 Processors are WikiMacros designed to provide alternative markup formats for the [TracWiki Wiki engine]. Processors can be thought of as ''macro functions to process user-edited text''. 4 4 5 The Wiki engine uses processors to allow using [wiki:WikiRestructuredText Restructured Text], [wiki:WikiHtml raw HTML] and [http://www.textism.com/tools/textile/ textile] in any Wiki text throughout Trac. 5 Wiki processors can be used in any Wiki text throughout Trac, 6 for various different purposes, like: 7 - [#CodeHighlightingSupport syntax highlighting] or for rendering text verbatim, 8 - rendering [#HTMLrelated Wiki markup inside a context], 9 like inside <div> blocks or <span> or within <td> or <th> table cells, 10 - using an alternative markup syntax, like [wiki:WikiHtml raw HTML] and 11 [wiki:WikiRestructuredText Restructured Text], 12 or [http://www.textism.com/tools/textile/ textile] 6 13 7 14 8 15 == Using Processors == 9 16 10 To use a processor on a block of text, use a Wiki code block, selecting a processor by name using ''shebang notation'' (#!), familiar to most UNIX users from scripts. 17 To use a processor on a block of text, first delimit the lines using 18 a Wiki ''code block'': 19 {{{ 20 {{{ 21 The lines 22 that should be processed... 23 }}} 24 }}} 11 25 12 '''Example 1''' (''inserting raw HTML in a wiki text''): 26 Immediately after the `{{{` or on the line just below, 27 add `#!` followed by the ''processor name''. 13 28 29 {{{ 30 {{{ 31 #!processorname 32 The lines 33 that should be processed... 34 }}} 35 }}} 36 37 This is the "shebang" notation, familiar to most UNIX users. 38 39 Besides their content, some Wiki processors can also accept ''parameters'', 40 which are then given as `key=value` pairs after the processor name, 41 on the same line. If `value` has to contain space, as it's often the case for 42 the style parameter, a quoted string can be used (`key="value with space"`). 43 44 As some processors are meant to process Wiki markup, it's quite possible to 45 ''nest'' processor blocks. 46 You may want to indent the content of nested blocks for increased clarity, 47 this extra indentation will be ignored when processing the content. 48 49 50 == Examples == 51 52 ||= Wiki Markup =||= Display =|| 53 {{{#!td colspan=2 align=center style="border: none" 54 55 __Example 1__: Inserting raw HTML 56 }}} 57 |----------------------------------------------------------------- 58 {{{#!td style="border: none" 14 59 {{{ 15 60 #!html 16 61 <pre class="wiki">{{{ 17 62 #!html 18 <h1 style="color: orange">This is raw HTML</h1>63 <h1 style="color: grey">This is raw HTML</h1> 19 64 }}}</pre> 20 65 }}} 21 22 '''Results in:''' 66 }}} 67 {{{#!td valign=top style="border: none; padding-left: 2em" 23 68 {{{ 24 69 #!html 25 <h1 style="color: orange">This is raw HTML</h1> 70 <h1 style="color: grey">This is raw HTML</h1> 71 }}} 72 }}} 73 |----------------------------------------------------------------- 74 {{{#!td colspan=2 align=center style="border: none" 75 76 __Example 2__: Highlighted Python code in a <div> block with custom style 77 }}} 78 |----------------------------------------------------------------- 79 {{{#!td style="border: none" 80 {{{ 81 {{{#!div style="background: #ffd; border: 3px ridge" 82 83 This is an example of embedded "code" block: 84 85 {{{ 86 #!python 87 def hello(): 88 return "world" 89 }}} 90 91 }}} 92 }}} 93 }}} 94 {{{#!td valign=top style="border: none; padding: 1em" 95 {{{#!div style="background: #ffd; border: 3px ridge" 96 97 This is an example of embedded "code" block: 98 99 {{{ 100 #!python 101 def hello(): 102 return "world" 103 }}} 104 105 }}} 26 106 }}} 27 107 28 Note that since 0.11, such blocks of HTML have to be self-contained, i.e. you can't start an HTML element in one block and close it later in a second block. Use div or span processors for achieving similar effect (see WikiHtml).29 30 ----31 32 '''Example 2''' (''inserting Restructured Text in wiki text''):33 34 {{{35 #!html36 <pre class="wiki">{{{37 #!rst38 A header39 --------40 This is some **text** with a footnote [*]_.41 42 .. [*] This is the footnote.43 }}}</pre>44 }}}45 46 '''Results in:'''47 {{{48 #!rst49 A header50 --------51 This is some **text** with a footnote [*]_.52 53 .. [*] This is the footnote.54 }}}55 ----56 '''Example 3''' (''inserting a block of C source code in wiki text''):57 58 {{{59 #!html60 <pre class="wiki">{{{61 #!c62 int main(int argc, char *argv[])63 {64 printf("Hello World\n");65 return 0;66 }67 }}}</pre>68 }}}69 70 '''Results in:'''71 {{{72 #!c73 int main(int argc, char *argv[])74 {75 printf("Hello World\n");76 return 0;77 }78 }}}79 80 ----81 108 82 109 == Available Processors == 110 83 111 The following processors are included in the Trac distribution: 84 * '''html''' -- Insert custom HTML in a wiki page. See WikiHtml. 85 * '''div''' -- Wrap an arbitrary Wiki content in a <div> element (''since 0.11''). See WikiHtml. 86 * '''span''' -- Wrap an arbitrary Wiki content in a <span> element (''since 0.11''). See also WikiHtml. 87 * '''htmlcomment''' -- Insert an HTML comment in a wiki page (''since 0.12''). See WikiHtml. 88 * '''rst''' -- Trac support for Restructured Text. See WikiRestructuredText. 89 * '''textile''' -- Supported if [http://cheeseshop.python.org/pypi/textile Textile] is installed. See [http://www.textism.com/tools/textile/ a Textile reference]. 90 * '''comment''' -- Do not process the text in this section (i.e. contents exist only in the plain text - not in the rendered page). 91 * '''diff''' -- Pretty print patches and diffs. 112 113 `#!default` :: Present the text verbatim in a preformatted text block. 114 This is the same as specifying ''no'' processor name 115 (and no `#!`) 116 `#!comment` :: Do not process the text in this section (i.e. contents exist 117 only in the plain text - not in the rendered page). 118 119 === HTML related === 120 121 `#!html` :: Insert custom HTML in a wiki page. 122 `#!htmlcomment` :: Insert an HTML comment in a wiki page (''since 0.12''). 123 124 Note that `#!html` blocks have to be ''self-contained'', 125 i.e. you can't start an HTML element in one block and close it later in a second block. Use the following processors for achieving a similar effect. 126 127 `#!div` :: Wrap an arbitrary Wiki content inside a <div> element 128 (''since 0.11''). 129 `#!span` :: Wrap an arbitrary Wiki content inside a <span> element 130 (''since 0.11''). 131 132 `#!td` :: Wrap an arbitrary Wiki content inside a <td> element (''since 0.12'') 133 `#!th` :: Wrap an arbitrary Wiki content inside a <th> element (''since 0.12'') 134 `#!tr` :: Can optionally be used for wrapping `#!td` and `#!th` blocks, 135 either for specifying row attributes of better visual grouping 136 (''since 0.12'') 137 138 See WikiHtml for more details about these processors. 139 140 === Other Markups === 141 142 `#!rst` :: Trac support for Restructured Text. See WikiRestructuredText. 143 `#!textile` :: Supported if [http://cheeseshop.python.org/pypi/textile Textile] 144 is installed. 145 See [http://www.textism.com/tools/textile/ a Textile reference]. 146 92 147 93 148 === Code Highlighting Support === 94 Trac includes processors to provide inline [wiki:TracSyntaxColoring syntax highlighting] for the following languages:95 * '''c''' -- C96 * '''cpp''' -- C++97 * '''csharp''' --- C# (''use #!text/x-csharp'')98 * '''python''' -- Python99 * '''perl''' -- Perl100 * '''ruby''' -- Ruby101 * '''php''' -- PHP102 * '''asp''' -- ASP103 * '''java''' -- Java104 * '''js''' -- Javascript105 * '''sql''' -- SQL106 * '''xml''' -- XML107 * '''sh''' -- Bourne/Bash shell108 149 109 '''Note:''' ''Trac relies on external software packages for syntax coloring. See TracSyntaxColoring for more info.'' 150 Trac includes processors to provide inline syntax highlighting: 151 `#!c` (C), `#!cpp` (C++), `#!python` (Python), `#!perl` (Perl), 152 `#!ruby` (Ruby), `#!php` (PHP), `#!asp` (ASP), `#!java` (Java), 153 `#!js` (Javascript), `#!sql (SQL)`, `#!xml` (XML or HTML), 154 `#!sh` (Bourne/Bash shell), etc. 110 155 111 By using the MIME type as processor, it is possible to syntax-highlight the same languages that are supported when browsing source code. For example, you can write: 156 Trac relies on external software packages for syntax coloring, 157 like [http://pygments.org Pygments]. 158 159 See TracSyntaxColoring for informations about which languages 160 are supported and how to enable support for more languages. 161 162 Note also that by using the MIME type as processor, it is possible to syntax-highlight the same languages that are supported when browsing source code. For example, you can write: 112 163 {{{ 113 164 {{{ … … 125 176 The same is valid for all other mime types supported. 126 177 127 178 === Additional Processors === 128 179 For more processor macros developed and/or contributed by users, visit: 129 180 * [trac:ProcessorBazaar] … … 131 182 * [th:WikiStart Trac Hacks] community site 132 183 133 134 == Advanced Topics: Developing Processor Macros == 135 Developing processors is no different from Wiki macros. In fact they work the same way, only the usage syntax differs. See WikiMacros for more information.184 Developing processors is no different from Wiki macros. 185 In fact they work the same way, only the usage syntax differs. 186 See WikiMacros#DevelopingCustomMacros for more information. 136 187 137 188
Note:
See TracChangeset
for help on using the changeset viewer.