Edgewall Software

Ticket #295: patch_for_295-1107.diff

File patch_for_295-1107.diff, 35.3 KB (added by cboos@…, 4 years ago)

Copy and Rename support patch, against [1107]

  • htdocs/css/diff.css

     
    2020/* Colors for change types */ 
    2121#overview .mod, .diff #legend .mod { background: #fd8 } 
    2222#overview .rem, .diff #legend .rem { background: #f88 } 
    23 #overview .add, .diff #legend .add { background: #dfd } 
     23#overview .add, .diff #legend .add { background: #bfb } 
     24#overview .cp, .diff #legend .cp { background: #88f } 
     25#overview .mv, .diff #legend .mv { background: #ccc } 
    2426 
    2527/* Legend for diff colors */ 
    2628.diff #legend { 
     
    4648 margin: 0; 
    4749 margin-right: .5em; 
    4850} 
    49  
     51.diff #legend dt.unmod div.mod { 
     52 border: 0; 
     53 margin: 0; 
     54 float: right; 
     55 width: .4em; height: .8em; 
     56} 
    5057/* Styles for the list of diffs */ 
    5158.diff ul.entries { clear: both; margin: 0; padding: 0 } 
    5259.diff li.entry { 
  • htdocs/css/changeset.css

     
    1212 overflow: hidden; 
    1313 width: .8em; height: .8em; 
    1414} 
     15#overview div.add div, #overview div.cp div, #overview div.mv div { 
     16 border: 0; 
     17 margin: 0; 
     18 float: right; 
     19 width: .35em;  
     20} 
     21 
    1522#overview .message { padding: 1em 0 1px } 
    1623#overview dd.message p, #overview dd.message ul, #overview dd.message ol { 
    1724 margin-bottom: 1em; 
  • trac/sync.py

     
    2121 
    2222from svn import fs, util, delta, repos, core 
    2323 
     24import posixpath 
     25 
    2426def sync(db, repos, fs_ptr, pool): 
    2527    """ 
    26     updates the revision and node_change tables to be in sync with 
     28    Update the revision and node_change tables to be in sync with 
    2729    the repository. 
    2830    """ 
    2931 
     
    5961    core.svn_pool_destroy(subpool) 
    6062    db.commit() 
    6163 
    62 def insert_change (pool, fs_ptr, rev, cursor): 
    6364 
     65def insert_change(pool, fs_ptr, rev, cursor): 
     66    """ 
     67    Save node changes for revision 'rev'. 
     68 
     69    Analyse the difference tree at revision 'rev', as given by 
     70    'repos.svn_repos_replay' (which offers usable 'copyfrom_' information). 
     71 
     72    The results are cached in the 'node_change' table, as follows: 
     73 
     74    || rev || action || name                                          || 
     75    || --- || ------ || --------------------------------------------- || 
     76    ||     ||  'A'   || added path                                    || 
     77    ||     ||  'D'   || removed path                                  || 
     78    ||     ||  'M'   || modified path                                 || 
     79    ||     ||  'C'   || original rev, original path // copied path    || 
     80    ||     ||  'R'   || original rev, original path // renamed path   || 
     81    ||     ||  'd'   || original rev, original path // deleted path   || 
     82 
     83    The 'ADM' operations are direct operations. 
     84    The 'CR' operation can be direct operations. 
     85    The 'CRdm' may happen after a 'CR' operation on a parent path. 
     86    """ 
     87 
    6488    class ChangeEditor(delta.Editor): 
    65         def __init__(self, rev, old_root, new_root, cursor): 
     89        def __init__(self, rev, new_root, cursor): 
    6690            self.rev = rev 
    6791            self.cursor = cursor 
    68             self.old_root = old_root 
    6992            self.new_root = new_root 
    70             self.dir_has_prop_change = 0 
     93            self.additions = [] # List of tuples 
     94                                # (file/dir,new_path,old_path,old_rev,action) 
     95            self.deletions = {} # Used to detect rename and copy operations. 
     96            self.skip_dir_prop_change = 0 
     97             
     98        def _norm(self, path): 
     99            if path and path[0] == '/': 
     100                path = path[1:] 
     101            return path 
    71102 
    72         def delete_entry(self, path, revision, parent_baton, pool): 
    73             self.cursor.execute('INSERT INTO node_change (rev, name, change) ' 
    74                                 'VALUES (%s, %s, \'D\')', self.rev, path) 
     103        # -- svn.delta.Editor callbacks 
     104         
     105        # A directory_baton is a tuple (old_path,old_rev,new_path). 
     106        # This information is used to keep track of the original path 
     107        # after a copy or a rename operation on the parent. 
    75108 
    76         def add_directory(self, path, parent_baton, 
    77                           copyfrom_path, copyfrom_revision, dir_pool): 
    78             self.cursor.execute('INSERT INTO node_change (rev, name, change) ' 
    79                                 'VALUES (%s, %s, \'A\')', self.rev, path) 
     109        # -- -- directory 
     110         
     111        def open_root(self, base_revision, dir_pool): 
     112            return (None, None, '/') 
     113        # Note: '/' is needed for proper handling of prop changes at root 
     114        #       (like SVK does for the svm:mirrors property). 
    80115 
    81         def open_directory(self, path, parent_baton, base_revision, dir_pool): 
    82             self.dir_has_prop_change = 0 
    83             return [path, path, dir_pool] 
     116        def add_directory(self, path, dir_baton, copyfrom_path, copyfrom_rev, 
     117                          dir_pool): 
     118            old_path, old_rev = dir_baton[:2] 
     119            if copyfrom_path: # copied or renamed directory 
     120                old_path = self._norm(copyfrom_path) 
     121                old_rev = copyfrom_rev 
     122                action = 'C' 
     123            elif old_path:    # already on a branch, expand the original path 
     124                old_path = posixpath.join(old_path, posixpath.split(path)[1]) 
     125                action = 'A' 
     126            else: 
     127                self._save_change(core.svn_node_file, 'A', path)  
     128                action = None 
    84129 
     130            if action: 
     131                self.additions.append( (core.svn_node_dir, self._norm(path), 
     132                                        old_path, old_rev, action) ) 
     133 
     134            # don't create an additional 'M' entry for this directory 
     135            # in case there's also a dir property change 
     136            self.skip_dir_prop_change = 1  
     137 
     138            return (old_path, old_rev, path) 
     139 
     140        def open_directory(self, path, dir_baton, base_revision, dir_pool): 
     141            old_path, old_rev = dir_baton[:2] 
     142            if old_path: # already on a branch, expand the original path 
     143                old_path = posixpath.join(old_path, posixpath.split(path)[1]) 
     144            self.skip_dir_prop_change = 0 
     145            return (old_path, old_rev, path) 
     146 
    85147        def change_dir_prop(self, dir_baton, name, value, pool): 
    86             if not dir_baton or self.dir_has_prop_change: 
     148            if self.skip_dir_prop_change: 
    87149                return 
    88             self.cursor.execute('INSERT INTO node_change (rev, name, change) ' 
    89                                 'VALUES (%s, %s, \'M\')', self.rev, dir_baton[1]) 
    90             self.dir_has_prop_change = 1 
     150            old_path, old_rev, path = dir_baton 
     151            if old_path: # already on a branch 
     152                self._save_change(core.svn_node_dir, 'm', path, old_path, old_rev) 
     153            else: 
     154                self._save_change(core.svn_node_dir, 'M', path) 
    91155 
     156            self.skip_dir_prop_change = 1 
     157 
    92158        def close_directory(self, dir_baton): 
    93             if not dir_baton: 
    94                 return 
    95             self.dir_has_prop_change = 0 
     159            self.skip_dir_prop_change = 0 
    96160 
    97         def add_file(self, path, parent_baton, 
    98                      copyfrom_path, copyfrom_revision, file_pool): 
    99             self.cursor.execute('INSERT INTO node_change (rev, name, change) ' 
    100                                 'VALUES (%s, %s, \'A\')',self.rev, path) 
     161        def delete_entry(self, path, revision, dir_baton, pool): 
     162            """ 
     163            This is a removed path. It corresponds to one of the 
     164            following actions: 'R'ename, 'D'elete, or 'd'elete on a branch. 
     165            """ 
     166            old_path, old_rev = dir_baton[:2] 
     167            if old_path: # already on a branch, expand the original path 
     168                old_path = posixpath.join(old_path, posixpath.split(path)[1]) 
     169                path_info = (old_path, old_rev) 
     170            else: 
     171                path_info = core.svn_node_unknown 
     172            self.deletions[self._norm(path)] = path_info 
    101173 
    102         def open_file(self, path, parent_baton, base_revision, file_pool): 
     174        # -- -- file 
     175 
     176        def add_file(self, path, dir_baton, copyfrom_path, copyfrom_revision, 
     177                     dir_pool): 
     178            old_path, old_rev = dir_baton[:2] 
     179            if copyfrom_path: # copied or renamed file 
     180                old_path = self._norm(copyfrom_path) 
     181                old_rev = copyfrom_revision 
     182                action = 'C' 
     183            elif old_path: # already on a branch, resolve old_rev later 
     184                old_path = posixpath.join(old_path, posixpath.split(path)[1]) 
     185                old_rev = -1 
     186                action = 'A' 
     187            else: 
     188                return self._save_change(core.svn_node_file, 'A', self._norm(path)) 
     189             
     190            self.additions.append( (core.svn_node_file, self._norm(path), 
     191                                    old_path, old_rev, action) ) 
     192 
     193        def open_file(self, path, dir_baton, dummy_rev, file_pool): 
     194            old_path, old_rev = dir_baton[:2] 
     195            if old_path: # already on a branch (at b_rev) 
     196                old_path = posixpath.join(old_path, posixpath.split(path)[1]) 
     197                # then this is a copy from a file for which f_rev > b_rev 
     198                self.additions.append( (core.svn_node_file, self._norm(path), 
     199                                        old_path, -1, 'C') ) 
     200            else: # no branch, it must be a modification 
     201                self._save_change(core.svn_node_file, 'M', self._norm(path)) 
     202 
     203        def _save_change(self, node_type, action, path, old_path=None, old_rev=None): 
     204            # Note: node_type is ignored for now 
     205            if old_path and old_rev: 
     206                path = "%s // %d, %s" % ( path, old_rev, old_path ) 
    103207            self.cursor.execute('INSERT INTO node_change (rev, name, change) ' 
    104                                 'VALUES (%s, %s, \'M\')',self.rev, path) 
     208                                'VALUES (%s, %s, %s)', self.rev, path, action) 
    105209 
     210        def finalize(self): 
     211            """ 
     212            The rename detection is deferred until the end of edition, 
     213            as 'delete' and 'add' notifications can happen in any order. 
     214            """ 
     215            for node_type, path, old_path, old_rev, action in self.additions: 
     216                if self.deletions.has_key(old_path): # normal rename 
     217                    self.deletions.pop(old_path) 
     218                    action = 'R' 
     219                elif self.deletions.has_key(path):   # copy+modification in a branch 
     220                    action = 'C' 
     221                    # FIXME: actually, this could be a 'R' if the parent branch 
     222                    #        is a 'R'. This can be fixed if the parent branch 
     223                    #        is recorded in addition to the old_path. 
     224                    self.deletions.pop(path) 
     225                if action == 'A':                    # add on a branch 
     226                    old_path = old_rev = None 
     227                self._save_change(node_type, action, path, old_path, old_rev) 
     228            for path, path_info in self.deletions.items():  
     229                if path_info == core.svn_node_unknown: # simple deletion 
     230                    self._save_change(core.svn_node_unknown, 'D', path) 
     231                else:                   # delete on a branch 
     232                    self._save_change(core.svn_node_unknown, 'd', path, *path_info) 
    106233 
    107     old_root = fs.revision_root(fs_ptr, rev - 1, pool) 
     234 
    108235    new_root = fs.revision_root(fs_ptr, rev, pool) 
    109      
    110     editor = ChangeEditor(rev, old_root, new_root, cursor) 
     236 
     237    editor = ChangeEditor(rev, new_root, cursor) 
    111238    e_ptr, e_baton = delta.make_editor(editor, pool) 
    112239 
    113     def authz_cb(root, path, pool): return 1 
    114     repos.svn_repos_dir_delta(old_root, '', '', 
    115                               new_root, '', e_ptr, e_baton, authz_cb, 
    116                               0, 1, 0, 1, pool) 
     240    repos.svn_repos_replay(new_root, e_ptr, e_baton, pool) 
     241 
     242    editor.finalize() # Editor's close_edit not called... 
     243 
     244 
  • trac/Changeset.py

     
    2323import sys 
    2424import time 
    2525import util 
     26import re 
     27import posixpath 
    2628from StringIO import StringIO 
    2729from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED 
    2830 
    2931import svn 
    3032import svn.delta 
    3133import svn.fs 
     34import svn.core 
    3235 
    3336import Diff 
    3437import perm 
     
    4144    Base class for diff renderers. 
    4245    """ 
    4346 
    44     def __init__(self, old_root, new_root, rev, req, args, env): 
     47    def __init__(self, old_root, new_root, rev, req, args, env, path_info): 
     48        self.path_info = path_info 
    4549        self.old_root = old_root 
    4650        self.new_root = new_root 
    4751        self.rev = rev 
     
    4953        self.args = args 
    5054        self.env = env 
    5155 
    52     def open_directory(self, path, parent_baton, base_revision, dir_pool): 
    53         return [path, path, dir_pool] 
     56    # svn.delta.Editor callbacks: 
     57    #   This editor will be driven by a 'repos.svn_repos_dir_delta' call. 
     58    #   With this driver, The 'copyfrom_path' will always be 'None'. 
     59    #   We can't use it. 
     60    #   This is why we merge the path_info data (obtained during a 
     61    #   'repos.svn_repos_replay' call) back into this process. 
     62     
     63    def _retrieve_old_path(self, parent_baton, path, pool): 
     64        old_path = parent_baton[0] 
     65        self.prefix = None 
     66        if self.path_info.has_key(path): # retrieve 'copyfrom_path' info 
     67            seq, old_path = self.path_info[path][:2] 
     68            self.prefix = 'changeset.changes.%d' % seq 
     69        elif old_path:    # already on a branch, expand the original path 
     70            old_path = posixpath.join(old_path, posixpath.split(path)[1]) 
     71        else: 
     72            old_path = path 
     73        return (old_path, path, pool) 
    5474 
     75    def open_root(self, base_revision, dir_pool): 
     76        return self._retrieve_old_path((None, None, None), '/', dir_pool) 
     77     
     78    def open_directory(self, path, dir_baton, base_revision, dir_pool): 
     79        return self._retrieve_old_path(dir_baton, path, dir_pool) 
     80 
    5581    def open_file(self, path, parent_baton, base_revision, file_pool): 
    56         return [path, path, file_pool] 
     82        return self._retrieve_old_path(parent_baton, path, file_pool) 
    5783 
    5884 
    5985class HtmlDiffEditor(BaseDiffEditor): 
     
    6288    the output is written to stdout. 
    6389    """ 
    6490 
    65     def __init__(self, old_root, new_root, rev, req, args, env): 
    66         BaseDiffEditor.__init__(self, old_root, new_root, rev, req, args, env) 
    67         self.prev_path = None 
    68         self.fileno = -1 
     91    def __init__(self, old_root, new_root, rev, req, args, env, change_info): 
     92        BaseDiffEditor.__init__(self, old_root, new_root, rev, req, args, 
     93                                env, change_info) 
    6994        self.prefix = None 
    7095 
    71     def _check_next(self, old_path, new_path, pool): 
    72         if self.prev_path == (old_path or new_path): 
    73             return 
    74  
    75         self.fileno += 1 
    76         self.prev_path = old_path or new_path 
    77  
    78         self.prefix = 'changeset.changes.%d' % (self.fileno) 
    79         if old_path: 
    80             old_rev = svn.fs.node_created_rev(self.old_root, old_path, pool) 
    81             self.req.hdf.setValue('%s.rev.old' % self.prefix, str(old_rev)) 
    82             self.req.hdf.setValue('%s.browser_href.old' % self.prefix, 
    83                                   self.env.href.browser(old_path, old_rev)) 
    84         if new_path: 
    85             new_rev = svn.fs.node_created_rev(self.new_root, new_path, pool) 
    86             self.req.hdf.setValue('%s.rev.new' % self.prefix, str(new_rev)) 
    87             self.req.hdf.setValue('%s.browser_href.new' % self.prefix, 
    88                                   self.env.href.browser(new_path, new_rev)) 
    89  
    9096    def add_directory(self, path, parent_baton, copyfrom_path, 
    9197                      copyfrom_revision, dir_pool): 
    92         self._check_next(None, path, dir_pool) 
     98        return self._retrieve_old_path(parent_baton, path, dir_pool) 
    9399 
     100    def add_file(self, path, parent_baton, copyfrom_path, copyfrom_revision, 
     101                 file_pool): 
     102        return self._retrieve_old_path(parent_baton, path, file_pool) 
     103 
    94104    def delete_entry(self, path, revision, parent_baton, pool): 
    95         self._check_next(path, None, pool) 
     105        old_path = self._retrieve_old_path(parent_baton, path, pool)[0] 
     106        return old_path, None, pool 
    96107 
    97     def change_dir_prop(self, dir_baton, name, value, dir_pool): 
    98         if not dir_baton: 
    99             return 
    100         (old_path, new_path, pool) = dir_baton 
    101         self._check_next(old_path, new_path, dir_pool) 
    102108 
    103         prefix = '%s.props.%s' % (self.prefix, name) 
    104         if old_path: 
    105             old_value = svn.fs.node_prop(self.old_root, old_path, name, dir_pool) 
    106             if old_value: 
    107                 self.req.hdf.setValue(prefix + '.old', old_value) 
    108         if value: 
    109             self.req.hdf.setValue(prefix + '.new', value) 
     109    # -- changes: 
    110110 
    111     def add_file(self, path, parent_baton, copyfrom_path, copyfrom_revision, 
    112                  file_pool): 
    113         self._check_next(None, path, file_pool) 
     111    def _old_root(self, new_path, pool): 
     112        if not new_path: 
     113            return  
     114        old_rev = self.path_info[new_path][2] 
     115        if not old_rev: 
     116            return  
     117        elif old_rev == self.rev - 1: 
     118            return self.old_root 
     119        else: 
     120            return svn.fs.revision_root(svn.fs.root_fs(self.old_root), 
     121                                        old_rev, pool) 
     122         
     123    # -- -- textual changes: 
    114124 
    115125    def apply_textdelta(self, file_baton, base_checksum): 
    116         if not file_baton: 
     126        old_path, new_path, pool = file_baton 
     127        if not self.prefix or not (old_path and new_path): 
    117128            return 
    118         (old_path, new_path, pool) = file_baton 
    119         self._check_next(old_path, new_path, pool) 
    120  
     129        old_root = self._old_root(new_path, pool) 
     130        if not old_root: 
     131            return 
     132         
    121133        # Try to figure out the charset used. We assume that both the old 
    122134        # and the new version uses the same charset, not always the case 
    123135        # but that's all we can do... 
     
    128140        ctpos = mime_type and mime_type.find('charset=') or -1 
    129141        if ctpos >= 0: 
    130142            charset = mime_type[ctpos + 8:] 
    131             self.log.debug("Charset %s selected" % charset) 
    132143        else: 
    133144            charset = self.env.get_config('trac', 'default_charset', 
    134145                                          'iso-8859-15') 
    135146 
    136147        # Start up the diff process 
    137148        options = Diff.get_options(self.env, self.req, self.args, 1) 
    138         differ = svn.fs.FileDiff(self.old_root, old_path, self.new_root, 
    139                                  new_path, pool, options) 
     149        differ = svn.fs.FileDiff(old_root, old_path, 
     150                                 self.new_root, new_path, pool, options) 
    140151        differ.get_files() 
    141152        pobj = differ.get_pipe() 
    142153 
     
    157168                os.waitpid(-1, 0) 
    158169            except OSError: pass 
    159170 
     171    # -- -- property changes: 
     172     
     173    def change_dir_prop(self, dir_baton, name, value, dir_pool): 
     174        self._change_prop(dir_baton, name, value, dir_pool) 
     175 
    160176    def change_file_prop(self, file_baton, name, value, file_pool): 
    161         if not file_baton: 
     177        self._change_prop(file_baton, name, value, file_pool) 
     178 
     179    def _change_prop(self, baton, name, value, pool): 
     180        if not self.prefix: 
    162181            return 
    163         (old_path, new_path, pool) = file_baton 
    164         self._check_next(old_path, new_path, file_pool) 
     182        old_path, new_path, pool = baton 
    165183 
    166184        prefix = '%s.props.%s' % (self.prefix, name) 
    167185        if old_path: 
    168             old_value = svn.fs.node_prop(self.old_root, old_path, name, file_pool) 
    169             if old_value: 
    170                 self.req.hdf.setValue(prefix + '.old', old_value) 
     186            old_root = self._old_root(new_path, pool) 
     187            if old_root: 
     188                old_value = svn.fs.node_prop(old_root, old_path, name, pool) 
     189                if old_value: 
     190                    if value == old_value: 
     191                        return # spurious change prop after a copy 
     192                    self.req.hdf.setValue(prefix + '.old', util.escape(old_value)) 
    171193        if value: 
    172             self.req.hdf.setValue(prefix + '.new', value) 
     194            self.req.hdf.setValue(prefix + '.new', util.escape(value)) 
    173195 
    174196 
    175197class UnifiedDiffEditor(BaseDiffEditor): 
     
    178200    the output is written to stdout. 
    179201    """ 
    180202 
     203    def add_file(self, path, parent_baton, copyfrom_path, copyfrom_revision, 
     204                 file_pool): 
     205        return (None, path, file_pool) 
     206 
     207    def delete_entry(self, path, revision, parent_baton, pool): 
     208        if svn.fs.check_path(self.old_root, path, pool) == svn.core.svn_node_file: 
     209            self.apply_textdelta((path, None, pool),None) 
     210 
    181211    def apply_textdelta(self, file_baton, base_checksum): 
    182212        if not file_baton: 
    183213            return 
     
    193223        differ.get_files() 
    194224        pobj = differ.get_pipe() 
    195225        line = pobj.readline() 
     226        # rewrite 'None' as appropriate ('A' and 'D' support) 
     227        fix_second_line = 0 
     228        if line[:6] != 'Files ' and line[:7] != 'Binary ': 
     229            if old_path == None:            # 'A' 
     230                line = '--- %s %s' % (new_path, line[9:]) 
     231            elif new_path == None:          # 'D'         
     232                fix_second_line = 1 
    196233        while line: 
    197234            self.req.write(line) 
    198235            line = pobj.readline() 
     236            if fix_second_line:         # 'D' 
     237                line = '--- %s %s' % (old_path, line[9:]) 
     238                fix_second_line = 0 
    199239        pobj.close() 
    200240        if sys.platform[:3] != "win" and sys.platform != "os2emx": 
    201241            try: 
     
    208248    Generates a ZIP archive containing the modified and added files. 
    209249    """ 
    210250 
    211     def __init__(self, old_root, new_root, rev, req, args, env): 
    212         BaseDiffEditor.__init__(self, old_root, new_root, rev, req, args, env) 
     251    def __init__(self, old_root, new_root, rev, req, args, env, path_info): 
     252        BaseDiffEditor.__init__(self, old_root, new_root, rev, req, args, 
     253                                env, path_info) 
    213254        self.buffer = StringIO() 
    214255        self.zip = ZipFile(self.buffer, 'w', ZIP_DEFLATED) 
    215256 
     
    256297        row = cursor.fetchone() 
    257298        if not row: 
    258299            raise util.TracError('Changeset %d does not exist.' % rev, 
    259                             'Invalid Changset') 
     300                                 'Invalid Changset') 
    260301        return row 
    261302 
    262     def get_change_info (self, rev): 
     303    def get_change_info(self, rev): 
    263304        cursor = self.db.cursor () 
    264305        cursor.execute ('SELECT name, change FROM node_change ' + 
    265306                        'WHERE rev=%d', rev) 
     
    268309            row = cursor.fetchone() 
    269310            if not row: 
    270311                break 
    271             info.append({'name': row['name'], 
    272                          'change': row['change'], 
    273                          'log_href': self.env.href.log(row['name'])}) 
    274         return info 
     312            change = row['change'] 
     313            name = row['name'] 
     314            if change in 'CRdm': # 'C'opy, 'R'eplace or 'd'elete on a branch 
     315                # the name column contains the encoded ''path_info'' 
     316                # (see _save_change method in sync.py). 
     317                m = re.match('(.*) // (-?\d+), (.*)', name) 
     318                if change == 'd': 
     319                    new_path = None 
     320                else: 
     321                    new_path = m.group(1) 
     322                old_rev = int(m.group(2)) 
     323                if old_rev < 0: 
     324                    old_rev = None 
     325                old_path = m.group(3) 
     326            elif change == 'D':         # 'D'elete 
     327                new_path = None 
     328                old_path = name 
     329                old_rev = None 
     330            elif change == 'A':         # 'A'dd 
     331                new_path = name 
     332                old_path = old_rev = None 
     333            else:                       # 'M'odify 
     334                new_path = old_path = name 
     335                old_rev = None 
     336            if old_path and not old_rev: # 'D' and 'M' 
     337                history = svn.fs.node_history(self.old_root, old_path, self.pool) 
     338                history = svn.fs.history_prev(history, 0, self.pool) # what an API... 
     339                old_rev = svn.fs.history_location(history, self.pool)[1] 
     340                # Note: 'node_created_rev' doesn't work reliably 
     341            key = (new_path or old_path) 
     342            info.append((key, change, new_path, old_path, old_rev)) 
    275343 
     344        info.sort(lambda x,y: cmp(x[0],y[0])) 
     345        self.path_info = {} 
     346        #  path_info is a mapping of paths to sequence number and additional info 
     347        #   'new_path' to '(seq, copyfrom_path, copyfrom_rev)', 
     348        #   'old_path' to '(seq)' 
     349        sinfo = [] 
     350        seq = 0 
     351        for _, change, new_path, old_path, old_rev in info: 
     352            cinfo = { 'name.new': new_path, 
     353                      'name.old': old_path, 
     354                      'log_href': new_path or old_path } 
     355            if new_path: 
     356                self.path_info[new_path] = (seq, old_path, old_rev) 
     357                cinfo['rev.new'] = str(rev) 
     358                cinfo['browser_href.new'] = self.env.href.browser(new_path, rev) 
     359            if old_path: 
     360                cinfo['rev.old'] = str(old_rev) 
     361                cinfo['browser_href.old'] = self.env.href.browser(old_path, old_rev) 
     362            if change in 'CRm': 
     363                cinfo['copyfrom_path'] = old_path 
     364            cinfo['change'] = change.upper() 
     365            cinfo['seq'] = seq 
     366            sinfo.append(cinfo) 
     367            seq += 1 
     368        return sinfo 
     369 
    276370    def render(self): 
    277371        self.perm.assert_permission (perm.CHANGESET_VIEW) 
    278372 
     
    291385        if self.args.has_key('update'): 
    292386            self.req.redirect(self.env.href.changeset(self.rev)) 
    293387 
    294         change_info = self.get_change_info (self.rev) 
    295         changeset_info = self.get_changeset_info (self.rev) 
     388        try: 
     389            self.old_root = svn.fs.revision_root(self.fs_ptr, int(self.rev) - 1, self.pool) 
     390            self.new_root = svn.fs.revision_root(self.fs_ptr, int(self.rev), self.pool) 
     391        except svn.core.SubversionException: 
     392            raise util.TracError('Invalid revision number: %d' % int(self.rev)) 
    296393 
     394        cinfo = self.get_change_info(self.rev) 
     395        changeset_info = self.get_changeset_info(self.rev) 
     396 
    297397        self.req.hdf.setValue('title', '[%d] (changeset)' % self.rev) 
    298398        self.req.hdf.setValue('changeset.time', 
    299399                              time.asctime(time.localtime(int(changeset_info['time']))))