00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
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()