#!/usr/bin/env python ################################################################################################## # # Stupid 'p4-like' interface for Subversion # # version 2008/07/02 # # Mitch Haile # mitch.haile@gmail.com # http://www.degana.com/ # # This just implements the commands that I use and the flags I wanted. No real attempt to # implement the entire p4 command line is made. :-) Don't expect 'p4 resolve' or 'p4 integrate' # any time soon (or ever). # # No attempt is made for this code to be 'pythonic' either. If you have suggestions or bug # fixes, let me know. But don't flame me for not being properly pythonic. # # Constructive feedback always welcome. # ################################################################################################## # # Install: # # 1. Place this somewhere in your PATH. I use ~/bin/, but /usr/local/bin or elsewhere is fine. # # 2. Set the +x bit with chmod. # # 3. Set any alias or symlinks you might want (I use a p4 symlink). # # 4. I use $TOP to represent the top of my source tree. You may want to change TOP_VARIABLE # to whatever you use, if you want this tool to find the top of your tree and provide info # based on the top of the tree rather than the current directory. # # I have only tested this with Python 2.5.1 at this time. # ################################################################################################## # # Acknowledgements: # # Thanks to Chuck R for his example patch for 's4 describe -s' support. # ################################################################################################## # # Copyright (C) 2008 Mitch Haile # # This program is free software; you can redistribute it and/or modify it under the terms of # the GNU General Public License Version 2 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; # without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See # the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along with this program; if # not, see . # ################################################################################################## import getopt import sys import os import xml.sax import xml.sax.handler TOP_VARIABLE = "TOP" ################################################################################################## ## Subversion output parsing code ################################################################################################## class svnLogEntry: def __init__(self): self.revision = -1 self.author = None self.date = None self.msg = None # # Format results from 'svn log --xml'. Records look like this: # # # mitch # 2007-09-07T11:26:38.313604Z # hello message. # # class svnLogXmlHandler(xml.sax.handler.ContentHandler): def __init__(self): self.entry = svnLogEntry() self.in_author = False self.in_date = False self.in_msg = False self.data = dict() def startElement(self, name, attributes): if name == "logentry": self.entry.revision = int(attributes['revision']) elif name == "author": self.in_author = True elif name == "date": self.in_date = True elif name == "msg": self.in_msg = True return def characters(self, data): if self.in_author: self.entry.author = data if self.in_date: self.entry.date = data if self.in_msg: self.entry.msg = data; def endElement(self, name): if name == "author": self.in_author = False if name == "date": self.in_date = False if name == "msg": self.in_msg = False if name == "logentry": self.data[self.entry.revision] = self.entry self.entry = svnLogEntry() ################################################################################################## ## Main code ################################################################################################## TREE_TOP = "." def usage(): sys.stderr.write("s4 syntax:\n") sys.stderr.write("\n") sys.stderr.write(" s4 changes [-u ] [-m ]\n") sys.stderr.write(" s4 describe \n") sys.stderr.write(" s4 diff [ \"...\" | ]\n") sys.stderr.write(" s4 opened [ \"...\" | ]\n") # filelog sys.exit(2) # # Given an optstring (e.g., ab:c:d), return an array containing two things: # # 1. A dictionary containing only the keys that were present in the input string, and the values of # the keys are either 'True' for present with no arguments or a string containing the value for # that option. # # 2. The 'args' array of any suffix arguments that were not matched. # def parse_opts(optstr, cmd_argv): d = dict() try: opts, args = getopt.getopt(cmd_argv, optstr) except getopt.GetoptError: usage() # XXX could improve this quite a bit output = None for o, a in opts: if a == None: d[o] = True else: d[o] = a return [d, args] # # 'describe' command # # s4 describe [-s] # def do_describe(cmd_argv): cmd_opts = parse_opts("s", cmd_argv) flags, args = cmd_opts if len(args) == 0: sys.stderr.write("Error: no changeset provided\n") sys.exit(2) if len(args) != 1: sys.stderr.write("Error: only one changeset at a time, please\n") sys.exit(2) try: cs = int(args[0]) except: sys.stderr.write("Error: changeset must be a number\n") sys.exit(2) prev_cs = cs - 1 if prev_cs <= 0: sys.stderr.write("Error: need to handle changeset 1 in a special manner\n") sys.exit(2) diff_args = "" if flags.has_key('-s'): # 'short' version os.system("svn log -r" + str(cs)) # Here, we assume 'diff' is not a graphical differ. diff_args = "--diff-cmd diff -r" + str(prev_cs) + ":" + str(cs) + " --summarize" else: diff_args = "-r" + str(prev_cs) + ":" + str(cs) os.system("svn diff " + diff_args) return # # 'changes' command # # s4 changes [-u ] [-m ] [path/file] # # Unfortunately, using -u (which we do locally) and -m means we need to get all of the history # to be sure we get -m lines of -u. :-( So... # def do_changes(cmd_argv): cmd_opts = parse_opts("u:m:", cmd_argv) flags, args = cmd_opts # No file args? Use the top of the tree. if len(args) == 0: args.append(TREE_TOP) if len(args) != 1: sys.stderr.write("Error: only one path supported\n") sys.exit(2) user_limit = 0 path = args[0] limit_str = "" if flags.has_key('-m'): try: limit = int(flags['-m']) except: sys.stderr.write("Error: -m requires a number.\n") sys.exit(3) if not flags.has_key('-u'): limit_str = "--limit " + str(limit) else: user_limit = limit p = os.popen("svn log --xml " + limit_str + " " + path) lines = [] count = 0 while 1: line = p.readline() if not line: break lines.append(line) count = count + 1 if count > 100: count = 0 # OK this counter is confusing because the number is the lines of XML which # really doesn't mean much, but it's SOMETHING anyway. sys.stderr.write("\rworking (" + str(len(lines)) + ")...") sys.stderr.flush() # os.pclose(p) XXX no pclose() ? sys.stderr.write("\r") sys.stderr.flush() parser = xml.sax.make_parser() handler = svnLogXmlHandler() xml.sax.parseString("".join(lines), handler) revs = handler.data.keys() revs.sort() revs.reverse() user_count = 0 # For when -m and -u are used together for rev in revs: entry = handler.data[rev] print_this = False if flags.has_key('-u'): if entry.author == flags['-u']: user_count = user_count + 1 print_this = True else: print_this = True if print_this: date_str = str(entry.date) if date_str.index('T'): date_str = date_str[:date_str.index('T')] line_str = "Change " + str(rev) \ + " on " + date_str \ + " by " + str(entry.author) \ + " '" + str(entry.msg) line_str = line_str[:78] # force it to fit into 80 columns line_str = line_str.replace('\n', ' ') # any spurious newlines get chopped line_str = line_str + "'" print line_str if user_limit > 0: if user_count >= user_limit: print "break!" break return # # 'diff' command # # s4 diff [ "..." | ] # # Bare bones for now # def do_diff(cmd_argv): arg_str = None if len(cmd_argv) == 0: arg_str = TREE_TOP elif cmd_argv[0] == "...": arg_str = "." else: arg_str = " ".join(cmd_argv) os.system("svn diff " + arg_str) return # # 'sync' command # # s4 sync [ "..." | ] # # Bare bones for now # def do_sync(cmd_argv): arg_str = None if len(cmd_argv) == 0: arg_str = TREE_TOP elif cmd_argv[0] == "...": arg_str = "." else: arg_str = " ".join(cmd_argv) os.system("svn update " + arg_str) return # # 'opened' command # # s4 opened [ "..." | ] # # Since Subversion doesn't have a notion of editing a file in the same that Perforce does, this # just shows the 'svn status' output # def do_opened(cmd_argv): arg_str = None if len(cmd_argv) == 0: arg_str = TREE_TOP elif cmd_argv[0] == "...": arg_str = "." else: arg_str = " ".join(cmd_argv) os.system("svn status " + arg_str) return def main(): if len(sys.argv) == 1: usage() if os.environ.has_key(TOP_VARIABLE): global TREE_TOP TREE_TOP = os.environ[TOP_VARIABLE] # XXX check to see if this is a valid path cmd = sys.argv[1] cmd_args = [] if len(sys.argv) > 2: cmd_args = sys.argv[2:] # print "cmd = " + cmd if cmd == "describe": do_describe(cmd_args) elif cmd == "changes": do_changes(cmd_args) elif cmd == "diff": do_diff(cmd_args) elif cmd == "opened": do_opened(cmd_args) elif cmd == "sync": do_sync(cmd_args) else: sys.stderr.write("Unsupported command\n") sys.exit(2) if __name__ == "__main__": main()