diff options
Diffstat (limited to 'release/svn2log.py')
-rw-r--r-- | release/svn2log.py | 322 |
1 files changed, 322 insertions, 0 deletions
diff --git a/release/svn2log.py b/release/svn2log.py new file mode 100644 index 0000000..a216b8e --- /dev/null +++ b/release/svn2log.py @@ -0,0 +1,322 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- +# +# Copyright (c) 2003 The University of Wroclaw. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. The name of the University may not be used to endorse or promote +# products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +# NO EVENT SHALL THE UNIVERSITY BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +import sys +import os +import time +import re +import getopt +import string +import codecs +import locale + +from xml.utils import qp_xml + +kill_prefix_rx = None +default_domain = "localhost" +exclude = [] +users = { } +reloc = { } +max_join_delta = 3 * 60 +list_format = False + +date_rx = re.compile(r"^(\d+-\d+-\d+T\d+:\d+:\d+)") + +def die(msg): + sys.stderr.write(msg + "\n") + sys.exit(1) + +def attr(e, n): + return e.attrs[("", n)] + +def has_child(e, n): + for c in e.children: + if c.name == n: return 1 + return 0 + +def child(e, n): + for c in e.children: + if c.name == n: return c + die("<%s> doesn't have <%s> child" % (e.name, n)) + +def convert_path(n): + for src in reloc.keys(): + n = string.replace(n, src, reloc[src]) + if kill_prefix_rx != None: + if kill_prefix_rx.search(n): + n = kill_prefix_rx.sub("", n) + else: + return None + if n.startswith("/"): n = n[1:] + if n == "": n = "/" + for pref in exclude: + if n.startswith(pref): + return None + return n + +def convert_user(u): + if users.has_key(u): + return users[u] + else: + return u + +def wrap_text_line(str, pref, width): + ret = u"" + line = u"" + first_line = True + for word in str.split(): + if line == u"": + line = word + else: + if len(line + u" " + word) > width: + if first_line: + ret += line + u"\n" + first_line = False + line = word + else: + ret += pref + line + u"\n" + line = word + else: + line += u" " + word + if first_line: + ret += line + u"\n" + else: + ret += pref + line + u"\n" + return ret + +def wrap_text(str, pref, width): + if not list_format: + return wrap_text_line(str,pref,width) + else: + items = re.split(r"\-\s+",str) + ret = wrap_text_line(items[0],pref,width) + for item in items[1:]: + ret += pref + u"- " + wrap_text_line(item,pref+" ",width) + return ret + +class Entry: + def __init__(self, tm, rev, author, msg): + self.tm = tm + self.rev = rev + self.author = author + self.msg = msg + self.beg_tm = tm + self.beg_rev = rev + + def join(self, other): + self.tm = other.tm + self.rev = other.rev + self.msg += other.msg + + def dump(self, out): + sys.stderr.write(self.rev+"\n") + if self.rev != self.beg_rev: + out.write("%s [r%s-%s] %s\n\n" % \ + (time.strftime("%Y-%m-%d %H:%M +0000", time.localtime(self.beg_tm)), \ + self.rev, self.beg_rev, convert_user(self.author))) + else: + out.write(u"%s [r%s] %s\n\n" % \ + (time.strftime("%Y-%m-%d %H:%M +0000", time.localtime(self.beg_tm)), \ + self.rev, convert_user(self.author))) + out.write(self.msg) + + def can_join(self, other): + return self.author == other.author and abs(self.tm - other.tm) < max_join_delta + +def process_entry(e): + rev = attr(e, "revision") + if has_child(e, "author"): + author = child(e, "author").textof() + else: + author = "anonymous" + m = date_rx.search(child(e, "date").textof()) + msg = child(e, "msg").textof() + if m: + tm = time.mktime(time.strptime(m.group(1), "%Y-%m-%dT%H:%M:%S")) + else: + die("evil date: %s" % child(e, "date").textof()) + paths = [] + for path in child(e, "paths").children: + if path.name != "path": die("<paths> has non-<path> child") + nam = convert_path(path.textof()) + if nam != None: + if attr(path, "action") == "D": + paths.append(nam + " (removed)") + elif attr(path, "action") == "A": + paths.append(nam + " (added)") + else: + paths.append(nam) + + if msg.startswith("SVN_SILENT"): + return None + + if msg.startswith("CVS_SILENT"): + return None + + if msg.startswith("This commit was manufactured by cvs2svn to create branch"): + return None + + if paths != []: + return Entry(tm, rev, author, "\t* %s\n" % wrap_text(", ".join(paths) + ": " + msg, "\t ", 65)) + + return None + +def process(fin, fout): + parser = qp_xml.Parser() + root = parser.parse(fin) + + if root.name != "log": die("root is not <log>") + + cur = None + + for logentry in root.children: + if logentry.name != "logentry": die("non <logentry> <log> child") + e = process_entry(logentry) + if e != None: + if cur != None: + if cur.can_join(e): + cur.join(e) + else: + cur.dump(fout) + cur = e + else: cur = e + + if cur != None: cur.dump(fout) + +def usage(): + sys.stderr.write(\ +"""Usage: %s [OPTIONS] [FILE] +Convert specified subversion xml logfile to GNU-style ChangeLog. + +Options: + -p, --prefix=REGEXP set root directory of project (it will be striped off + from ChangeLog entries, paths outside it will be + ignored) + -x, --exclude=DIR exclude DIR from ChangeLog (relative to prefix) + -o, --output set output file (defaults to 'ChangeLog') + -d, --domain=DOMAIN set default domain for logins not listed in users file + -u, --users=FILE read logins from specified file + --users-charset=ENCODING + specify encoding of users. defaults to ISO8859-1 + -F, --list-format format commit logs with enumerated change list (items + prefixed by '- ') + -r, --relocate=X=Y before doing any other operations on paths, replace + X with Y (useful for directory moves) + -D, --delta=SECS when log entries differ by less then SECS seconds and + have the same author -- they are merged, it defaults + to 180 seconds + -h, --help print this information + +Users file is used to map svn logins to real names to appear in ChangeLog. +If login is not found in users file "login <login@domain>" is used. + +Example users file: +john John X. Foo <jfoo@example.org> +mark Marcus Blah <mb@example.org> + +Typical usage of this script is something like this: + + svn log -v --xml | %s -p '/foo/(branches/[^/]+|trunk)' -u aux/users + +Please send bug reports and comments to author: + Michal Moskal <malekith@pld-linux.org> + +""" % (sys.argv[0], sys.argv[0])) + +def utf_open(name, mode): + return codecs.open(name, mode, encoding="utf-8", errors="replace") + +def process_opts(): + try: + opts, args = getopt.gnu_getopt(sys.argv[1:], "o:u:p:x:d:r:d:D:Fh", + ["users-charset=", "users=", "prefix=", "domain=", "delta=", + "exclude=", "help", "output=", "relocate=", + "list-format"]) + except getopt.GetoptError: + usage() + sys.exit(2) + fin = sys.stdin + fout = None + users_file = None + users_charset = 'ISO8859-1' + global kill_prefix_rx, exclude, users, default_domain, reloc, max_join_delta, list_format + for o, a in opts: + if o in ("--prefix", "-p"): + kill_prefix_rx = re.compile("^" + a) + elif o in ("--exclude", "-x"): + exclude.append(a) + elif o in ("--help", "-h"): + usage() + sys.exit(0) + elif o in ("--output", "-o"): + fout = utf_open(a, "w") + elif o in ("--domain", "-d"): + default_domain = a + elif o in ("--users", "-u"): + users_file = a + elif o in ("--users-charset"): + users_charset = a + elif o in ("--relocate", "-r"): + (src, target) = a.split("=") + reloc[src] = target + elif o in ("--delta", "-D"): + max_join_delta = int(a) + elif o in ("--list-format", "-F"): + list_format = True + else: + usage() + sys.exit(2) + + if len(args) > 1: + usage() + sys.exit(2) + if len(args) == 1: + fin = open(args[0], "r") + if fout == None: + fout = utf_open("ChangeLog", "w") + + if users_file != None: + f = utf_open(users_file, "r") + for line in f.xreadlines(): + w = line.split() + if len(line) < 1 or line[0] == '#' or len(w) < 2: + continue + users[w[0]] = " ".join(w[1:]).decode(users_charset) + process(fin, fout) + +if __name__ == "__main__": + os.environ['TZ'] = 'UTC' + try: + time.tzset() + except AttributeError: + pass + process_opts() + +# vim:ts=2:sw=2:et |