--- svnlook.py.1 2012-05-28 19:17:16.034482394 +0300 +++ svnlook.py 2012-05-28 19:15:41.868237664 +0300 @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# svnlook.py : a Python-based replacement for svnlook +# svnlook.py : alternative svnlook in Python with library API # ###################################################################### # Licensed to the Apache Software Foundation (ASF) under one @@ -22,87 +22,175 @@ ###################################################################### # +__version__ = '2012.05.24' + +""" +svnlook.py can also be used as a Python module:: + + >>> import svnlook + >>> svnlook = svnlook.SVNLook("/testrepo") + >>> svnlook.get_author() + 'randomjoe' + + +Accessible API:: + +[x] author +[x] changed +[x] date +[ ] diff +[x] dirs-changed +[ ] ids +[x] info +[x] log +[ ] tree +--- +[ ] generator API to avoid passing lists +""" + + import sys import time import os from svn import core, fs, delta, repos -class SVNLook: - def __init__(self, path, cmd, rev, txn): +class SVNLook(object): + def __init__(self, path, rev=None, txn=None, cmd=None): + """ + path - path to repository + rev - revision number + txn - path to transaction (usually to the one about to be committed) + cmd - if set, specifies cmd_* method to execute + + if both rev and txn params are empty, inspect latest committed revision + """ path = core.svn_path_canonicalize(path) repos_ptr = repos.open(path) self.fs_ptr = repos.fs(repos_ptr) + # if set, txn takes precendence if txn: self.txn_ptr = fs.open_txn(self.fs_ptr, txn) else: self.txn_ptr = None - if rev is None: - rev = fs.youngest_rev(self.fs_ptr) - self.rev = rev + if rev is None: + rev = fs.youngest_rev(self.fs_ptr) + self.rev = int(rev) - getattr(self, 'cmd_' + cmd)() + if cmd != None: + getattr(self, 'cmd_' + cmd)() def cmd_default(self): self.cmd_info() self.cmd_tree() def cmd_author(self): - # get the author property, or empty string if the property is not present - author = self._get_property(core.SVN_PROP_REVISION_AUTHOR) or '' - print(author) + print(self.get_author() or '') def cmd_changed(self): - self._print_tree(ChangedEditor, pass_root=1) + for status, path in self.get_changed(): + print("%-3s %s" % (status, path)) def cmd_date(self): - if self.txn_ptr: - print("") - else: - date = self._get_property(core.SVN_PROP_REVISION_DATE) - if date: - aprtime = core.svn_time_from_cstring(date) - # ### convert to a time_t; this requires intimate knowledge of - # ### the apr_time_t type - secs = aprtime / 1000000 # aprtime is microseconds; make seconds - - # assume secs in local TZ, convert to tuple, and format - ### we don't really know the TZ, do we? - print(time.strftime('%Y-%m-%d %H:%M', time.localtime(secs))) - else: - print("") + # duplicate original svnlook format, which is + # 2010-02-08 21:53:15 +0200 (Mon, 08 Feb 2010) + secs = self.get_date(unixtime=True) + # convert to tuple, detect time zone and format + stamp = time.localtime(secs) + isdst = stamp.tm_isdst + utcoffset = -(time.altzone if (time.daylight and isdst) else time.timezone) // 60 + + suffix = "%+03d%02d" % (utcoffset // 60, abs(utcoffset) % 60) + outstr = time.strftime('%Y-%m-%d %H:%M:%S ', stamp) + suffix + outstr += time.strftime(' (%a, %d %b %Y)', stamp) + print(outstr) def cmd_diff(self): self._print_tree(DiffEditor, pass_root=1) def cmd_dirs_changed(self): - self._print_tree(DirsChangedEditor) + for dir in self.get_changed_dirs(): + print(dir) def cmd_ids(self): self._print_tree(Editor, base_rev=0, pass_root=1) def cmd_info(self): + """print the author, data, log_size, and log message""" self.cmd_author() self.cmd_date() - self.cmd_log(1) + log = self.get_log() or '' + print(len(log)) + print(log) - def cmd_log(self, print_size=0): - # get the log property, or empty string if the property is not present - log = self._get_property(core.SVN_PROP_REVISION_LOG) or '' - if print_size: - print(len(log)) - print(log) + def cmd_log(self): + print(self.get_log() or '') def cmd_tree(self): self._print_tree(Editor, base_rev=0) + + # --- API getters + def get_author(self): + """return string with the author name or None""" + return self._get_property(core.SVN_PROP_REVISION_AUTHOR) + + def get_changed(self): + """return list of tuples (status, path)""" + ret = [] + def list_callback(status, path): + ret.append( (status, path) ) + self._walk_tree(ChangedEditor, pass_root=1, callback=list_callback) + return ret + + def get_date(self, unixtime=False): + """return commit timestamp in RFC 3339 format (2010-02-08T20:37:25.195000Z) + if unixtime is True, return unix timestamp + return None if date is not set (txn properties) + """ + if self.txn_ptr: + return None + + date = self._get_property(core.SVN_PROP_REVISION_DATE) + if not unixtime or date == None: + return date + + # convert to unix time + aprtime = core.svn_time_from_cstring(date) + # ### convert to a time_t; this requires intimate knowledge of + # ### the apr_time_t type + secs = aprtime / 1000000 # aprtime is microseconds; make seconds + return secs + + def get_changed_dirs(self): + """return list of changed dirs + dir names end with trailing forward slash even on windows + """ + dirlist = [] + def list_callback(item): + dirlist.append(item) + self._walk_tree(DirsChangedEditor, callback=list_callback) + return dirlist + + def get_log(self): + """return log message string or None if not present""" + return self._get_property(core.SVN_PROP_REVISION_LOG) + + + # --- Internal helpers def _get_property(self, name): if self.txn_ptr: return fs.txn_prop(self.txn_ptr, name) return fs.revision_prop(self.fs_ptr, self.rev, name) - def _print_tree(self, e_factory, base_rev=None, pass_root=0): + def _print_tree(self, e_factory, base_rev=None, pass_root=0): + def print_callback(msg): + print(msg) + self._walk_tree(e_factory, base_rev, pass_root, callback=print_callback) + + # svn fs, delta, repos calls needs review according to DeltaEditor documentation + def _walk_tree(self, e_factory, base_rev=None, pass_root=0, callback=None): if base_rev is None: # a specific base rev was not provided. use the transaction base, # or the previous revision @@ -119,11 +207,14 @@ # the base of the comparison base_root = fs.revision_root(self.fs_ptr, base_rev) + + if callback == None: + callback = lambda msg: None if pass_root: - editor = e_factory(root, base_root) + editor = e_factory(root, base_root, callback) else: - editor = e_factory() + editor = e_factory(callback=callback) # construct the editor for printing these things out e_ptr, e_baton = delta.make_editor(editor) @@ -135,8 +226,17 @@ e_ptr, e_baton, authz_cb, 0, 1, 0, 0) +# --------------------------------------------------------- +# Delta Editors. For documentation see: +# http://subversion.apache.org/docs/community-guide/#docs + +# this one doesn't process delete_entry, change_dir_prop, apply_text_delta, +# change_file_prop, close_file, close_edit, abort_edit +# ?set_target_revision +# need review class Editor(delta.Editor): - def __init__(self, root=None, base_root=None): + def __init__(self, root=None, base_root=None, callback=None): + """callback argument is unused for this editor""" self.root = root # base_root ignored @@ -172,7 +272,14 @@ return ' <%s>' % fs.unparse_id(id) return '' +# doesn't process close_directory, apply_text_delta, +# change_file_prop, close_file, close_edit, abort_edit +# ?set_target_revision class DirsChangedEditor(delta.Editor): + """print names of changed dirs, callback(dir) is a printer function""" + def __init__(self, callback): + self.callback = callback + def open_root(self, base_revision, dir_pool): return [ 1, '' ] @@ -201,13 +308,15 @@ def _dir_changed(self, baton): if baton[0]: # the directory hasn't been printed yet. do it. - print(baton[1] + '/') + self.callback(baton[1] + '/') baton[0] = 0 class ChangedEditor(delta.Editor): - def __init__(self, root, base_root): + def __init__(self, root, base_root, callback): + """callback(status, path) is a printer function""" self.root = root self.base_root = base_root + self.callback = callback def open_root(self, base_revision, dir_pool): return [ 1, '' ] @@ -215,13 +324,13 @@ def delete_entry(self, path, revision, parent_baton, pool): ### need more logic to detect 'replace' if fs.is_dir(self.base_root, '/' + path): - print('D ' + path + '/') + self.callback('D', path + '/') else: - print('D ' + path) + self.callback('D', path) def add_directory(self, path, parent_baton, copyfrom_path, copyfrom_revision, dir_pool): - print('A ' + path + '/') + self.callback('A', path + '/') return [ 0, path ] def open_directory(self, path, parent_baton, base_revision, dir_pool): @@ -230,12 +339,12 @@ def change_dir_prop(self, dir_baton, name, value, pool): if dir_baton[0]: # the directory hasn't been printed yet. do it. - print('_U ' + dir_baton[1] + '/') + self.callback('_U', dir_baton[1] + '/') dir_baton[0] = 0 def add_file(self, path, parent_baton, copyfrom_path, copyfrom_revision, file_pool): - print('A ' + path) + self.callback('A', path) return [ '_', ' ', None ] def open_file(self, path, parent_baton, base_revision, file_pool): @@ -257,11 +366,12 @@ status = text_mod + prop_mod # was there some kind of change? if status != '_ ': - print(status + ' ' + path) + self.callback(status.rstrip(), path) class DiffEditor(delta.Editor): - def __init__(self, root, base_root): + def __init__(self, root, base_root, callback=None): + """callback argument is unused for this editor""" self.root = root self.base_root = base_root self.target_revision = 0 @@ -378,7 +488,7 @@ output.write( "usage: %s REPOS_PATH rev REV [COMMAND] - inspect revision REV\n" " %s REPOS_PATH txn TXN [COMMAND] - inspect transaction TXN\n" - " %s REPOS_PATH [COMMAND] - inspect the youngest revision\n" + " %s REPOS_PATH [COMMAND] - inspect the latest revision\n" "\n" "REV is a revision number > 0.\n" "TXN is a transaction name.\n" @@ -435,7 +545,7 @@ if not hasattr(SVNLook, 'cmd_' + cmd): usage(1) - SVNLook(sys.argv[1], cmd, rev, txn) + SVNLook(sys.argv[1], rev, txn, cmd) if __name__ == '__main__': main()