svn2log.py
Go to the documentation of this file.
00001 #!/usr/bin/python
00002 #
00003 # Copyright (c) 2003 The University of Wroclaw.
00004 # All rights reserved.
00005 #
00006 # Redistribution and use in source and binary forms, with or without
00007 # modification, are permitted provided that the following conditions
00008 # are met:
00009 #    1. Redistributions of source code must retain the above copyright
00010 #       notice, this list of conditions and the following disclaimer.
00011 #    2. Redistributions in binary form must reproduce the above copyright
00012 #       notice, this list of conditions and the following disclaimer in the
00013 #       documentation and/or other materials provided with the distribution.
00014 #    3. The name of the University may not be used to endorse or promote
00015 #       products derived from this software without specific prior
00016 #       written permission.
00017 # 
00018 # THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY ``AS IS'' AND ANY EXPRESS OR
00019 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
00020 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
00021 # NO EVENT SHALL THE UNIVERSITY BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
00022 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
00023 # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
00024 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
00025 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
00026 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
00027 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
00028 #
00029 # History:
00030 # 
00031 # 2006-08-03  Przedsiebiorstwo Informatyczne CORE  <biuro at core.com.pl>:
00032 #         * Following options were added:
00033 #           -s, --strip-comments strip /* ... */ comments in log
00034 #           -O, --only-date      generate only dates (without time)
00035 #           -L, --no-files       generate log without filenames
00036 #           -A, --no-author      generate log without author names
00037 #           -H, --no-host        generate author name without hostname
00038 #
00039 
00040 
00041 import sys
00042 import os
00043 import time
00044 import re
00045 import getopt
00046 import string
00047 import codecs
00048 
00049 from xml.utils import qp_xml
00050 
00051 kill_prefix_rx = None
00052 default_domain = "localhost"
00053 exclude = []
00054 users = { }
00055 reloc = { }
00056 max_join_delta = 3 * 60
00057 list_format = False
00058 strip = False
00059 date_only = False
00060 no_files = False
00061 no_host = False
00062 no_author = False
00063 
00064 date_rx = re.compile(r"^(\d+-\d+-\d+T\d+:\d+:\d+)")
00065 
00066 def die(msg):
00067   sys.stderr.write(msg + "\n")
00068   sys.exit(1)
00069 
00070 def attr(e, n):
00071   return e.attrs[("", n)]
00072 
00073 def has_child(e, n):
00074   for c in e.children:
00075     if c.name == n: return 1
00076   return 0
00077 
00078 def child(e, n):
00079   for c in e.children:
00080     if c.name == n: return c
00081   die("<%s> doesn't have <%s> child" % (e.name, n))
00082   
00083 def convert_path(n):
00084   for src in reloc.keys():
00085     n = string.replace(n, src, reloc[src])
00086   if kill_prefix_rx != None:
00087     if kill_prefix_rx.search(n):
00088       n = kill_prefix_rx.sub("", n)
00089     else:
00090       return None
00091   if n.startswith("/"): n = n[1:]
00092   if n == "": n = "/"
00093   for pref in exclude:
00094     if n.startswith(pref):
00095       return None
00096   return n
00097 
00098 def convert_user(u):
00099   if no_author == False:
00100     if users.has_key(u):
00101       return users[u]
00102     else:
00103       if no_host:
00104         return u + ":"   
00105       else:
00106         return "%s <%s@%s>:" % (u, u, default_domain)
00107   else:
00108     return ''
00109 
00110 def wrap_text_line(str, pref, width, start):
00111   ret = u""
00112   line = u""
00113   first_line = True
00114   for word in str.split():
00115     if line == u"":
00116       line = word
00117     else:
00118       if len(line + u" " + word) > (width - start):
00119         if first_line:
00120           ret += line + u"\n"
00121           first_line = False
00122           start = 0
00123           line = word
00124         else:
00125           ret += pref + line + u"\n"
00126           line = word
00127       else:
00128         line += u" " + word
00129   if first_line:
00130     ret += line
00131   else:
00132     ret += pref + line
00133   return ret
00134 
00135 def wrap_text(str, pref, width, start = 0):
00136   if not list_format:
00137     return wrap_text_line(str,pref,width,start)
00138   else:
00139     items = re.split(r"\-\s+",str)
00140     ret = wrap_text_line(items[0],pref,width,start)
00141     for item in items[1:]:
00142       ret += pref + u"- " + wrap_text_line(item,pref+"  ",width,start)
00143     return ret
00144 
00145 class Entry:
00146   def __init__(self, tm, rev, author, msg):
00147     self.tm = tm
00148     self.rev = rev
00149     self.author = author
00150     self.msg = msg
00151     self.beg_tm = tm
00152     self.beg_rev = rev
00153 
00154   def join(self, other):
00155     self.tm = other.tm
00156     self.rev = other.rev
00157     self.msg += other.msg
00158 
00159   def dump(self, out):
00160     if len(self.msg) > 0:
00161       if date_only == False:
00162         tformat = "%Y-%m-%d %H:%M +0000"
00163       else:
00164         tformat = "%Y-%m-%d"
00165 
00166       if self.rev != self.beg_rev:
00167         out.write("%s [r%s-%s]  %s\n\n" % \
00168                           (time.strftime(tformat, time.localtime(self.beg_tm)), \
00169                            self.rev, self.beg_rev, convert_user(self.author)))
00170       else:
00171         out.write("%s [r%s]  %s\n\n" % \
00172                           (time.strftime(tformat, time.localtime(self.beg_tm)), \
00173                            self.rev, convert_user(self.author)))
00174       out.write(self.msg)
00175   
00176   def can_join(self, other):
00177     return self.author == other.author and abs(self.tm - other.tm) < max_join_delta
00178 
00179 def process_entry(e):
00180   rev = attr(e, "revision")
00181   if has_child(e, "author"):
00182     author = child(e, "author").textof()
00183   else:
00184     author = "anonymous"
00185   m = date_rx.search(child(e, "date").textof())
00186   msg = ' ' + child(e, "msg").textof()
00187   if strip == True:
00188     ibegin = string.find(msg, "/*")
00189     if ibegin > 0:
00190       iend = string.find(msg, "*/") + 2
00191       msg = msg[0:ibegin] + msg[iend:]
00192 
00193   if m:
00194     tm = time.mktime(time.strptime(m.group(1), "%Y-%m-%dT%H:%M:%S"))
00195   else:
00196     die("evil date: %s" % child(e, "date").textof())
00197   paths = []
00198   if len(msg) > 1: 
00199     for path in child(e, "paths").children:
00200       if path.name != "path": die("<paths> has non-<path> child")
00201       nam = convert_path(path.textof())
00202       if nam != None:
00203         if attr(path, "action") == "D":
00204           paths.append(nam + " (removed)")
00205         elif attr(path, "action") == "A":
00206           paths.append(nam + " (added)")
00207         else:
00208           paths.append(nam)
00209      
00210     if paths != [] and no_files == False:
00211       pathlines = wrap_text(", ".join(paths),"\t* ", 65)
00212       start = len(pathlines) - pathlines.rfind("\n") + 1
00213       message   = wrap_text(": " + msg, "\t  ", 65, start )
00214       return Entry(tm, rev, author, "\t* %s %s\n\n" % (pathlines, message))
00215     elif paths != [] and no_files == True:
00216       return Entry(tm, rev, author, "\t* %s\n\n" % wrap_text(msg, "\t  ", 65))
00217 
00218   return None
00219 
00220 def process(fin, fout):
00221   parser = qp_xml.Parser()
00222   root = parser.parse(fin)
00223 
00224   if root.name != "log": die("root is not <log>")
00225   
00226   cur = None
00227   
00228   for logentry in root.children:
00229     if logentry.name != "logentry": die("non <logentry> <log> child")
00230     e = process_entry(logentry)
00231     if e != None:
00232       if cur != None:
00233         if cur.can_join(e):
00234           cur.join(e)
00235         else:
00236           cur.dump(fout)
00237           cur = e
00238       else: cur = e
00239         
00240   if cur != None: cur.dump(fout)
00241 
00242 def usage():
00243   sys.stderr.write(\
00244 """Usage: %s [OPTIONS] [FILE]
00245 Convert specified subversion xml logfile to GNU-style ChangeLog.
00246 
00247 Options:
00248   -p, --prefix=REGEXP  set root directory of project (it will be striped off
00249                        from ChangeLog entries, paths outside it will be 
00250                        ignored)
00251   -x, --exclude=DIR    exclude DIR from ChangeLog (relative to prefix)
00252   -o, --output         set output file (defaults to 'ChangeLog')
00253   -d, --domain=DOMAIN  set default domain for logins not listed in users file
00254   -u, --users=FILE     read logins from specified file
00255   -F, --list-format    format commit logs with enumerated change list (items
00256                        prefixed by '- ')
00257   -r, --relocate=X=Y   before doing any other operations on paths, replace
00258                        X with Y (useful for directory moves)
00259   -D, --delta=SECS     when log entries differ by less then SECS seconds and
00260                        have the same author -- they are merged, it defaults
00261                        to 180 seconds
00262   -h, --help           print this information
00263   -s, --strip-comments strip /* ... */ comments in log
00264   -O, --only-date      generate only dates (without time)
00265   -L, --no-files       generate log without filenames
00266   -A, --no-author      generate log without author names
00267   -H, --no-host        generate author name without hostname
00268 
00269 Users file is used to map svn logins to real names to appear in ChangeLog.
00270 If login is not found in users file "login <login@domain>" is used.
00271 
00272 Example users file:
00273 john    John X. Foo <jfoo@example.org>
00274 mark    Marcus Blah <mb@example.org>
00275 
00276 Typical usage of this script is something like this:
00277 
00278   svn log -v --xml | %s -p '/foo/(branches/[^/]+|trunk)' -u aux/users
00279   
00280 Please send bug reports and comments to author:
00281   Michal Moskal <malekith@pld-linux.org>
00282   
00283 Regarding -s, -O, -L, -A, -H options see
00284   http://www.core.com.pl/svn2log
00285    
00286 
00287 """ % (sys.argv[0], sys.argv[0]))
00288 
00289 def utf_open(name, mode):
00290   return codecs.open(name, mode, encoding="utf-8", errors="replace")
00291 
00292 def process_opts():
00293   try:
00294     opts, args = getopt.gnu_getopt(sys.argv[1:], "o:u:p:x:d:r:d:D:FhsOLHA", 
00295                                    ["users=", "prefix=", "domain=", "delta=",
00296                                     "exclude=", "help", "output=", "relocate=",
00297                                     "list-format","strip-comments", "only-date", "no-files",
00298                                                                                                                                                 "no-host", "no-author"])
00299   except getopt.GetoptError:
00300     usage()
00301     sys.exit(2)
00302   fin = sys.stdin
00303   fout = None
00304   global kill_prefix_rx, exclude, users, default_domain, reloc, max_join_delta, list_format, strip, date_only, no_files, no_host, no_author
00305   for o, a in opts:
00306     if o in ("--prefix", "-p"):
00307       kill_prefix_rx = re.compile("^" + a)
00308     elif o in ("--exclude", "-x"):
00309       exclude.append(a)
00310     elif o in ("--help", "-h"):
00311       usage()
00312       sys.exit(0)
00313     elif o in ("--output", "-o"):
00314       fout = utf_open(a, "w")
00315     elif o in ("--domain", "-d"):
00316       default_domain = a
00317     elif o in ("--strip-comments", "-s"):
00318       strip = True
00319     elif o in ("--only-date", "-O"):
00320       date_only = True
00321     elif o in ("--no-files", "-L"):
00322       no_files = True
00323     elif o in ("--no-host", "-H"):
00324       no_host = True
00325     elif o in ("--no-author", "-A"):
00326       no_author = True
00327     elif o in ("--users", "-u"):
00328       f = utf_open(a, "r")
00329       for line in f.xreadlines():
00330         w = line.split()
00331         if len(line) < 1 or line[0] == '#' or len(w) < 2: 
00332           continue
00333         users[w[0]] = " ".join(w[1:])
00334     elif o in ("--relocate", "-r"):
00335       (src, target) = a.split("=")
00336       reloc[src] = target
00337     elif o in ("--delta", "-D"):
00338       max_join_delta = int(a)
00339     elif o in ("--list-format", "-F"):
00340       list_format = True
00341     else:
00342       usage()
00343       sys.exit(2)
00344   if len(args) > 1:
00345     usage()
00346     sys.exit(2)
00347   if len(args) == 1:
00348     fin = open(args[0], "r")
00349   if fout == None:
00350     fout = utf_open("ChangeLog", "w")
00351   process(fin, fout)
00352 
00353 if __name__ == "__main__":
00354   os.environ['TZ'] = 'UTC'
00355   try:
00356     time.tzset()
00357   except AttributeError:
00358     pass
00359   process_opts()


rtt
Author(s): RTT Developers
autogenerated on Mon Oct 6 2014 03:13:54