File indexing completed on 2024-04-21 05:42:00

0001 #!/usr/bin/python
0002 # -*- coding: UTF-8 -*-
0003 #
0004 # Copyright (c) 2003 The University of Wroclaw.
0005 # All rights reserved.
0006 #
0007 # Redistribution and use in source and binary forms, with or without
0008 # modification, are permitted provided that the following conditions
0009 # are met:
0010 #    1. Redistributions of source code must retain the above copyright
0011 #       notice, this list of conditions and the following disclaimer.
0012 #    2. Redistributions in binary form must reproduce the above copyright
0013 #       notice, this list of conditions and the following disclaimer in the
0014 #       documentation and/or other materials provided with the distribution.
0015 #    3. The name of the University may not be used to endorse or promote
0016 #       products derived from this software without specific prior
0017 #       written permission.
0018 # 
0019 # THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY ``AS IS'' AND ANY EXPRESS OR
0020 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
0021 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
0022 # NO EVENT SHALL THE UNIVERSITY BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
0023 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
0024 # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
0025 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
0026 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
0027 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
0028 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
0029 #
0030 
0031 import sys
0032 import os
0033 import time
0034 import re
0035 import getopt
0036 import string
0037 import codecs
0038 import locale
0039 
0040 from xml.utils import qp_xml
0041 
0042 kill_prefix_rx = None
0043 default_domain = "localhost"
0044 exclude = []
0045 users = { }
0046 reloc = { }
0047 max_join_delta = 3 * 60
0048 list_format = False
0049 
0050 date_rx = re.compile(r"^(\d+-\d+-\d+T\d+:\d+:\d+)")
0051 
0052 def die(msg):
0053   sys.stderr.write(msg + "\n")
0054   sys.exit(1)
0055 
0056 def attr(e, n):
0057   return e.attrs[("", n)]
0058 
0059 def has_child(e, n):
0060   for c in e.children:
0061     if c.name == n: return 1
0062   return 0
0063 
0064 def child(e, n):
0065   for c in e.children:
0066     if c.name == n: return c
0067   die("<%s> doesn't have <%s> child" % (e.name, n))
0068   
0069 def convert_path(n):
0070   for src in reloc.keys():
0071     n = string.replace(n, src, reloc[src])
0072   if kill_prefix_rx != None:
0073     if kill_prefix_rx.search(n):
0074       n = kill_prefix_rx.sub("", n)
0075     else:
0076       return None
0077   if n.startswith("/"): n = n[1:]
0078   if n == "": n = "/"
0079   for pref in exclude:
0080     if n.startswith(pref):
0081       return None
0082   return n
0083 
0084 def convert_user(u):
0085   if users.has_key(u):
0086     return users[u]
0087   else:
0088     return u
0089 
0090 def wrap_text_line(str, pref, width):
0091   ret = u""
0092   line = u""
0093   first_line = True
0094   for word in str.split():
0095     if line == u"":
0096       line = word
0097     else:
0098       if len(line + u" " + word) > width:
0099         if first_line:
0100           ret += line + u"\n"
0101           first_line = False
0102           line = word
0103         else:
0104           ret += pref + line + u"\n"
0105           line = word
0106       else:
0107         line += u" " + word
0108   if first_line:
0109     ret += line + u"\n"
0110   else:
0111     ret += pref + line + u"\n"
0112   return ret
0113 
0114 def wrap_text(str, pref, width):
0115   if not list_format:
0116     return wrap_text_line(str,pref,width)
0117   else:
0118     items = re.split(r"\-\s+",str)
0119     ret = wrap_text_line(items[0],pref,width)
0120     for item in items[1:]:
0121       ret += pref + u"- " + wrap_text_line(item,pref+"  ",width)
0122     return ret
0123 
0124 class Entry:
0125   def __init__(self, tm, rev, author, msg):
0126     self.tm = tm
0127     self.rev = rev
0128     self.author = author
0129     self.msg = msg
0130     self.beg_tm = tm
0131     self.beg_rev = rev
0132 
0133   def join(self, other):
0134     self.tm = other.tm
0135     self.rev = other.rev
0136     self.msg += other.msg
0137 
0138   def dump(self, out):
0139     sys.stderr.write(self.rev+"\n")
0140     if self.rev != self.beg_rev:
0141       out.write("%s [r%s-%s]  %s\n\n" % \
0142                           (time.strftime("%Y-%m-%d %H:%M +0000", time.localtime(self.beg_tm)), \
0143                            self.rev, self.beg_rev, convert_user(self.author)))
0144     else:
0145       out.write(u"%s [r%s]  %s\n\n" % \
0146                           (time.strftime("%Y-%m-%d %H:%M +0000", time.localtime(self.beg_tm)), \
0147                            self.rev, convert_user(self.author)))
0148     out.write(self.msg)
0149   
0150   def can_join(self, other):
0151     return self.author == other.author and abs(self.tm - other.tm) < max_join_delta
0152 
0153 def process_entry(e):
0154   rev = attr(e, "revision")
0155   if has_child(e, "author"):
0156     author = child(e, "author").textof()
0157   else:
0158     author = "anonymous"
0159   m = date_rx.search(child(e, "date").textof())
0160   msg = child(e, "msg").textof()
0161   if m:
0162     tm = time.mktime(time.strptime(m.group(1), "%Y-%m-%dT%H:%M:%S"))
0163   else:
0164     die("evil date: %s" % child(e, "date").textof())
0165   paths = []
0166   for path in child(e, "paths").children:
0167     if path.name != "path": die("<paths> has non-<path> child")
0168     nam = convert_path(path.textof())
0169     if nam != None:
0170       if attr(path, "action") == "D":
0171         paths.append(nam + " (removed)")
0172       elif attr(path, "action") == "A":
0173         paths.append(nam + " (added)")
0174       else:
0175         paths.append(nam)
0176   
0177   if msg.startswith("SVN_SILENT"):
0178     return None
0179  
0180   if msg.startswith("CVS_SILENT"):
0181     return None
0182   
0183   if msg.startswith("This commit was manufactured by cvs2svn to create branch"):
0184     return None
0185   
0186   if paths != []:
0187     return Entry(tm, rev, author, "\t* %s\n" % wrap_text(", ".join(paths) + ": " + msg, "\t  ", 65))
0188 
0189   return None
0190 
0191 def process(fin, fout):
0192   parser = qp_xml.Parser()
0193   root = parser.parse(fin)
0194 
0195   if root.name != "log": die("root is not <log>")
0196   
0197   cur = None
0198   
0199   for logentry in root.children:
0200     if logentry.name != "logentry": die("non <logentry> <log> child")
0201     e = process_entry(logentry)
0202     if e != None:
0203       if cur != None:
0204         if cur.can_join(e):
0205           cur.join(e)
0206         else:
0207           cur.dump(fout)
0208           cur = e
0209       else: cur = e
0210         
0211   if cur != None: cur.dump(fout)
0212 
0213 def usage():
0214   sys.stderr.write(\
0215 """Usage: %s [OPTIONS] [FILE]
0216 Convert specified subversion xml logfile to GNU-style ChangeLog.
0217 
0218 Options:
0219   -p, --prefix=REGEXP  set root directory of project (it will be striped off
0220                        from ChangeLog entries, paths outside it will be 
0221                        ignored)
0222   -x, --exclude=DIR    exclude DIR from ChangeLog (relative to prefix)
0223   -o, --output         set output file (defaults to 'ChangeLog')
0224   -d, --domain=DOMAIN  set default domain for logins not listed in users file
0225   -u, --users=FILE     read logins from specified file
0226       --users-charset=ENCODING
0227                        specify encoding of users. defaults to ISO8859-1
0228   -F, --list-format    format commit logs with enumerated change list (items
0229                        prefixed by '- ')
0230   -r, --relocate=X=Y   before doing any other operations on paths, replace
0231                        X with Y (useful for directory moves)
0232   -D, --delta=SECS     when log entries differ by less then SECS seconds and
0233                        have the same author -- they are merged, it defaults
0234                        to 180 seconds
0235   -h, --help           print this information
0236 
0237 Users file is used to map svn logins to real names to appear in ChangeLog.
0238 If login is not found in users file "login <login@domain>" is used.
0239 
0240 Example users file:
0241 john    John X. Foo <jfoo@example.org>
0242 mark    Marcus Blah <mb@example.org>
0243 
0244 Typical usage of this script is something like this:
0245 
0246   svn log -v --xml | %s -p '/foo/(branches/[^/]+|trunk)' -u aux/users
0247   
0248 Please send bug reports and comments to author:
0249   Michal Moskal <malekith@pld-linux.org>
0250 
0251 """ % (sys.argv[0], sys.argv[0]))
0252 
0253 def utf_open(name, mode):
0254   return codecs.open(name, mode, encoding="utf-8", errors="replace")
0255 
0256 def process_opts():
0257   try:
0258     opts, args = getopt.gnu_getopt(sys.argv[1:], "o:u:p:x:d:r:d:D:Fh", 
0259                                    ["users-charset=", "users=", "prefix=", "domain=", "delta=",
0260                                     "exclude=", "help", "output=", "relocate=",
0261                                     "list-format"])
0262   except getopt.GetoptError:
0263     usage()
0264     sys.exit(2)
0265   fin = sys.stdin
0266   fout = None
0267   users_file = None
0268   users_charset = 'ISO8859-1'
0269   global kill_prefix_rx, exclude, users, default_domain, reloc, max_join_delta, list_format
0270   for o, a in opts:
0271     if o in ("--prefix", "-p"):
0272       kill_prefix_rx = re.compile("^" + a)
0273     elif o in ("--exclude", "-x"):
0274       exclude.append(a)
0275     elif o in ("--help", "-h"):
0276       usage()
0277       sys.exit(0)
0278     elif o in ("--output", "-o"):
0279       fout = utf_open(a, "w")
0280     elif o in ("--domain", "-d"):
0281       default_domain = a
0282     elif o in ("--users", "-u"):
0283       users_file = a
0284     elif o in ("--users-charset"):
0285       users_charset = a
0286     elif o in ("--relocate", "-r"):
0287       (src, target) = a.split("=")
0288       reloc[src] = target
0289     elif o in ("--delta", "-D"):
0290       max_join_delta = int(a)
0291     elif o in ("--list-format", "-F"):
0292       list_format = True
0293     else:
0294       usage()
0295       sys.exit(2)
0296 
0297   if len(args) > 1:
0298     usage()
0299     sys.exit(2)
0300   if len(args) == 1:
0301     fin = open(args[0], "r")
0302   if fout == None:
0303     fout = utf_open("ChangeLog", "w")
0304 
0305   if users_file != None:
0306     f = utf_open(users_file, "r")
0307     for line in f.xreadlines():
0308       w = line.split()
0309       if len(line) < 1 or line[0] == '#' or len(w) < 2:
0310         continue
0311       users[w[0]] = " ".join(w[1:]).decode(users_charset)
0312   process(fin, fout)
0313 
0314 if __name__ == "__main__":
0315   os.environ['TZ'] = 'UTC'
0316   try:
0317     time.tzset()
0318   except AttributeError:
0319     pass
0320   process_opts()
0321 
0322 # vim:ts=2:sw=2:et