~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to tools/fixed-in.py

  • Committer: John Arbash Meinel
  • Date: 2013-05-19 14:29:37 UTC
  • mfrom: (6437.63.9 2.5)
  • mto: (6437.63.10 2.5)
  • mto: This revision was merged to the branch mainline in revision 6575.
  • Revision ID: john@arbash-meinel.com-20130519142937-21ykz2n2y2f22za9
Merge in the actual 2.5 branch. It seems I failed before

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()