Ticket #199: diff_module_alpha2.diff
| File diff_module_alpha2.diff, 32.1 KB (added by cboos, 3 years ago) |
|---|
-
htdocs/css/browser.css
45 45 #dirlist td.name a, #dirlist td.rev a { border-bottom: none; display: block } 46 46 #dirlist td.change * { font-size: 9px } 47 47 48 /* Log */ 49 #diff { margin: 0; } 50 48 51 /* Styles for the revision log table 49 52 (extends the styles for "table.listing") */ 50 53 #chglist { margin-top: 0 } -
trac/db_default.py
458 458 459 459 default_components = ('trac.About', 'trac.attachment', 'trac.Browser', 460 460 'trac.Changeset', 'trac.Search', 'trac.Settings', 461 'trac.Diff', 461 462 'trac.ticket.query', 'trac.ticket.report', 462 463 'trac.Roadmap', 463 464 'trac.ticket.web_ui', 'trac.Timeline', 'trac.wiki.web_ui', -
trac/versioncontrol/svn_fs.py
27 27 import os.path 28 28 import time 29 29 import weakref 30 import posixpath 30 31 31 32 from svn import fs, repos, core, delta 32 33 … … 175 176 def __del__(self): 176 177 self.close() 177 178 179 def has_node(self, path, rev): 180 rev_root = fs.revision_root(self.fs_ptr, rev, self.pool) 181 node_type = fs.check_path(rev_root, path, self.pool) 182 return node_type in _kindmap 183 178 184 def normalize_path(self, path): 179 return path == '/' and path or path .strip('/')185 return path == '/' and path or path and path.strip('/') or '' 180 186 181 187 def normalize_rev(self, rev): 182 188 try: … … 267 273 rev = self.normalize_rev(rev) 268 274 expect_deletion = False 269 275 while rev: 270 rev_root = fs.revision_root(self.fs_ptr, rev, self.pool) 271 node_type = fs.check_path(rev_root, path, self.pool) 272 if node_type in _kindmap: # then path exists at that rev 276 if self.has_node(path, rev): 273 277 if expect_deletion: 274 278 # it was missing, now it's there again: rev+1 must be a delete 275 279 yield path, rev+1, Changeset.DELETE … … 294 298 expect_deletion = True 295 299 rev = self.previous_rev(rev) 296 300 301 def get_diffs(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=1): 302 old_node = new_node = None 303 old_rev = self.normalize_rev(old_rev) 304 new_rev = self.normalize_rev(new_rev) 305 if self.has_node(old_path, old_rev): 306 old_node = self.get_node(old_path, old_rev) 307 old_path = old_node.created_path 308 old_rev = old_node.created_rev 309 if self.has_node(new_path, new_rev): 310 new_node = self.get_node(new_path, new_rev) 311 new_path = new_node.created_path 312 new_rev = new_node.created_rev 313 if not old_node and not new_node: 314 raise TracError, ('None of the diff arguments are valid: ' 315 'both %s at %s and %s at %s don\'t exist ' 316 'in the repository' % (old_path, old_rev, 317 new_path, new_rev)) 318 elif old_node and new_node: 319 if new_node.kind != old_node.kind: 320 raise TracError, ('Diff mismatch: Trying to diff a %s (%s at %s) ' 321 'with a %s (%s at %s).' \ 322 % (old_node.kind, old_path, old_rev, 323 new_node.kind, new_path, new_rev)) 324 if new_node: 325 isdir = new_node.isdir 326 else: 327 isdir = old_node.isdir 328 if isdir: 329 old_base = old_path 330 new_base = new_path 331 else: 332 old_base = posixpath.split(old_path)[0] 333 new_base = posixpath.split(new_path)[0] 297 334 335 editor = DiffChangeEditor() 336 e_ptr, e_baton = delta.make_editor(editor, self.pool) 337 old_root = fs.revision_root(self.fs_ptr, old_rev, self.pool) 338 new_root = fs.revision_root(self.fs_ptr, new_rev, self.pool) 339 if isdir: 340 old_dir, old_entry = old_path, '' 341 else: 342 old_dir, old_entry = posixpath.split(old_path) 343 def authz_cb(root, path, pool): return 1 344 text_deltas = 0 # as this is currently re-done in Diff.py... 345 entry_props = 0 # ("... typically used only for working copy updates") 346 repos.svn_repos_dir_delta(old_root, 347 old_dir, old_entry, 348 new_root, new_path, 349 e_ptr, e_baton, authz_cb, 350 text_deltas, 351 isdir and 1 or 0, 352 entry_props, 353 ignore_ancestry, 354 self.pool) 355 for d in editor.deltas: 356 yield (posixpath.join(old_base,d[0]), posixpath.join(new_base,d[0]), 357 d[1], d[2]) 358 359 298 360 class SubversionNode(Node): 299 361 300 362 pool = property(fget=lambda self: self._pool(), … … 450 512 451 513 def _get_prop(self, name): 452 514 return fs.revision_prop(self.fs_ptr, self.rev, name, self.pool) 515 516 517 # 518 # Delta editor for diffs between arbitrary nodes (recycling my old code for #295 :) ) 519 # 520 # Note 1: the 'copyfrom_path' and 'copyfrom_rev' information is not used 521 # because 'repos.svn_repos_dir_delta' *doesn't* provide it. 522 # 523 # Note 2: the 'dir_baton' is the path of the parent directory 524 # 525 526 class DiffChangeEditor(delta.Editor): 527 528 def __init__(self): 529 self.deltas = [] 530 self.skip_dir_prop_change = 0 531 532 def _norm(self, path): 533 """Path are normalized to __not__ have a leading slash""" 534 return path == '/' and path or path and path.strip('/') or '' 535 536 # -- svn.delta.Editor callbacks 537 538 def open_root(self, base_revision, dir_pool): 539 return '/' 540 541 def add_directory(self, path, dir_baton, copyfrom_path, copyfrom_rev, dir_pool): 542 self.deltas.append((path, Node.DIRECTORY, Changeset.ADD)) 543 # don't create an additional 'Changeset.EDIT' entry for this directory 544 # in case there's also a dir property change: 545 self.skip_dir_prop_change = 1 546 return path 547 548 def open_directory(self, path, dir_baton, base_revision, dir_pool): 549 self.deltas.append((path, Node.DIRECTORY, Changeset.EDIT)) 550 self.skip_dir_prop_change = 0 551 return path 552 553 def change_dir_prop(self, dir_baton, name, value, pool): 554 if self.skip_dir_prop_change: 555 return 556 self.deltas.append((dir_baton, Node.DIRECTORY, Changeset.EDIT)) 557 self.skip_dir_prop_change = 1 558 559 def close_directory(self, dir_baton): 560 self.skip_dir_prop_change = 0 561 562 def delete_entry(self, path, revision, dir_baton, pool): 563 self.deltas.append((path, Node.FILE, Changeset.DELETE)) # should be Node.UNKNOWN 564 565 def add_file(self, path, dir_baton, copyfrom_path, copyfrom_revision, dir_pool): 566 self.deltas.append((self._norm(path), Node.FILE, Changeset.ADD)) 567 568 def open_file(self, path, dir_baton, dummy_rev, file_pool): 569 self.deltas.append((self._norm(path), Node.FILE, Changeset.EDIT)) 570 -
trac/versioncontrol/cache.py
88 88 def get_node(self, path, rev=None): 89 89 return self.repos.get_node(path, rev) 90 90 91 def has_node(self, path, rev): 92 return self.repos.has_node(path, rev) 93 91 94 def get_oldest_rev(self): 92 95 return self.repos.oldest_rev 93 96 … … 112 115 def normalize_rev(self, rev): 113 116 return self.repos.normalize_rev(rev) 114 117 118 def get_diffs(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=1): 119 return self.repos.get_diffs(old_path, old_rev, new_path, new_rev, ignore_ancestry) 115 120 121 116 122 class CachedChangeset(Changeset): 117 123 118 124 def __init__(self, rev, db, authz): -
trac/versioncontrol/main.py
42 42 """ 43 43 raise NotImplementedError 44 44 45 def has_node(self, path, rev): 46 """ 47 Tell if there's a node at the specified (path,rev) combination. 48 """ 49 raise NotImplementedError 50 45 51 def get_node(self, path, rev=None): 46 52 """ 47 53 Retrieve a Node (directory or file) from the repository at the … … 114 120 'None' is a valid revision value and represents the youngest revision. 115 121 """ 116 122 return NotImplementedError 117 118 123 124 def get_diffs(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=1): 125 """ 126 Generator that yields (old_path, new_path, kind, change) tuples 127 for each node change between the two arbitrary (path,rev) pairs. 128 """ 129 raise NotImplementedError 130 131 119 132 class Node(object): 120 133 """ 121 134 Represents a directory or file in the repository. -
trac/Diff.py
24 24 from __future__ import generators 25 25 import time 26 26 import re 27 import posixpath 27 28 28 29 from trac import mimeview, perm, util 29 30 from trac.core import * 30 from trac.Timeline import ITimelineEventProvider31 31 from trac.versioncontrol import Changeset, Node 32 32 from trac.versioncontrol.diff import get_diff_options, hdf_diff, unified_diff 33 33 from trac.web.chrome import add_link, add_stylesheet … … 35 35 from trac.wiki import wiki_to_html, wiki_to_oneliner 36 36 37 37 38 class ChangesetModule(Component): 38 class Diff(dict): 39 def __getattr__(self,str): 40 return self[str] 41 39 42 40 implements(IRequestHandler, ITimelineEventProvider) 43 class DiffModule(Component): 41 44 45 implements(IRequestHandler) 46 42 47 # IRequestHandler methods 43 48 44 49 def match_request(self, req): 45 match = re.match(r'/ changeset/([0-9]+)$', req.path_info)50 match = re.match(r'/diff(?:(/.*)|$)', req.path_info) 46 51 if match: 47 req.args[' rev'] = match.group(1)52 req.args['path'] = match.group(1) 48 53 return 1 49 54 50 55 def process_request(self, req): 51 56 req.perm.assert_permission(perm.CHANGESET_VIEW) 52 57 53 rev = req.args.get('rev')58 path = req.args.get('path') 54 59 repos = self.env.get_repository(req.authname) 60 path = repos.normalize_path(path) 61 rev = req.args.get('rev', repos.youngest_rev) # 'path history' mode 62 old = req.args.get('old') # 'arbitrary diff' mode 63 new = req.args.get('new') 64 old_path = req.args.get('old_path', path) 55 65 56 66 diff_options = get_diff_options(req) 57 67 if req.args.has_key('update'): 58 req.redirect(self.env.href.changeset(rev)) 68 if old or new: 69 req.redirect(self.env.href.diff(path, new=new, old_path=old_path, old=old)) 70 else: 71 req.redirect(self.env.href.diff(path, rev=rev)) 59 72 60 chgset = repos.get_changeset(rev) 61 req.check_modified(chgset.date, 62 diff_options[0] + ''.join(diff_options[1])) 73 if old or new: 74 chgset = None 75 if not new: 76 new = repos.next_rev(old) # FIXME: must lookup the next entry in node history 77 elif not old: 78 old = repos.previous_rev(new) 79 if not old_path: 80 old_path = path 81 diff = Diff(old_path=old_path, old_rev=old, 82 new_path=path, new_rev=new) 83 else: 84 chgset = repos.get_changeset(rev) 85 diff = Diff(old_path=path, old_rev=repos.previous_rev(rev), 86 new_path=path, new_rev=rev) 87 88 # TODO: 89 # req.check_modified(chgset.date, 90 # diff_options[0] + ''.join(diff_options[1])) 63 91 64 92 format = req.args.get('format') 65 93 if format == 'diff': 66 self._render_diff(req, repos, chgset, diff_options)94 self._render_diff(req, repos, diff, chgset, diff_options) 67 95 return 68 96 elif format == 'zip': 69 self._render_zip(req, repos, chgset)97 self._render_zip(req, repos, diff, chgset) 70 98 return 71 99 72 self._render_html(req, repos, chgset, diff_options)100 self._render_html(req, repos, diff, chgset, diff_options) 73 101 add_link(req, 'alternate', '?format=diff', 'Unified Diff', 74 102 'text/plain', 'diff') 75 103 add_link(req, 'alternate', '?format=zip', 'Zip Archive', 76 104 'application/zip', 'zip') 77 105 add_stylesheet(req, 'changeset.css') 78 106 add_stylesheet(req, 'diff.css') 79 return ' changeset.cs', None107 return 'diff.cs', None 80 108 81 # ITimelineEventProvider methods82 109 83 def get_timeline_filters(self, req):84 if req.perm.has_permission(perm.CHANGESET_VIEW):85 yield ('changeset', 'Repository checkins')86 87 def get_timeline_events(self, req, start, stop, filters):88 if 'changeset' in filters:89 absurls = req.args.get('format') == 'rss' # Kludge90 show_files = int(self.config.get('timeline',91 'changeset_show_files'))92 db = self.env.get_db_cnx()93 repos = self.env.get_repository()94 rev = repos.youngest_rev95 while rev:96 chgset = repos.get_changeset(rev)97 if chgset.date < start:98 return99 if chgset.date < stop:100 if absurls:101 href = self.env.abs_href.changeset(chgset.rev)102 else:103 href = self.env.href.changeset(chgset.rev)104 title = 'Changeset <em>[%s]</em> by %s' % (105 util.escape(chgset.rev), util.escape(chgset.author))106 message = wiki_to_oneliner(util.shorten_line(chgset.message or '--'),107 self.env, db, absurls=absurls)108 if show_files:109 files = []110 for chg in chgset.get_changes():111 if show_files > 0 and len(files) >= show_files:112 files.append('...')113 break114 files.append('<span class="%s">%s</span>'115 % (chg[2], util.escape(chg[0])))116 message = '<span class="changes">' + ', '.join(files) +\117 '</span>: ' + message118 yield 'changeset', href, title, chgset.date, chgset.author,\119 message120 rev = repos.previous_rev(rev)121 122 110 # Internal methods 123 111 124 def _render_html(self, req, repos, chgset, diff_options):112 def _render_html(self, req, repos, diff, chgset, diff_options): 125 113 """HTML version""" 126 req.hdf['title'] = '[%s]' % chgset.rev 127 req.hdf['changeset'] = { 128 'revision': chgset.rev, 129 'time': time.strftime('%c', time.localtime(chgset.date)), 130 'author': util.escape(chgset.author or 'anonymous'), 131 'message': wiki_to_html(chgset.message or '--', self.env, req, 132 escape_newlines=True) 133 } 114 req.hdf['diff'] = diff 115 req.hdf['diff.href'] = { 116 'new_rev': self.env.href.changeset(diff.new_rev), 117 'old_rev': self.env.href.changeset(diff.old_rev), 118 'new_path': self.env.href.browser(diff.new_path, rev=diff.new_rev), 119 'old_path': self.env.href.changeset(diff.old_path, rev=diff.old_rev) 120 } 121 if chgset: # 'path history' mode 122 req.hdf['title'] = 'Changes for %s at Revision %s' % (diff.new_path, chgset.rev) 123 req.hdf['changeset'] = { 124 'revision': chgset.rev, 125 'time': time.strftime('%c', time.localtime(chgset.date)), 126 'author': util.escape(chgset.author or 'anonymous'), 127 'message': wiki_to_html(chgset.message or '--', self.env, req, 128 escape_newlines=True) 129 } 134 130 135 oldest_rev = repos.oldest_rev 136 if chgset.rev != oldest_rev: 137 add_link(req, 'first', self.env.href.changeset(oldest_rev), 138 'Changeset %s' % oldest_rev) 139 previous_rev = repos.previous_rev(chgset.rev) 140 add_link(req, 'prev', self.env.href.changeset(previous_rev), 141 'Changeset %s' % previous_rev) 142 youngest_rev = repos.youngest_rev 143 if str(chgset.rev) != str(youngest_rev): 144 next_rev = repos.next_rev(chgset.rev) 145 add_link(req, 'next', self.env.href.changeset(next_rev), 146 'Changeset %s' % next_rev) 147 add_link(req, 'last', self.env.href.changeset(youngest_rev), 148 'Changeset %s' % youngest_rev) 131 oldest_rev = repos.oldest_rev 132 if chgset.rev != oldest_rev: 133 add_link(req, 'first', self.env.href.diff(diff.old_path, rev=oldest_rev), 134 'Changeset %s' % oldest_rev) # FIXME (use the history) 135 previous_rev = repos.previous_rev(chgset.rev) 136 add_link(req, 'prev', self.env.href.diff(diff.old_path, rev=previous_rev), 137 'Changeset %s' % previous_rev) 138 youngest_rev = repos.youngest_rev 139 if str(chgset.rev) != str(youngest_rev): 140 next_rev = repos.next_rev(chgset.rev) 141 add_link(req, 'next', self.env.href.diff(diff.new_path, rev=next_rev), 142 'Changeset %s' % next_rev) 143 add_link(req, 'last', self.env.href.diff(diff.new_path, rev=youngest_rev), 144 'Changeset %s' % youngest_rev) 145 elif diff.new_path == diff.old_path: # 'diff between 2 revisions' mode 146 req.hdf['title'] = 'Diff for %s between Revisions %s and %s' \ 147 % (diff.new_path, diff.old_rev, diff.new_rev) 148 else: # 'arbitrary diff' mode 149 req.hdf['title'] = 'Diff between %s at Revision %s and %s at Revision %s' \ 150 % (diff.old_path, diff.old_rev, 151 diff.new_path, diff.new_rev) 149 152 150 153 edits = [] 151 154 idx = 0 152 for path, kind, change, base_path, base_rev in chgset.get_changes(): 155 old_rev = diff.old_rev 156 new_rev = diff.new_rev 157 for old_path, new_path, kind, change in repos.get_diffs(**diff): 158 print 'delta %d: %s %s delta from %s@%s to %s@%s' \ 159 % (idx, change, kind, old_path, old_rev, new_path, new_rev) 153 160 info = {'change': change} 154 if base_path:155 info['path.old'] = base_path156 info['rev.old'] = base_rev157 info['browser_href.old'] = self.env.href.browser( base_path,158 rev= base_rev)159 if path:160 info['path.new'] = path161 info['rev.new'] = chgset.rev162 info['browser_href.new'] = self.env.href.browser( path,163 rev= chgset.rev)161 if old_path: 162 info['path.old'] = old_path 163 info['rev.old'] = old_rev 164 info['browser_href.old'] = self.env.href.browser(old_path, 165 rev=old_rev) 166 if new_path: 167 info['path.new'] = new_path 168 info['rev.new'] = new_rev 169 info['browser_href.new'] = self.env.href.browser(new_path, 170 rev=new_rev) 164 171 if change in (Changeset.COPY, Changeset.EDIT, Changeset.MOVE): 165 edits.append((idx, path, kind, base_path, base_rev))166 req.hdf[' changeset.changes.%d' % idx] = info172 edits.append((idx, old_path, new_path, kind)) 173 req.hdf['diff.changes.%d' % idx] = info 167 174 idx += 1 168 169 for idx, path, kind, base_path, base_revin edits:170 old_node = repos.get_node( base_path or path, base_rev)171 new_node = repos.get_node( path, chgset.rev)172 175 176 for idx, old_path, new_path, kind in edits: 177 old_node = repos.get_node(old_path, old_rev) 178 new_node = repos.get_node(new_path, new_rev) 179 173 180 # Property changes 174 181 old_props = old_node.get_properties() 175 182 new_props = new_node.get_properties() … … 183 190 for k,v in new_props.items(): 184 191 if not k in old_props: 185 192 changed_props[k] = {'new': v} 186 req.hdf[' changeset.changes.%d.props' % idx] = changed_props193 req.hdf['diff.changes.%d.props' % idx] = changed_props 187 194 188 195 if kind == Node.DIRECTORY: 189 196 continue 190 197 191 198 # Content changes 192 199 default_charset = self.config.get('trac', 'default_charset') 193 old_content = old_node.get_content().read() 200 old_content = old_node.get_content().read() 194 201 if mimeview.is_binary(old_content): 195 202 continue 196 203 charset = mimeview.get_charset(old_node.content_type) or \ … … 217 224 ignore_blank_lines='-B' in diff_options[1], 218 225 ignore_case='-i' in diff_options[1], 219 226 ignore_space_changes='-b' in diff_options[1]) 220 req.hdf[' changeset.changes.%d.diff' % idx] = changes227 req.hdf['diff.changes.%d.diff' % idx] = changes 221 228 222 def _render_diff(self, req, repos, chgset, diff_options): 229 230 def _render_diff(self, req, repos, chgset, diff_options): # TODO 223 231 """Raw Unified Diff version""" 224 232 req.send_response(200) 225 233 req.send_header('Content-Type', 'text/plain;charset=utf-8') … … 284 292 ignore_space_changes='-b' in diff_options[1]): 285 293 req.write(line + util.CRLF) 286 294 287 def _render_zip(self, req, repos, chgset): 295 def _render_zip(self, req, repos, chgset): # TODO 288 296 """ZIP archive with all the added and/or modified files.""" 289 297 req.send_response(200) 290 298 req.send_header('Content-Type', 'application/zip') -
trac/Browser.py
118 118 'props': dict([(util.escape(name), util.escape(value)) 119 119 for name, value in node.get_properties().items()]), 120 120 'href': self.env.href.browser(path,rev=rev or repos.youngest_rev), 121 'diff_href': self.env.href.diff(path,rev=rev or repos.youngest_rev), 121 122 'log_href': self.env.href.log(path) 122 123 } 123 124 … … 268 269 stop_rev = req.args.get('stop_rev') 269 270 verbose = req.args.get('verbose') 270 271 limit = int(req.args.get('limit') or 100) 272 diff = req.args.get('diff') 273 old = req.args.get('old') 274 new = req.args.get('new') 271 275 276 repos = self.env.get_repository(req.authname) 277 normpath = repos.normalize_path(path) 278 rev = str(repos.normalize_rev(rev)) 279 old = old or str(repos.previous_rev(rev)) 280 new = new or rev 281 272 282 req.hdf['title'] = path + ' (log)' 273 283 req.hdf['log'] = { 274 284 'path': path, … … 276 286 'verbose': verbose, 277 287 'stop_rev': stop_rev, 278 288 'browser_href': self.env.href.browser(path, rev=rev), 279 'log_href': self.env.href.log(path, rev=rev) 289 'log_href': self.env.href.log(path, rev=rev), 290 'select_diff_href': self.env.href.log(path, rev=rev, diff=1), 291 'diff': diff, 292 'diff_href': self.env.href.diff(path, old=old, new=new), 293 'old': old, 294 'new': new 280 295 } 281 296 282 297 path_links = _get_path_links(self.env.href, path, rev) … … 284 299 if path_links: 285 300 add_link(req, 'up', path_links[-1]['href'], 'Parent directory') 286 301 287 repos = self.env.get_repository(req.authname)288 normpath = repos.normalize_path(path)289 rev = str(repos.normalize_rev(rev))290 291 302 # 'node' or 'path' history: use get_node()/get_history() or get_path_history() 292 303 if mode != 'path_history': 293 304 try: -
templates/log.cs
2 2 <?cs include "macros.cs"?> 3 3 4 4 <div id="ctxtnav" class="nav"> 5 <ul> 6 <li class="last"><a href="<?cs 7 var:log.browser_href ?>">View Latest Revision</a></li><?cs 5 <ul><?cs 6 if:!log.diff ?> 7 <li class="first"> 8 <a href="<?cs var:log.select_diff_href ?>">Select for Diff</a> 9 </li><?cs 10 /if ?> 11 <li class="last"> 12 <a href="<?cs var:log.browser_href ?>">View Latest Revision</a> 13 </li><?cs 8 14 if:len(chrome.links.prev) ?> 9 15 <li class="first<?cs if:!len(chrome.links.next) ?> last<?cs /if ?>"> 10 16 ← <a href="<?cs var:chrome.links.prev.0.href ?>" title="<?cs … … 74 80 </dl> 75 81 </div> 76 82 </div> 77 <table id="chglist" class="listing"> 78 <thead> 79 <tr> 83 <table id="chglist" class="listing"><?cs 84 if:log.diff ?> 85 <form action="<?cs var:log.diff_href ?>" method="get"><?cs 86 /if ?> 87 <thead><?cs 88 if:log.diff ?> 89 <tr> 90 <th colspan="2"> 91 <div class="buttons"> 92 <input type="submit" id="diff" value="Diff" 93 title="Diff from Old Revision to New Revision (select them below)" /> 94 </div> 95 </th> 96 </tr><?cs 97 /if ?> 98 <tr><?cs 99 if:log.diff ?> 100 <th>Old</th> 101 <th>New</th><?cs 102 /if ?> 80 103 <th class="change"></th> 81 104 <th class="data">Date</th> 82 105 <th class="rev">Rev</th>
