| 1 |
|
|---|
| 2 |
|
|---|
| 3 |
|
|---|
| 4 |
|
|---|
| 5 |
|
|---|
| 6 |
|
|---|
| 7 |
|
|---|
| 8 |
|
|---|
| 9 |
|
|---|
| 10 |
|
|---|
| 11 |
|
|---|
| 12 |
|
|---|
| 13 |
|
|---|
| 14 |
|
|---|
| 15 |
|
|---|
| 16 |
|
|---|
| 17 |
|
|---|
| 18 |
|
|---|
| 19 |
|
|---|
| 20 |
|
|---|
| 21 |
|
|---|
| 22 |
|
|---|
| 23 |
|
|---|
| 24 |
|
|---|
| 25 |
|
|---|
| 26 |
|
|---|
| 27 |
|
|---|
| 28 |
|
|---|
| 29 |
|
|---|
| 30 |
import sys |
|---|
| 31 |
import os |
|---|
| 32 |
import time |
|---|
| 33 |
import re |
|---|
| 34 |
import getopt |
|---|
| 35 |
import string |
|---|
| 36 |
import codecs |
|---|
| 37 |
|
|---|
| 38 |
from xml.utils import qp_xml |
|---|
| 39 |
|
|---|
| 40 |
kill_prefix_rx = None |
|---|
| 41 |
default_domain = "localhost" |
|---|
| 42 |
exclude = [] |
|---|
| 43 |
users = { } |
|---|
| 44 |
reloc = { } |
|---|
| 45 |
max_join_delta = 3 * 60 |
|---|
| 46 |
list_format = False |
|---|
| 47 |
|
|---|
| 48 |
date_rx = re.compile(r"^(\d+-\d+-\d+T\d+:\d+:\d+)") |
|---|
| 49 |
|
|---|
| 50 |
def die(msg): |
|---|
| 51 |
sys.stderr.write(msg + "\n") |
|---|
| 52 |
sys.exit(1) |
|---|
| 53 |
|
|---|
| 54 |
def attr(e, n): |
|---|
| 55 |
return e.attrs[("", n)] |
|---|
| 56 |
|
|---|
| 57 |
def has_child(e, n): |
|---|
| 58 |
for c in e.children: |
|---|
| 59 |
if c.name == n: return 1 |
|---|
| 60 |
return 0 |
|---|
| 61 |
|
|---|
| 62 |
def child(e, n): |
|---|
| 63 |
for c in e.children: |
|---|
| 64 |
if c.name == n: return c |
|---|
| 65 |
die("<%s> doesn't have <%s> child" % (e.name, n)) |
|---|
| 66 |
|
|---|
| 67 |
def convert_path(n): |
|---|
| 68 |
for src in reloc.keys(): |
|---|
| 69 |
n = string.replace(n, src, reloc[src]) |
|---|
| 70 |
if kill_prefix_rx != None: |
|---|
| 71 |
if kill_prefix_rx.search(n): |
|---|
| 72 |
n = kill_prefix_rx.sub("", n) |
|---|
| 73 |
else: |
|---|
| 74 |
return None |
|---|
| 75 |
if n.startswith("/"): n = n[1:] |
|---|
| 76 |
if n == "": n = "/" |
|---|
| 77 |
for pref in exclude: |
|---|
| 78 |
if n.startswith(pref): |
|---|
| 79 |
return None |
|---|
| 80 |
return n |
|---|
| 81 |
|
|---|
| 82 |
def convert_user(u): |
|---|
| 83 |
if users.has_key(u): |
|---|
| 84 |
return users[u] |
|---|
| 85 |
else: |
|---|
| 86 |
return "%s <%s@%s>" % (u, u, default_domain) |
|---|
| 87 |
|
|---|
| 88 |
def wrap_text_line(str, pref, width): |
|---|
| 89 |
ret = u"" |
|---|
| 90 |
line = u"" |
|---|
| 91 |
first_line = True |
|---|
| 92 |
for word in str.split(): |
|---|
| 93 |
if line == u"": |
|---|
| 94 |
line = word |
|---|
| 95 |
else: |
|---|
| 96 |
if len(line + u" " + word) > width: |
|---|
| 97 |
if first_line: |
|---|
| 98 |
ret += line + u"\n" |
|---|
| 99 |
first_line = False |
|---|
| 100 |
line = word |
|---|
| 101 |
else: |
|---|
| 102 |
ret += pref + line + u"\n" |
|---|
| 103 |
line = word |
|---|
| 104 |
else: |
|---|
| 105 |
line += u" " + word |
|---|
| 106 |
if first_line: |
|---|
| 107 |
ret += line + u"\n" |
|---|
| 108 |
else: |
|---|
| 109 |
ret += pref + line + u"\n" |
|---|
| 110 |
return ret |
|---|
| 111 |
|
|---|
| 112 |
def wrap_text(str, pref, width): |
|---|
| 113 |
if not list_format: |
|---|
| 114 |
return wrap_text_line(str,pref,width) |
|---|
| 115 |
else: |
|---|
| 116 |
items = re.split(r"\-\s+",str) |
|---|
| 117 |
ret = wrap_text_line(items[0],pref,width) |
|---|
| 118 |
for item in items[1:]: |
|---|
| 119 |
ret += pref + u"- " + wrap_text_line(item,pref+" ",width) |
|---|
| 120 |
return ret |
|---|
| 121 |
|
|---|
| 122 |
class Entry: |
|---|
| 123 |
def __init__(self, tm, rev, author, msg): |
|---|
| 124 |
self.tm = tm |
|---|
| 125 |
self.rev = rev |
|---|
| 126 |
self.author = author |
|---|
| 127 |
self.msg = msg |
|---|
| 128 |
self.beg_tm = tm |
|---|
| 129 |
self.beg_rev = rev |
|---|
| 130 |
|
|---|
| 131 |
def join(self, other): |
|---|
| 132 |
self.tm = other.tm |
|---|
| 133 |
self.rev = other.rev |
|---|
| 134 |
self.msg += other.msg |
|---|
| 135 |
|
|---|
| 136 |
def dump(self, out): |
|---|
| 137 |
if self.rev != self.beg_rev: |
|---|
| 138 |
out.write("%s [r%s-%s] %s\n\n" % \ |
|---|
| 139 |
(time.strftime("%Y-%m-%d %H:%M +0000", time.localtime(self.beg_tm)), \ |
|---|
| 140 |
self.rev, self.beg_rev, convert_user(self.author))) |
|---|
| 141 |
else: |
|---|
| 142 |
out.write("%s [r%s] %s\n\n" % \ |
|---|
| 143 |
(time.strftime("%Y-%m-%d %H:%M +0000", time.localtime(self.beg_tm)), \ |
|---|
| 144 |
self.rev, convert_user(self.author))) |
|---|
| 145 |
out.write(self.msg) |
|---|
| 146 |
|
|---|
| 147 |
def can_join(self, other): |
|---|
| 148 |
return self.author == other.author and abs(self.tm - other.tm) < max_join_delta |
|---|
| 149 |
|
|---|
| 150 |
def process_entry(e): |
|---|
| 151 |
rev = attr(e, "revision") |
|---|
| 152 |
if has_child(e, "author"): |
|---|
| 153 |
author = child(e, "author").textof() |
|---|
| 154 |
else: |
|---|
| 155 |
author = "anonymous" |
|---|
| 156 |
m = date_rx.search(child(e, "date").textof()) |
|---|
| 157 |
msg = child(e, "msg").textof() |
|---|
| 158 |
if m: |
|---|
| 159 |
tm = time.mktime(time.strptime(m.group(1), "%Y-%m-%dT%H:%M:%S")) |
|---|
| 160 |
else: |
|---|
| 161 |
die("evil date: %s" % child(e, "date").textof()) |
|---|
| 162 |
paths = [] |
|---|
| 163 |
for path in child(e, "paths").children: |
|---|
| 164 |
if path.name != "path": die("<paths> has non-<path> child") |
|---|
| 165 |
nam = convert_path(path.textof()) |
|---|
| 166 |
if nam != None: |
|---|
| 167 |
if attr(path, "action") == "D": |
|---|
| 168 |
paths.append(nam + " (removed)") |
|---|
| 169 |
elif attr(path, "action") == "A": |
|---|
| 170 |
paths.append(nam + " (added)") |
|---|
| 171 |
else: |
|---|
| 172 |
paths.append(nam) |
|---|
| 173 |
|
|---|
| 174 |
if paths != []: |
|---|
| 175 |
return Entry(tm, rev, author, "\t* %s\n" % wrap_text(", ".join(paths) + ": " + msg, "\t ", 65)) |
|---|
| 176 |
|
|---|
| 177 |
return None |
|---|
| 178 |
|
|---|
| 179 |
def process(fin, fout): |
|---|
| 180 |
parser = qp_xml.Parser() |
|---|
| 181 |
root = parser.parse(fin) |
|---|
| 182 |
|
|---|
| 183 |
if root.name != "log": die("root is not <log>") |
|---|
| 184 |
|
|---|
| 185 |
cur = None |
|---|
| 186 |
|
|---|
| 187 |
for logentry in root.children: |
|---|
| 188 |
if logentry.name != "logentry": die("non <logentry> <log> child") |
|---|
| 189 |
e = process_entry(logentry) |
|---|
| 190 |
if e != None: |
|---|
| 191 |
if cur != None: |
|---|
| 192 |
if cur.can_join(e): |
|---|
| 193 |
cur.join(e) |
|---|
| 194 |
else: |
|---|
| 195 |
cur.dump(fout) |
|---|
| 196 |
cur = e |
|---|
| 197 |
else: cur = e |
|---|
| 198 |
|
|---|
| 199 |
if cur != None: cur.dump(fout) |
|---|
| 200 |
|
|---|
| 201 |
def usage(): |
|---|
| 202 |
sys.stderr.write(\ |
|---|
| 203 |
"""Usage: %s [OPTIONS] [FILE] |
|---|
| 204 |
Convert specified subversion xml logfile to GNU-style ChangeLog. |
|---|
| 205 |
|
|---|
| 206 |
Options: |
|---|
| 207 |
-p, --prefix=REGEXP set root directory of project (it will be striped off |
|---|
| 208 |
from ChangeLog entries, paths outside it will be |
|---|
| 209 |
ignored) |
|---|
| 210 |
-x, --exclude=DIR exclude DIR from ChangeLog (relative to prefix) |
|---|
| 211 |
-o, --output set output file (defaults to 'ChangeLog') |
|---|
| 212 |
-d, --domain=DOMAIN set default domain for logins not listed in users file |
|---|
| 213 |
-u, --users=FILE read logins from specified file |
|---|
| 214 |
-F, --list-format format commit logs with enumerated change list (items |
|---|
| 215 |
prefixed by '- ') |
|---|
| 216 |
-r, --relocate=X=Y before doing any other operations on paths, replace |
|---|
| 217 |
X with Y (useful for directory moves) |
|---|
| 218 |
-D, --delta=SECS when log entries differ by less then SECS seconds and |
|---|
| 219 |
have the same author -- they are merged, it defaults |
|---|
| 220 |
to 180 seconds |
|---|
| 221 |
-h, --help print this information |
|---|
| 222 |
|
|---|
| 223 |
Users file is used to map svn logins to real names to appear in ChangeLog. |
|---|
| 224 |
If login is not found in users file "login <login@domain>" is used. |
|---|
| 225 |
|
|---|
| 226 |
Example users file: |
|---|
| 227 |
john John X. Foo <jfoo@example.org> |
|---|
| 228 |
mark Marcus Blah <mb@example.org> |
|---|
| 229 |
|
|---|
| 230 |
Typical usage of this script is something like this: |
|---|
| 231 |
|
|---|
| 232 |
svn log -v --xml | %s -p '/foo/(branches/[^/]+|trunk)' -u aux/users |
|---|
| 233 |
|
|---|
| 234 |
Please send bug reports and comments to author: |
|---|
| 235 |
Michal Moskal <malekith@pld-linux.org> |
|---|
| 236 |
|
|---|
| 237 |
""" % (sys.argv[0], sys.argv[0])) |
|---|
| 238 |
|
|---|
| 239 |
def utf_open(name, mode): |
|---|
| 240 |
return codecs.open(name, mode, encoding="utf-8", errors="replace") |
|---|
| 241 |
|
|---|
| 242 |
def process_opts(): |
|---|
| 243 |
try: |
|---|
| 244 |
opts, args = getopt.gnu_getopt(sys.argv[1:], "o:u:p:x:d:r:d:D:Fh", |
|---|
| 245 |
["users=", "prefix=", "domain=", "delta=", |
|---|
| 246 |
"exclude=", "help", "output=", "relocate=", |
|---|
| 247 |
"list-format"]) |
|---|
| 248 |
except getopt.GetoptError: |
|---|
| 249 |
usage() |
|---|
| 250 |
sys.exit(2) |
|---|
| 251 |
fin = sys.stdin |
|---|
| 252 |
fout = None |
|---|
| 253 |
global kill_prefix_rx, exclude, users, default_domain, reloc, max_join_delta, list_format |
|---|
| 254 |
for o, a in opts: |
|---|
| 255 |
if o in ("--prefix", "-p"): |
|---|
| 256 |
kill_prefix_rx = re.compile("^" + a) |
|---|
| 257 |
elif o in ("--exclude", "-x"): |
|---|
| 258 |
exclude.append(a) |
|---|
| 259 |
elif o in ("--help", "-h"): |
|---|
| 260 |
usage() |
|---|
| 261 |
sys.exit(0) |
|---|
| 262 |
elif o in ("--output", "-o"): |
|---|
| 263 |
fout = open(a, "w") |
|---|
| 264 |
elif o in ("--domain", "-d"): |
|---|
| 265 |
default_domain = a |
|---|
| 266 |
elif o in ("--users", "-u"): |
|---|
| 267 |
f = utf_open(a, "r") |
|---|
| 268 |
for line in f.xreadlines(): |
|---|
| 269 |
w = line.split() |
|---|
| 270 |
if len(line) < 1 or line[0] == '#' or len(w) < 2: |
|---|
| 271 |
continue |
|---|
| 272 |
users[w[0]] = " ".join(w[1:]) |
|---|
| 273 |
elif o in ("--relocate", "-r"): |
|---|
| 274 |
(src, target) = a.split("=") |
|---|
| 275 |
reloc[src] = target |
|---|
| 276 |
elif o in ("--delta", "-D"): |
|---|
| 277 |
max_join_delta = int(a) |
|---|
| 278 |
elif o in ("--list-format", "-F"): |
|---|
| 279 |
list_format = True |
|---|
| 280 |
else: |
|---|
| 281 |
usage() |
|---|
| 282 |
sys.exit(2) |
|---|
| 283 |
if len(args) > 1: |
|---|
| 284 |
usage() |
|---|
| 285 |
sys.exit(2) |
|---|
| 286 |
if len(args) == 1: |
|---|
| 287 |
fin = open(args[0], "r") |
|---|
| 288 |
if fout == None: |
|---|
| 289 |
fout = utf_open("ChangeLog", "w") |
|---|
| 290 |
process(fin, fout) |
|---|
| 291 |
|
|---|
| 292 |
if __name__ == "__main__": |
|---|
| 293 |
os.environ['TZ'] = 'UTC' |
|---|
| 294 |
try: |
|---|
| 295 |
time.tzset() |
|---|
| 296 |
except AttributeError: |
|---|
| 297 |
pass |
|---|
| 298 |
process_opts() |
|---|