3
# Simple script that will output the release where a given bug was fixed
4
# searching the NEWS file
11
class NewsParser(object):
13
paren_exp_re = re.compile('\(([^)]+)\)')
14
release_re = re.compile("bzr[ -]")
15
release_prefix_length = len('bzr ')
16
bugs_re = re.compile('#([0-9]+)')
18
def __init__(self, news):
20
# Temporary attributes used by the parser
23
self.may_be_release = None
24
self.release_markup = None
29
def set_line(self, line):
31
self.lrs = line.rstrip()
33
def try_release(self):
34
if self.release_re.match(self.lrs) is not None:
35
# May be a new release
36
self.may_be_release = self.lrs
37
# We know the markup will have the same length as the release
38
self.release_markup = '#' * len(self.may_be_release)
42
def confirm_release(self):
43
if self.may_be_release is not None and self.lrs == self.release_markup:
44
# The release is followed by the right markup
45
self.release = self.may_be_release[self.release_prefix_length:]
46
# Wait for the associated date
52
if self.release is None:
54
date_re = re.compile(':%s: (NOT RELEASED YET|\d{4}-\d{2}-\d{2})'
56
match = date_re.match(self.lrs)
58
self.date = match.group(1)
61
released_re = re.compile(':Released:\s+(\d{4}-\d{2}-\d{2})')
62
match = released_re.match(self.lrs)
64
self.date = match.group(1)
68
def add_line_to_entry(self):
71
self.entry += self.line
74
def extract_bugs_from_entry(self):
75
"""Possibly extract bugs from a NEWS entry and yield them.
77
Not all entries will contain bugs and some entries are even garbage and
78
we don't try to parse them (yet). The trigger is a '#' and what looks
79
like a bug number inside parens to start with. From that we extract
80
authors (when present) and multiple bugs if needed.
82
# FIXME: Malone entries are different
83
# Join all entry lines to simplify multiple line matching
84
flat_entry = ' '.join(self.entry.splitlines())
85
# Fixed bugs are always inside parens
86
for par in self.paren_exp_re.findall(flat_entry):
89
# We have at least one bug inside parens.
90
bugs = list(self.bugs_re.finditer(par))
92
# See where the first bug is mentioned
93
start = bugs[0].start()
100
authors = par[:start]
101
for bug_match in bugs:
102
bug_number = bug_match.group(0)
103
yield (bug_number, authors,
104
self.release, self.date, self.entry)
105
# We've consumed the entry
108
def parse_bugs(self):
109
for line in self.news:
111
if self.try_release():
112
continue # line may a be release
114
if self.confirm_release():
115
continue # previous line was indeed a release
117
self.may_be_release = None
119
continue # The release date has been seen
120
if self.add_line_to_entry():
121
continue # accumulate in self.enrty
122
for b in self.extract_bugs_from_entry():
123
yield b # all bugs in the news entry
126
opt_parser = optparse.OptionParser(
127
usage="""Usage: %prog [options] BUG_NUMBER
129
opt_parser.add_option(
130
'-f', '--file', type='str', dest='news_file',
131
help='NEWS file (defaults to ./NEWS)')
132
opt_parser.add_option(
133
'-m', '--message', type='str', dest='msg_re',
134
help='A regexp to search for in the news entry '
135
'(BUG_NUMBER should not be specified in this case)')
136
opt_parser.set_defaults(news_file='./NEWS')
137
(opts, args) = opt_parser.parse_args(sys.argv[1:])
138
if opts.msg_re is not None:
140
opt_parser.error('BUG_NUMBER and -m are mutually exclusive')
142
msg_re = re.compile(opts.msg_re)
144
opt_parser.error('Expected a single bug number, got %r' % args)
148
news = open(opts.news_file)
149
parser = NewsParser(news)
152
for b in parser.parse_bugs():
153
(number, authors, release, date, entry,) = b
155
entry = '\n'.join([' ' + l for l in entry.splitlines()])
158
if number[1:] == bug: # Strip the leading '#'
160
elif msg_re.search(entry) is not None:
163
print 'Bug %s was fixed in bzr-%s/%s by %s:' % (
164
number, release, date, authors)
168
print '%s bugs seen' % (seen,)