root/trunk/aux/svn2log.py

Revision 152, 8.8 kB (checked in by jajcus, 4 years ago)

- better format of ChangeLog? entries

  • Property svn:eol-style set to native
  • Property svn:executable set to *
Line 
1 #!/usr/bin/python
2 #
3 # Copyright (c) 2003 The University of Wroclaw.
4 # All rights reserved.
5 #
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions
8 # are met:
9 #    1. Redistributions of source code must retain the above copyright
10 #       notice, this list of conditions and the following disclaimer.
11 #    2. Redistributions in binary form must reproduce the above copyright
12 #       notice, this list of conditions and the following disclaimer in the
13 #       documentation and/or other materials provided with the distribution.
14 #    3. The name of the University may not be used to endorse or promote
15 #       products derived from this software without specific prior
16 #       written permission.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY ``AS IS'' AND ANY EXPRESS OR
19 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
20 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
21 # NO EVENT SHALL THE UNIVERSITY BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
23 # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
25 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
26 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
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()
Note: See TracBrowser for help on using the browser.