Ticket #295: patch_for_295-1107.diff
| File patch_for_295-1107.diff, 35.3 KB (added by cboos@…, 4 years ago) |
|---|
-
htdocs/css/diff.css
20 20 /* Colors for change types */ 21 21 #overview .mod, .diff #legend .mod { background: #fd8 } 22 22 #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 } 24 26 25 27 /* Legend for diff colors */ 26 28 .diff #legend { … … 46 48 margin: 0; 47 49 margin-right: .5em; 48 50 } 49 51 .diff #legend dt.unmod div.mod { 52 border: 0; 53 margin: 0; 54 float: right; 55 width: .4em; height: .8em; 56 } 50 57 /* Styles for the list of diffs */ 51 58 .diff ul.entries { clear: both; margin: 0; padding: 0 } 52 59 .diff li.entry { -
htdocs/css/changeset.css
12 12 overflow: hidden; 13 13 width: .8em; height: .8em; 14 14 } 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 15 22 #overview .message { padding: 1em 0 1px } 16 23 #overview dd.message p, #overview dd.message ul, #overview dd.message ol { 17 24 margin-bottom: 1em; -
trac/sync.py
21 21 22 22 from svn import fs, util, delta, repos, core 23 23 24 import posixpath 25 24 26 def sync(db, repos, fs_ptr, pool): 25 27 """ 26 updatesthe revision and node_change tables to be in sync with28 Update the revision and node_change tables to be in sync with 27 29 the repository. 28 30 """ 29 31 … … 59 61 core.svn_pool_destroy(subpool) 60 62 db.commit() 61 63 62 def insert_change (pool, fs_ptr, rev, cursor):63 64 65 def 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 64 88 class ChangeEditor(delta.Editor): 65 def __init__(self, rev, old_root,new_root, cursor):89 def __init__(self, rev, new_root, cursor): 66 90 self.rev = rev 67 91 self.cursor = cursor 68 self.old_root = old_root69 92 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 71 102 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. 75 108 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). 80 115 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 84 129 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 85 147 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: 87 149 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) 91 155 156 self.skip_dir_prop_change = 1 157 92 158 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 96 160 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 101 173 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 ) 103 207 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) 105 209 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) 106 233 107 old_root = fs.revision_root(fs_ptr, rev - 1, pool) 234 108 235 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) 111 238 e_ptr, e_baton = delta.make_editor(editor, pool) 112 239 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
23 23 import sys 24 24 import time 25 25 import util 26 import re 27 import posixpath 26 28 from StringIO import StringIO 27 29 from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED 28 30 29 31 import svn 30 32 import svn.delta 31 33 import svn.fs 34 import svn.core 32 35 33 36 import Diff 34 37 import perm … … 41 44 Base class for diff renderers. 42 45 """ 43 46 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 45 49 self.old_root = old_root 46 50 self.new_root = new_root 47 51 self.rev = rev … … 49 53 self.args = args 50 54 self.env = env 51 55 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) 54 74 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 55 81 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) 57 83 58 84 59 85 class HtmlDiffEditor(BaseDiffEditor): … … 62 88 the output is written to stdout. 63 89 """ 64 90 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) 69 94 self.prefix = None 70 95 71 def _check_next(self, old_path, new_path, pool):72 if self.prev_path == (old_path or new_path):73 return74 75 self.fileno += 176 self.prev_path = old_path or new_path77 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 90 96 def add_directory(self, path, parent_baton, copyfrom_path, 91 97 copyfrom_revision, dir_pool): 92 self._check_next(None, path, dir_pool)98 return self._retrieve_old_path(parent_baton, path, dir_pool) 93 99 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 94 104 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 96 107 97 def change_dir_prop(self, dir_baton, name, value, dir_pool):98 if not dir_baton:99 return100 (old_path, new_path, pool) = dir_baton101 self._check_next(old_path, new_path, dir_pool)102 108 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: 110 110 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: 114 124 115 125 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): 117 128 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 121 133 # Try to figure out the charset used. We assume that both the old 122 134 # and the new version uses the same charset, not always the case 123 135 # but that's all we can do... … … 128 140 ctpos = mime_type and mime_type.find('charset=') or -1 129 141 if ctpos >= 0: 130 142 charset = mime_type[ctpos + 8:] 131 self.log.debug("Charset %s selected" % charset)132 143 else: 133 144 charset = self.env.get_config('trac', 'default_charset', 134 145 'iso-8859-15') 135 146 136 147 # Start up the diff process 137 148 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) 140 151 differ.get_files() 141 152 pobj = differ.get_pipe() 142 153 … … 157 168 os.waitpid(-1, 0) 158 169 except OSError: pass 159 170 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 160 176 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: 162 181 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 165 183 166 184 prefix = '%s.props.%s' % (self.prefix, name) 167 185 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)) 171 193 if value: 172 self.req.hdf.setValue(prefix + '.new', value)194 self.req.hdf.setValue(prefix + '.new', util.escape(value)) 173 195 174 196 175 197 class UnifiedDiffEditor(BaseDiffEditor): … … 178 200 the output is written to stdout. 179 201 """ 180 202 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 181 211 def apply_textdelta(self, file_baton, base_checksum): 182 212 if not file_baton: 183 213 return … … 193 223 differ.get_files() 194 224 pobj = differ.get_pipe() 195 225 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 196 233 while line: 197 234 self.req.write(line) 198 235 line = pobj.readline() 236 if fix_second_line: # 'D' 237 line = '--- %s %s' % (old_path, line[9:]) 238 fix_second_line = 0 199 239 pobj.close() 200 240 if sys.platform[:3] != "win" and sys.platform != "os2emx": 201 241 try: … … 208 248 Generates a ZIP archive containing the modified and added files. 209 249 """ 210 250 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) 213 254 self.buffer = StringIO() 214 255 self.zip = ZipFile(self.buffer, 'w', ZIP_DEFLATED) 215 256 … … 256 297 row = cursor.fetchone() 257 298 if not row: 258 299 raise util.TracError('Changeset %d does not exist.' % rev, 259 'Invalid Changset')300 'Invalid Changset') 260 301 return row 261 302 262 def get_change_info (self, rev):303 def get_change_info(self, rev): 263 304 cursor = self.db.cursor () 264 305 cursor.execute ('SELECT name, change FROM node_change ' + 265 306 'WHERE rev=%d', rev) … … 268 309 row = cursor.fetchone() 269 310 if not row: 270 311 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)) 275 343 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 276 370 def render(self): 277 371 self.perm.assert_permission (perm.CHANGESET_VIEW) 278 372 … … 291 385 if self.args.has_key('update'): 292 386 self.req.redirect(self.env.href.changeset(self.rev)) 293 387 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)) 296 393 394 cinfo = self.get_change_info(self.rev) 395 changeset_info = self.get_changeset_info(self.rev) 396 297 397 self.req.hdf.setValue('title', '[%d] (changeset)' % self.rev) 298 398 self.req.hdf.setValue('changeset.time', 299 399 time.asctime(time.localtime(int(changeset_info['time']))))
