Ticket #199: diff_module_beta1.diff
| File diff_module_beta1.diff, 38.5 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 tr.diff input { 50 padding: 0 1em 0 1em; 51 margin: 0; 52 } 53 54 #anydiff { 55 background: #f7f7f0; 56 border: 1px outset #998; 57 margin: 0 1em 1em; 58 padding: .8em; 59 float: left; 60 } 61 #anydiff em { 62 background: #fbfbfb; 63 } 64 #anydiff form, #anydiff div { 65 vertical-align: top; 66 display: inline; 67 margin-right: 0; 68 margin-left: 0; 69 } 70 #anydiff input { 71 vertical-align: baseline; 72 } 73 48 74 /* Styles for the revision log table 49 75 (extends the styles for "table.listing") */ 50 76 #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 'neither %s in revision %s nor %s in revision %s 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 ' 321 'a %s (%s in revision %s) ' 322 'with a %s (%s in revision %s).' \ 323 % (old_node.kind, old_path, old_rev, 324 new_node.kind, new_path, new_rev)) 325 if new_node: 326 isdir = new_node.isdir 327 else: 328 isdir = old_node.isdir 329 if isdir: 330 editor = DiffChangeEditor() 331 e_ptr, e_baton = delta.make_editor(editor, self.pool) 332 old_root = fs.revision_root(self.fs_ptr, old_rev, self.pool) 333 new_root = fs.revision_root(self.fs_ptr, new_rev, self.pool) 334 if isdir: 335 old_dir, old_entry = old_path, '' 336 def authz_cb(root, path, pool): return 1 337 text_deltas = 0 # as this is currently re-done in Diff.py... 338 entry_props = 0 # ("... typically used only for working copy updates") 339 print 'svn_repos_dir_delta: ', old_dir, old_entry, ' -vs.- ', new_path 340 repos.svn_repos_dir_delta(old_root, old_path, '', 341 new_root, new_path, 342 e_ptr, e_baton, authz_cb, 343 text_deltas, 344 isdir and 1 or 0, 345 entry_props, 346 ignore_ancestry, 347 self.pool) 348 for d in editor.deltas: 349 yield (posixpath.join(old_path,d[0]), posixpath.join(new_path,d[0]), 350 d[1], d[2]) 351 else: 352 if new_node and old_node: 353 change = Changeset.EDIT 354 elif new_node: 355 change = Changeset.ADD 356 elif old_node: 357 change = Changeset.DELETE 358 yield (old_path, new_path, Node.FILE, change) 297 359 360 298 361 class SubversionNode(Node): 299 362 300 363 pool = property(fget=lambda self: self._pool(), … … 450 513 451 514 def _get_prop(self, name): 452 515 return fs.revision_prop(self.fs_ptr, self.rev, name, self.pool) 516 517 518 # 519 # Delta editor for diffs between arbitrary nodes (recycling my old code for #295 :) ) 520 # 521 # Note 1: the 'copyfrom_path' and 'copyfrom_rev' information is not used 522 # because 'repos.svn_repos_dir_delta' *doesn't* provide it. 523 # 524 # Note 2: the 'dir_baton' is the path of the parent directory 525 # 526 527 class DiffChangeEditor(delta.Editor): 528 529 def __init__(self): 530 self.deltas = [] 531 self.skip_dir_prop_change = 0 532 533 def _norm(self, path): 534 """Path are normalized to __not__ have a leading slash""" 535 return path == '/' and path or path and path.strip('/') or '' 536 537 # -- svn.delta.Editor callbacks 538 539 def open_root(self, base_revision, dir_pool): 540 return '/' 541 542 def add_directory(self, path, dir_baton, copyfrom_path, copyfrom_rev, dir_pool): 543 self.deltas.append((path, Node.DIRECTORY, Changeset.ADD)) 544 # don't create an additional 'Changeset.EDIT' entry for this directory 545 # in case there's also a dir property change: 546 self.skip_dir_prop_change = 1 547 return path 548 549 def open_directory(self, path, dir_baton, base_revision, dir_pool): 550 self.deltas.append((path, Node.DIRECTORY, Changeset.EDIT)) 551 self.skip_dir_prop_change = 0 552 return path 553 554 def change_dir_prop(self, dir_baton, name, value, pool): 555 if self.skip_dir_prop_change: 556 return 557 self.deltas.append((dir_baton, Node.DIRECTORY, Changeset.EDIT)) 558 self.skip_dir_prop_change = 1 559 560 def close_directory(self, dir_baton): 561 self.skip_dir_prop_change = 0 562 563 def delete_entry(self, path, revision, dir_baton, pool): 564 self.deltas.append((path, Node.FILE, Changeset.DELETE)) # should be Node.UNKNOWN 565 566 def add_file(self, path, dir_baton, copyfrom_path, copyfrom_revision, dir_pool): 567 self.deltas.append((self._norm(path), Node.FILE, Changeset.ADD)) 568 569 def open_file(self, path, dir_baton, dummy_rev, file_pool): 570 self.deltas.append((self._norm(path), Node.FILE, Changeset.EDIT)) 571 -
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) 65 if old_path == path and old == new: # force 'path history' mode 66 print "force 'path history' mode" 67 rev = old 68 old_path = old = new = None 55 69 56 70 diff_options = get_diff_options(req) 57 71 if req.args.has_key('update'): 58 req.redirect(self.env.href.changeset(rev)) 72 if old or new: 73 req.redirect(self.env.href.diff(path, new=new, old_path=old_path, old=old)) 74 else: 75 req.redirect(self.env.href.diff(path, rev=rev)) 59 76 60 chgset = repos.get_changeset(rev) 61 req.check_modified(chgset.date, 62 diff_options[0] + ''.join(diff_options[1])) 77 if old or new: 78 chgset = None 79 if not new: 80 new = repos.next_rev(old) # FIXME: must lookup the next entry in node history 81 elif not old: 82 old = repos.previous_rev(new) 83 if not old_path: 84 old_path = path 85 diff = Diff(old_path=old_path, old_rev=old, 86 new_path=path, new_rev=new) 87 diffargs = 'new=%s&old_path=%s&old=%s' \ 88 % (new, old_path, old) 89 else: 90 chgset = repos.get_changeset(rev) 91 diff = Diff(old_path=path, old_rev=repos.previous_rev(rev), 92 new_path=path, new_rev=rev) 93 diffargs = 'rev=%s' % rev 63 94 95 print diff 96 97 # TODO: 98 # req.check_modified(chgset.date, 99 # diff_options[0] + ''.join(diff_options[1])) 100 64 101 format = req.args.get('format') 65 102 if format == 'diff': 66 self._render_diff(req, repos, chgset, diff_options)103 self._render_diff(req, repos, diff, chgset, diff_options) 67 104 return 68 105 elif format == 'zip': 69 self._render_zip(req, repos, chgset)106 self._render_zip(req, repos, diff, chgset) 70 107 return 71 108 72 self._render_html(req, repos, chgset, diff_options)73 add_link(req, 'alternate', '?format=diff ', 'Unified Diff',109 self._render_html(req, repos, diff, chgset, diff_options) 110 add_link(req, 'alternate', '?format=diff&'+diffargs, 'Unified Diff', 74 111 'text/plain', 'diff') 75 add_link(req, 'alternate', '?format=zip ', 'Zip Archive',112 add_link(req, 'alternate', '?format=zip&'+diffargs, 'Zip Archive', 76 113 'application/zip', 'zip') 77 114 add_stylesheet(req, 'changeset.css') 78 115 add_stylesheet(req, 'diff.css') 79 return ' changeset.cs', None116 return 'diff.cs', None 80 117 81 # ITimelineEventProvider methods82 118 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 119 # Internal methods 123 120 124 def _render_html(self, req, repos, chgset, diff_options):121 def _render_html(self, req, repos, diff, chgset, diff_options): 125 122 """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 } 123 req.hdf['diff'] = diff 124 req.hdf['diff.href'] = { 125 'new_rev': self.env.href.changeset(diff.new_rev), 126 'old_rev': self.env.href.changeset(diff.old_rev), 127 'new_path': self.env.href.browser(diff.new_path, rev=diff.new_rev), 128 'old_path': self.env.href.browser(diff.old_path, rev=diff.old_rev) 129 } 130 if chgset: # 'path history' mode 131 req.hdf['title'] = 'Changes for %s at Revision %s' % (diff.new_path, chgset.rev) 132 req.hdf['changeset'] = { 133 'revision': chgset.rev, 134 'time': time.strftime('%c', time.localtime(chgset.date)), 135 'author': util.escape(chgset.author or 'anonymous'), 136 'message': wiki_to_html(chgset.message or '--', self.env, req, 137 escape_newlines=True) 138 } 134 139 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) 140 oldest_rev = repos.oldest_rev 141 if chgset.rev != oldest_rev: 142 add_link(req, 'first', self.env.href.diff(diff.old_path, rev=oldest_rev), 143 'Changeset %s' % oldest_rev) # FIXME (use the history) 144 previous_rev = repos.previous_rev(chgset.rev) 145 add_link(req, 'prev', self.env.href.diff(diff.old_path, rev=previous_rev), 146 'Changeset %s' % previous_rev) 147 youngest_rev = repos.youngest_rev 148 if str(chgset.rev) != str(youngest_rev): 149 next_rev = repos.next_rev(chgset.rev) 150 add_link(req, 'next', self.env.href.diff(diff.new_path, rev=next_rev), 151 'Changeset %s' % next_rev) 152 add_link(req, 'last', self.env.href.diff(diff.new_path, rev=youngest_rev), 153 'Changeset %s' % youngest_rev) 154 elif diff.new_path == diff.old_path: # 'diff between 2 revisions' mode 155 req.hdf['title'] = 'Diff for %s between Revisions %s and %s' \ 156 % (diff.new_path, diff.old_rev, diff.new_rev) 157 else: # 'arbitrary diff' mode 158 req.hdf['title'] = 'Diff between %s at Revision %s and %s at Revision %s' \ 159 % (diff.old_path, diff.old_rev, 160 diff.new_path, diff.new_rev) 149 161 150 162 edits = [] 151 163 idx = 0 152 for path, kind, change, base_path, base_rev in chgset.get_changes(): 164 old_rev = diff.old_rev 165 new_rev = diff.new_rev 166 for old_path, new_path, kind, change in repos.get_diffs(**diff): 167 print 'delta %d: %s %s delta from %s@%s to %s@%s' \ 168 % (idx, change, kind, old_path, old_rev, new_path, new_rev) 153 169 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)170 if old_path: 171 info['path.old'] = old_path 172 info['rev.old'] = old_rev 173 info['browser_href.old'] = self.env.href.browser(old_path, 174 rev=old_rev) 175 if new_path: 176 info['path.new'] = new_path 177 info['rev.new'] = new_rev 178 info['browser_href.new'] = self.env.href.browser(new_path, 179 rev=new_rev) 164 180 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] = info181 edits.append((idx, old_path, new_path, kind)) 182 req.hdf['diff.changes.%d' % idx] = info 167 183 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 184 185 for idx, old_path, new_path, kind in edits: 186 old_node = repos.get_node(old_path, old_rev) 187 new_node = repos.get_node(new_path, new_rev) 188 173 189 # Property changes 174 190 old_props = old_node.get_properties() 175 191 new_props = new_node.get_properties() … … 183 199 for k,v in new_props.items(): 184 200 if not k in old_props: 185 201 changed_props[k] = {'new': v} 186 req.hdf[' changeset.changes.%d.props' % idx] = changed_props202 req.hdf['diff.changes.%d.props' % idx] = changed_props 187 203 188 204 if kind == Node.DIRECTORY: 189 205 continue 190 206 191 207 # Content changes 192 208 default_charset = self.config.get('trac', 'default_charset') 193 old_content = old_node.get_content().read() 209 old_content = old_node.get_content().read() 194 210 if mimeview.is_binary(old_content): 195 211 continue 196 212 charset = mimeview.get_charset(old_node.content_type) or \ … … 217 233 ignore_blank_lines='-B' in diff_options[1], 218 234 ignore_case='-i' in diff_options[1], 219 235 ignore_space_changes='-b' in diff_options[1]) 220 req.hdf[' changeset.changes.%d.diff' % idx] = changes236 req.hdf['diff.changes.%d.diff' % idx] = changes 221 237 222 def _render_diff(self, req, repos, chgset, diff_options): 238 239 def _render_diff(self, req, repos, diff, chgset, diff_options): 223 240 """Raw Unified Diff version""" 224 241 req.send_response(200) 225 242 req.send_header('Content-Type', 'text/plain;charset=utf-8') … … 227 244 'filename=Changeset%s.diff' % req.args.get('rev')) 228 245 req.end_headers() 229 246 230 for path, kind, change, base_path, base_rev in chgset.get_changes(): 247 old_rev = diff.old_rev 248 new_rev = diff.new_rev 249 print diff 250 for old_path, new_path, kind, change in repos.get_diffs(**diff): 251 print '.diff delta : %s %s delta from %s@%s to %s@%s' \ 252 % ( change, kind, old_path, old_rev, new_path, new_rev) 231 253 if change == Changeset.ADD: 232 254 old_node = None 233 255 else: 234 old_node = repos.get_node( base_path or path, base_rev)256 old_node = repos.get_node(old_path, old_rev) 235 257 if change == Changeset.DELETE: 236 258 new_node = None 237 259 else: 238 new_node = repos.get_node( path, chgset.rev)260 new_node = repos.get_node(new_path, new_rev) 239 261 240 262 # TODO: Property changes 241 263 … … 271 293 if option[:2] == '-U': 272 294 context = int(option[2:]) 273 295 break 274 req.write('Index: ' + path + util.CRLF)296 req.write('Index: ' + new_path + util.CRLF) 275 297 req.write('=' * 67 + util.CRLF) 276 298 req.write('--- %s (revision %s)' % old_node_info + 277 299 util.CRLF) … … 284 306 ignore_space_changes='-b' in diff_options[1]): 285 307 req.write(line + util.CRLF) 286 308 287 def _render_zip(self, req, repos, chgset):309 def _render_zip(self, req, repos, diff, chgset): 288 310 """ZIP archive with all the added and/or modified files.""" 311 new_rev = diff.new_rev 289 312 req.send_response(200) 290 313 req.send_header('Content-Type', 'application/zip') 291 314 req.send_header('Content-Disposition', 292 'filename=Changeset%s.zip' % chgset.rev)315 'filename=Changeset%s.zip' % new_rev) 293 316 req.end_headers() 294 317 295 318 try: … … 300 323 301 324 buf = StringIO() 302 325 zipfile = ZipFile(buf, 'w', ZIP_DEFLATED) 303 for path, kind, change, base_path, base_rev in chgset.get_changes():326 for old_path, new_path, kind, change in repos.get_diffs(**diff): 304 327 if kind == Node.FILE and change != Changeset.DELETE: 305 node = repos.get_node( path, chgset.rev)328 node = repos.get_node(new_path, new_rev) 306 329 zipinfo = ZipInfo() 307 330 zipinfo.filename = node.path 308 331 zipinfo.date_time = time.gmtime(node.last_modified)[:6] -
trac/Browser.py
75 75 }) 76 76 return links 77 77 78 def _anydiff_support(env, req, node):
