~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to tools/fixed-in.py

Merge bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/python
2
 
 
3
 
# Simple script that will output the release where a given bug was fixed
4
 
# searching the NEWS file
5
 
 
6
 
import optparse
7
 
import re
8
 
import sys
9
 
 
10
 
 
11
 
class NewsParser(object):
12
 
 
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]+)')
17
 
 
18
 
    def __init__(self, news):
19
 
        self.news = news
20
 
        # Temporary attributes used by the parser
21
 
        self.release = None
22
 
        self.date = None
23
 
        self.may_be_release = None
24
 
        self.release_markup = None
25
 
        self.entry = ''
26
 
        self.line = None
27
 
        self.lrs = None
28
 
 
29
 
    def set_line(self, line):
30
 
        self.line = line
31
 
        self.lrs = line.rstrip()
32
 
 
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)
39
 
            return True
40
 
        return False
41
 
 
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
47
 
            self.date = None
48
 
            return True
49
 
        return False
50
 
 
51
 
    def try_date(self):
52
 
        if self.release is None:
53
 
            return False
54
 
        date_re = re.compile(':%s: (NOT RELEASED YET|\d{4}-\d{2}-\d{2})'
55
 
                             % (self.release,))
56
 
        match = date_re.match(self.lrs)
57
 
        if  match is not None:
58
 
            self.date = match.group(1)
59
 
            return True
60
 
        # The old fashion way
61
 
        released_re = re.compile(':Released:\s+(\d{4}-\d{2}-\d{2})')
62
 
        match = released_re.match(self.lrs)
63
 
        if  match is not None:
64
 
            self.date = match.group(1)
65
 
            return True
66
 
        return False
67
 
 
68
 
    def add_line_to_entry(self):
69
 
        if self.lrs == '':
70
 
            return False
71
 
        self.entry += self.line
72
 
        return True
73
 
 
74
 
    def extract_bugs_from_entry(self):
75
 
        """Possibly extract bugs from a NEWS entry and yield them.
76
 
 
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.
81
 
        """
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):
87
 
            sharp = par.find('#')
88
 
            if sharp is not None:
89
 
                # We have at least one bug inside parens.
90
 
                bugs = list(self.bugs_re.finditer(par))
91
 
                if bugs:
92
 
                    # See where the first bug is mentioned
93
 
                    start = bugs[0].start()
94
 
                    end = bugs[-1].end()
95
 
                    if start == 0:
96
 
                        # (bugs/authors)
97
 
                        authors = par[end:]
98
 
                    else:
99
 
                        # (authors/bugs)
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
106
 
        self.entry = ''
107
 
 
108
 
    def parse_bugs(self):
109
 
        for line in self.news:
110
 
            self.set_line(line)
111
 
            if self.try_release():
112
 
                continue # line may a be release
113
 
            try:
114
 
                if self.confirm_release():
115
 
                    continue # previous line was indeed a release
116
 
            finally:
117
 
                self.may_be_release = None
118
 
            if self.try_date():
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
124
 
 
125
 
def main():
126
 
    opt_parser = optparse.OptionParser(
127
 
        usage="""Usage: %prog [options] BUG_NUMBER
128
 
    """)
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:
139
 
        if len(args) != 0:
140
 
            opt_parser.error('BUG_NUMBER and -m are mutually exclusive')
141
 
        bug = None
142
 
        msg_re = re.compile(opts.msg_re)
143
 
    elif len(args) != 1:
144
 
        opt_parser.error('Expected a single bug number, got %r' % args)
145
 
    else:
146
 
        bug = args[0]
147
 
 
148
 
    news = open(opts.news_file)
149
 
    parser = NewsParser(news)
150
 
    try:
151
 
        seen = 0
152
 
        for b in parser.parse_bugs():
153
 
            (number, authors, release, date, entry,) = b
154
 
            # indent entry
155
 
            entry = '\n'.join(['    ' + l for l in entry.splitlines()])
156
 
            found = False
157
 
            if bug is not None:
158
 
                if number[1:] == bug: # Strip the leading '#'
159
 
                    found = True
160
 
            elif msg_re.search(entry) is not None:
161
 
                found = True
162
 
            if found:
163
 
                print 'Bug %s was fixed in bzr-%s/%s by %s:' % (
164
 
                    number, release, date, authors)
165
 
                print entry
166
 
            seen += 1
167
 
    finally:
168
 
        print '%s bugs seen' % (seen,)
169
 
        news.close()
170
 
 
171
 
 
172
 
main()