[[[ * tools/examples/svnlook.py: Make it usable as a library ]]] Of course it would be better to use iterator interface instead of copying lists in memory, but I don't know if Subversion bindings have one. Not all commands are implemented, as I don't have time to rewrite everything, but at least it is clearly documented what is left, so it should easy do to if anyone will need it for their hook scripts.
This is the same as: https://github.com/apache/subversion/pull/1/files Please, CC.
--- 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()