You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
323 lines
9.3 KiB
323 lines
9.3 KiB
#!/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
|