1#!/usr/bin/env python 2# 3# svnlook.py : alternative svnlook in Python with library API 4# 5###################################################################### 6# Licensed to the Apache Software Foundation (ASF) under one 7# or more contributor license agreements. See the NOTICE file 8# distributed with this work for additional information 9# regarding copyright ownership. The ASF licenses this file 10# to you under the Apache License, Version 2.0 (the 11# "License"); you may not use this file except in compliance 12# with the License. You may obtain a copy of the License at 13# 14# http://www.apache.org/licenses/LICENSE-2.0 15# 16# Unless required by applicable law or agreed to in writing, 17# software distributed under the License is distributed on an 18# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 19# KIND, either express or implied. See the License for the 20# specific language governing permissions and limitations 21# under the License. 22###################################################################### 23# 24# $HeadURL: https://svn.apache.org/repos/asf/subversion/branches/1.14.x/tools/examples/svnlook.py $ 25# $LastChangedDate: 2013-11-14 11:11:07 +0000 (Thu, 14 Nov 2013) $ 26# $LastChangedRevision: 1541878 $ 27 28""" 29svnlook.py can also be used as a Python module:: 30 31 >>> import svnlook 32 >>> svnlook = svnlook.SVNLook("/testrepo") 33 >>> svnlook.get_author() 34 'randomjoe' 35 36 37Accessible API:: 38 39[x] author 40[x] changed 41[x] date 42[ ] diff 43[x] dirs-changed 44[ ] ids 45[x] info 46[x] log 47[ ] tree 48--- 49[ ] generator API to avoid passing lists 50""" 51 52 53import sys 54import time 55import os 56 57from svn import core, fs, delta, repos 58 59class SVNLook(object): 60 def __init__(self, path, rev=None, txn=None, cmd=None): 61 """ 62 path - path to repository 63 rev - revision number 64 txn - name of transaction (usually the one about to be committed) 65 cmd - if set, specifies cmd_* method to execute 66 67 txn takes precedence over rev; if both are None, inspect the head revision 68 """ 69 path = core.svn_path_canonicalize(path) 70 repos_ptr = repos.open(path) 71 self.fs_ptr = repos.fs(repos_ptr) 72 73 # if set, txn takes precendence 74 if txn: 75 self.txn_ptr = fs.open_txn(self.fs_ptr, txn) 76 else: 77 self.txn_ptr = None 78 if rev is None: 79 rev = fs.youngest_rev(self.fs_ptr) 80 else: 81 rev = int(rev) 82 self.rev = rev 83 84 if cmd != None: 85 getattr(self, 'cmd_' + cmd)() 86 87 def cmd_default(self): 88 self.cmd_info() 89 self.cmd_tree() 90 91 def cmd_author(self): 92 print(self.get_author() or '') 93 94 def cmd_changed(self): 95 for status, path in self.get_changed(): 96 print("%-3s %s" % (status, path)) 97 98 def cmd_date(self): 99 # duplicate original svnlook format, which is 100 # 2010-02-08 21:53:15 +0200 (Mon, 08 Feb 2010) 101 secs = self.get_date(unixtime=True) 102 if secs is None: 103 print("") 104 else: 105 # convert to tuple, detect time zone and format 106 stamp = time.localtime(secs) 107 isdst = stamp.tm_isdst 108 utcoffset = -(time.altzone if (time.daylight and isdst) else time.timezone) // 60 109 110 suffix = "%+03d%02d" % (utcoffset // 60, abs(utcoffset) % 60) 111 outstr = time.strftime('%Y-%m-%d %H:%M:%S ', stamp) + suffix 112 outstr += time.strftime(' (%a, %d %b %Y)', stamp) 113 print(outstr) 114 115 116 def cmd_diff(self): 117 self._print_tree(DiffEditor, pass_root=1) 118 119 def cmd_dirs_changed(self): 120 for dir in self.get_changed_dirs(): 121 print(dir) 122 123 def cmd_ids(self): 124 self._print_tree(Editor, base_rev=0, pass_root=1) 125 126 def cmd_info(self): 127 """print the author, data, log_size, and log message""" 128 self.cmd_author() 129 self.cmd_date() 130 log = self.get_log() or '' 131 print(len(log)) 132 print(log) 133 134 def cmd_log(self): 135 print(self.get_log() or '') 136 137 def cmd_tree(self): 138 self._print_tree(Editor, base_rev=0) 139 140 141 # --- API getters 142 def get_author(self): 143 """return string with the author name or None""" 144 return self._get_property(core.SVN_PROP_REVISION_AUTHOR) 145 146 def get_changed(self): 147 """return list of tuples (status, path)""" 148 ret = [] 149 def list_callback(status, path): 150 ret.append( (status, path) ) 151 self._walk_tree(ChangedEditor, pass_root=1, callback=list_callback) 152 return ret 153 154 def get_date(self, unixtime=False): 155 """return commit timestamp in RFC 3339 format (2010-02-08T20:37:25.195000Z) 156 if unixtime is True, return unix timestamp 157 return None for a txn, or if date property is not set 158 """ 159 if self.txn_ptr: 160 return None 161 162 date = self._get_property(core.SVN_PROP_REVISION_DATE) 163 if not unixtime or date == None: 164 return date 165 166 # convert to unix time 167 aprtime = core.svn_time_from_cstring(date) 168 # ### convert to a time_t; this requires intimate knowledge of 169 # ### the apr_time_t type 170 secs = aprtime / 1000000 # aprtime is microseconds; make seconds 171 return secs 172 173 def get_changed_dirs(self): 174 """return list of changed dirs 175 dir names end with trailing forward slash even on windows 176 """ 177 dirlist = [] 178 def list_callback(item): 179 dirlist.append(item) 180 self._walk_tree(DirsChangedEditor, callback=list_callback) 181 return dirlist 182 183 def get_log(self): 184 """return log message string or None if not present""" 185 return self._get_property(core.SVN_PROP_REVISION_LOG) 186 187 188 # --- Internal helpers 189 def _get_property(self, name): 190 if self.txn_ptr: 191 return fs.txn_prop(self.txn_ptr, name) 192 return fs.revision_prop(self.fs_ptr, self.rev, name) 193 194 def _print_tree(self, e_factory, base_rev=None, pass_root=0): 195 def print_callback(msg): 196 print(msg) 197 self._walk_tree(e_factory, base_rev, pass_root, callback=print_callback) 198 199 # svn fs, delta, repos calls needs review according to DeltaEditor documentation 200 def _walk_tree(self, e_factory, base_rev=None, pass_root=0, callback=None): 201 if base_rev is None: 202 # a specific base rev was not provided. use the transaction base, 203 # or the previous revision 204 if self.txn_ptr: 205 base_rev = fs.txn_base_revision(self.txn_ptr) 206 elif self.rev == 0: 207 base_rev = 0 208 else: 209 base_rev = self.rev - 1 210 211 # get the current root 212 if self.txn_ptr: 213 root = fs.txn_root(self.txn_ptr) 214 else: 215 root = fs.revision_root(self.fs_ptr, self.rev) 216 217 # the base of the comparison 218 base_root = fs.revision_root(self.fs_ptr, base_rev) 219 220 if callback == None: 221 callback = lambda msg: None 222 223 if pass_root: 224 editor = e_factory(root, base_root, callback) 225 else: 226 editor = e_factory(callback=callback) 227 228 # construct the editor for printing these things out 229 e_ptr, e_baton = delta.make_editor(editor) 230 231 # compute the delta, printing as we go 232 def authz_cb(root, path, pool): 233 return 1 234 repos.dir_delta(base_root, '', '', root, '', 235 e_ptr, e_baton, authz_cb, 0, 1, 0, 0) 236 237 238# --------------------------------------------------------- 239# Delta Editors. For documentation see: 240# http://subversion.apache.org/docs/community-guide/#docs 241 242# this one doesn't process delete_entry, change_dir_prop, apply_text_delta, 243# change_file_prop, close_file, close_edit, abort_edit 244# ?set_target_revision 245# need review 246class Editor(delta.Editor): 247 def __init__(self, root=None, base_root=None, callback=None): 248 """callback argument is unused for this editor""" 249 self.root = root 250 # base_root ignored 251 252 self.indent = '' 253 254 def open_root(self, base_revision, dir_pool): 255 print('/' + self._get_id('/')) 256 self.indent = self.indent + ' ' # indent one space 257 258 def add_directory(self, path, *args): 259 id = self._get_id(path) 260 print(self.indent + _basename(path) + '/' + id) 261 self.indent = self.indent + ' ' # indent one space 262 263 # we cheat. one method implementation for two entry points. 264 open_directory = add_directory 265 266 def close_directory(self, baton): 267 # note: if indents are being performed, this slice just returns 268 # another empty string. 269 self.indent = self.indent[:-1] 270 271 def add_file(self, path, *args): 272 id = self._get_id(path) 273 print(self.indent + _basename(path) + id) 274 275 # we cheat. one method implementation for two entry points. 276 open_file = add_file 277 278 def _get_id(self, path): 279 if self.root: 280 id = fs.node_id(self.root, path) 281 return ' <%s>' % fs.unparse_id(id) 282 return '' 283 284# doesn't process close_directory, apply_text_delta, 285# change_file_prop, close_file, close_edit, abort_edit 286# ?set_target_revision 287class DirsChangedEditor(delta.Editor): 288 """print names of changed dirs, callback(dir) is a printer function""" 289 def __init__(self, callback): 290 self.callback = callback 291 292 def open_root(self, base_revision, dir_pool): 293 return [ 1, '' ] 294 295 def delete_entry(self, path, revision, parent_baton, pool): 296 self._dir_changed(parent_baton) 297 298 def add_directory(self, path, parent_baton, 299 copyfrom_path, copyfrom_revision, dir_pool): 300 self._dir_changed(parent_baton) 301 return [ 1, path ] 302 303 def open_directory(self, path, parent_baton, base_revision, dir_pool): 304 return [ 1, path ] 305 306 def change_dir_prop(self, dir_baton, name, value, pool): 307 self._dir_changed(dir_baton) 308 309 def add_file(self, path, parent_baton, 310 copyfrom_path, copyfrom_revision, file_pool): 311 self._dir_changed(parent_baton) 312 313 def open_file(self, path, parent_baton, base_revision, file_pool): 314 # some kind of change is going to happen 315 self._dir_changed(parent_baton) 316 317 def _dir_changed(self, baton): 318 if baton[0]: 319 # the directory hasn't been printed yet. do it. 320 self.callback(baton[1] + '/') 321 baton[0] = 0 322 323class ChangedEditor(delta.Editor): 324 def __init__(self, root, base_root, callback): 325 """callback(status, path) is a printer function""" 326 self.root = root 327 self.base_root = base_root 328 self.callback = callback 329 330 def open_root(self, base_revision, dir_pool): 331 return [ 1, '' ] 332 333 def delete_entry(self, path, revision, parent_baton, pool): 334 ### need more logic to detect 'replace' 335 if fs.is_dir(self.base_root, '/' + path): 336 self.callback('D', path + '/') 337 else: 338 self.callback('D', path) 339 340 def add_directory(self, path, parent_baton, 341 copyfrom_path, copyfrom_revision, dir_pool): 342 self.callback('A', path + '/') 343 return [ 0, path ] 344 345 def open_directory(self, path, parent_baton, base_revision, dir_pool): 346 return [ 1, path ] 347 348 def change_dir_prop(self, dir_baton, name, value, pool): 349 if dir_baton[0]: 350 # the directory hasn't been printed yet. do it. 351 self.callback('_U', dir_baton[1] + '/') 352 dir_baton[0] = 0 353 354 def add_file(self, path, parent_baton, 355 copyfrom_path, copyfrom_revision, file_pool): 356 self.callback('A', path) 357 return [ '_', ' ', None ] 358 359 def open_file(self, path, parent_baton, base_revision, file_pool): 360 return [ '_', ' ', path ] 361 362 def apply_textdelta(self, file_baton, base_checksum): 363 file_baton[0] = 'U' 364 365 # no handler 366 return None 367 368 def change_file_prop(self, file_baton, name, value, pool): 369 file_baton[1] = 'U' 370 371 def close_file(self, file_baton, text_checksum): 372 text_mod, prop_mod, path = file_baton 373 # test the path. it will be None if we added this file. 374 if path: 375 status = text_mod + prop_mod 376 # was there some kind of change? 377 if status != '_ ': 378 self.callback(status.rstrip(), path) 379 380 381class DiffEditor(delta.Editor): 382 def __init__(self, root, base_root, callback=None): 383 """callback argument is unused for this editor""" 384 self.root = root 385 self.base_root = base_root 386 self.target_revision = 0 387 388 def _do_diff(self, base_path, path): 389 if base_path is None: 390 print("Added: " + path) 391 label = path 392 elif path is None: 393 print("Removed: " + base_path) 394 label = base_path 395 else: 396 print("Modified: " + path) 397 label = path 398 print("===============================================================" + \ 399 "===============") 400 args = [] 401 args.append("-L") 402 args.append(label + "\t(original)") 403 args.append("-L") 404 args.append(label + "\t(new)") 405 args.append("-u") 406 differ = fs.FileDiff(self.base_root, base_path, self.root, 407 path, diffoptions=args) 408 pobj = differ.get_pipe() 409 while True: 410 line = pobj.readline() 411 if not line: 412 break 413 sys.stdout.write("%s " % line) 414 print("") 415 416 def _do_prop_diff(self, path, prop_name, prop_val, pool): 417 print("Property changes on: " + path) 418 print("_______________________________________________________________" + \ 419 "_______________") 420 421 old_prop_val = None 422 423 try: 424 old_prop_val = fs.node_prop(self.base_root, path, prop_name, pool) 425 except core.SubversionException: 426 pass # Must be a new path 427 428 if old_prop_val: 429 if prop_val: 430 print("Modified: " + prop_name) 431 print(" - " + str(old_prop_val)) 432 print(" + " + str(prop_val)) 433 else: 434 print("Deleted: " + prop_name) 435 print(" - " + str(old_prop_val)) 436 else: 437 print("Added: " + prop_name) 438 print(" + " + str(prop_val)) 439 440 print("") 441 442 def delete_entry(self, path, revision, parent_baton, pool): 443 ### need more logic to detect 'replace' 444 if not fs.is_dir(self.base_root, '/' + path): 445 self._do_diff(path, None) 446 447 def add_directory(self, path, parent_baton, copyfrom_path, 448 copyfrom_revision, dir_pool): 449 return [ 1, path ] 450 451 def add_file(self, path, parent_baton, 452 copyfrom_path, copyfrom_revision, file_pool): 453 self._do_diff(None, path) 454 return [ '_', ' ', None ] 455 456 def open_root(self, base_revision, dir_pool): 457 return [ 1, '' ] 458 459 def open_directory(self, path, parent_baton, base_revision, dir_pool): 460 return [ 1, path ] 461 462 def open_file(self, path, parent_baton, base_revision, file_pool): 463 return [ '_', ' ', path ] 464 465 def apply_textdelta(self, file_baton, base_checksum): 466 if file_baton[2] is not None: 467 self._do_diff(file_baton[2], file_baton[2]) 468 return None 469 470 def change_file_prop(self, file_baton, name, value, pool): 471 if file_baton[2] is not None: 472 self._do_prop_diff(file_baton[2], name, value, pool) 473 return None 474 475 def change_dir_prop(self, dir_baton, name, value, pool): 476 if dir_baton[1] is not None: 477 self._do_prop_diff(dir_baton[1], name, value, pool) 478 return None 479 480 def set_target_revision(self, target_revision): 481 self.target_revision = target_revision 482 483def _basename(path): 484 "Return the basename for a '/'-separated path." 485 idx = path.rfind('/') 486 if idx == -1: 487 return path 488 return path[idx+1:] 489 490 491def usage(exit): 492 if exit: 493 output = sys.stderr 494 else: 495 output = sys.stdout 496 497 output.write( 498 "usage: %s REPOS_PATH rev REV [COMMAND] - inspect revision REV\n" 499 " %s REPOS_PATH txn TXN [COMMAND] - inspect transaction TXN\n" 500 " %s REPOS_PATH [COMMAND] - inspect the youngest revision\n" 501 "\n" 502 "REV is a revision number > 0.\n" 503 "TXN is a transaction name.\n" 504 "\n" 505 "If no command is given, the default output (which is the same as\n" 506 "running the subcommands `info' then `tree') will be printed.\n" 507 "\n" 508 "COMMAND can be one of: \n" 509 "\n" 510 " author: print author.\n" 511 " changed: print full change summary: all dirs & files changed.\n" 512 " date: print the timestamp (revisions only).\n" 513 " diff: print GNU-style diffs of changed files and props.\n" 514 " dirs-changed: print changed directories.\n" 515 " ids: print the tree, with nodes ids.\n" 516 " info: print the author, data, log_size, and log message.\n" 517 " log: print log message.\n" 518 " tree: print the tree.\n" 519 "\n" 520 % (sys.argv[0], sys.argv[0], sys.argv[0])) 521 522 sys.exit(exit) 523 524def main(): 525 if len(sys.argv) < 2: 526 usage(1) 527 528 rev = txn = None 529 530 args = sys.argv[2:] 531 if args: 532 cmd = args[0] 533 if cmd == 'rev': 534 if len(args) == 1: 535 usage(1) 536 try: 537 rev = int(args[1]) 538 except ValueError: 539 usage(1) 540 del args[:2] 541 elif cmd == 'txn': 542 if len(args) == 1: 543 usage(1) 544 txn = args[1] 545 del args[:2] 546 547 if args: 548 if len(args) > 1: 549 usage(1) 550 cmd = args[0].replace('-', '_') 551 else: 552 cmd = 'default' 553 554 if not hasattr(SVNLook, 'cmd_' + cmd): 555 usage(1) 556 557 SVNLook(sys.argv[1], rev, txn, cmd) 558 559if __name__ == '__main__': 560 main() 561