~ossug-hychen/laika/getbzrlogin

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
#!/usr/bin/python
#
# laika prints a summary of the Launchpad bugs you touched this week
#
# man's best friend guides you through Launchpad
# 'Laika died within hours after launch' => an aptly named tool ;)
# http://en.wikipedia.org/wiki/Laika
#
# Copyright 2010 Alex Chiang <achiang@canonical.com>
#
# This program is distributed under the terms of the
# GNU General Public License version 3.

import ConfigParser
import datetime
from optparse import OptionParser
import os
import re
import sys
from string import Template

from launchpadlib.launchpad import Launchpad

UTCNOW = datetime.datetime.utcnow()

class LPWrap:
    '''Simple wrapper Cache-Proxy for LP objects'''
    def __init__(self, lpObj):
        self.lpObj = lpObj

    def __getattr__(self, attr):
        result = getattr(self.lpObj, attr)
        # Tricky. We look at the attr, and if it is another launchpadlib
        # object, then we wrap *it* too. e.g., you asked for task.bug, where
        # bug is a launchpadlib object contained within the task lplib object.
        # Eventually, we'll just get a normal attribute that isn't an lplib
        # object, which we cache with setattr and then later retrieve with
        # the above getattr.
        if result.__class__.__name__ == "Entry":
            result = LPWrap(result)
        setattr(self, attr, result)
        return result

class Report(object):
    '''An activity report for a specified Launchpad user.

    In order of decreasing importance, search and display activity on
    bugs assigned to me, then on bugs that I actually commented upon,
    and finally bugs that I just opened, but did nothing further with.

    Avoids duplication of activity. So if the status was changed on
    a bug assigned to you and you also commented on it, only report the
    status change.
    '''

    bugs = {}

    def __init__(self, user, window, bugpattern, ppas):
        cachedir = os.path.expanduser("~/.launchpadlib/cache")
        self.launchpad = Launchpad.login_with('laika', 'production', cachedir)

        self.user = self.launchpad.people[user]
        self.window = window
        self.since = UTCNOW - datetime.timedelta(window)
        self.bugpattern = bugpattern
        self.ppas = ppas
        self.status = ["New",
                       "Incomplete",
                       "Invalid",
                       "Won't Fix",
                       "Confirmed",
                       "Triaged",
                       "In Progress",
                       "Fix Committed",
                       "Fix Released" ]

    def print_header(self, header):
        print "==", header, "=="

    def in_window(self, date):
        '''Timezones do not exist, all datetime objects have to be naive.

        Time zone aware means broken.
        http://www.enricozini.org/2009/debian/using-python-datetime/
        '''
        win = datetime.timedelta(self.window)
        date = date.replace(tzinfo=None)
        delta = UTCNOW - date
        return delta <= win

    def print_bugid(self, task):
        '''Using this interface adds the bug to global bug list.'''
        ago = ""
        self.bugs[task.bug.id] = 1
        delta = UTCNOW - task.bug.date_last_updated.replace(tzinfo=None)
        if delta.days > 0:
            ago = "%d day%s" % (delta.days, "s" if delta.days > 1 else "")

        hours = delta.seconds / 3600
        if hours > 0:
            ago += ", " if ago else ""
            ago += "%d hour%s" % (hours, "s" if hours > 1 else "")

        minutes = (delta.seconds - (hours * 3600)) / 60
        if minutes > 0:
            ago += ", " if ago else ""
            ago += "%d minute%s" % (minutes, "s" if minutes > 1 else "")

        print task.title
        t = Template(self.bugpattern)
        print t.substitute(bugid=str(task.bug.id))
        print "last updated", ago, "ago"

    def print_assignments(self):
        statuses = ['closed', 'fix_released', 'fix_committed',
                'in_progress', 'triaged', 'confirmed', 'created']

        tasks = self.user.searchTasks(assignee=self.user,
                                      status=self.status,
                                      modified_since=self.since)
        tasks = [LPWrap(t) for t in tasks]
        self.print_header("Assigned Bugs")

        for t in tasks:
            if self.bugs.has_key(t.bug.id):
                continue
            updates = []
            for s in statuses:
                attr = 'date_' + s
                date = getattr(t, attr)
                if not date:
                    continue
                if self.in_window(date):
                    updates.append(
                        "\t" + s + ": " + re.sub("\s.*$", "", str(date)))

            if updates:
                self.print_bugid(t)
                for u in updates:
                    print u
                print
        print

    def print_comments(self):
        tasks = self.user.searchTasks(bug_commenter=self.user,
                                      status=self.status,
                                      modified_since=self.since)
        tasks = [LPWrap(t) for t in tasks]
        self.print_header("Commented Bugs")
        for t in tasks:
            if self.bugs.has_key(t.bug.id):
                continue
            for m in t.bug.messages:
                if m.owner_link != self.user.self_link:
                    continue
                if self.in_window(m.date_created):
                    self.print_bugid(t)
                    print
                    break
        print

    def print_reported(self):
        tasks = self.user.searchTasks(bug_reporter=self.user,
                                      status=self.status,
                                      modified_since=self.since)
        tasks = [LPWrap(t) for t in tasks]
        self.print_header("Reported Bugs")
        for t in tasks:
            if self.bugs.has_key(t.bug.id):
                continue
            if self.in_window(t.bug.date_created):
                self.print_bugid(t)
                print

    def print_ppa_activity(self):
        if self.ppas == '':
            return

        # Handle command line case, where user passes in comma-separated list
        self.ppas = re.sub(",", "\n", self.ppas)

        # Now each PPA is separated by a newline, which is how we might
        # expect them if they're specified in the config file.
        ppas = [p for p in self.ppas.split("\n") if p != '']

        self.print_header("PPA Activity")
        for ppa in ppas:
            # A PPA seems to always belong to a person (or a team, which
            # is a person in LP) so assuming "person/PPA" sounds reasonable.
            person, archive = ppa.split('/')
            owner = self.launchpad.people[person]
            archive = owner.getPPAByName(name=archive)
            sources = archive.getPublishedSources()

            print ppa
            for s in sources:
                if s.package_signer != self.user:
                    continue

                # There might be yet-unpublished packages in the PPA, which
                # would then have NoneType for date_published. Don't check
                # in_window in that case.
                if s.date_published:
                    if self.in_window(s.date_published):
                        print "\t", s.display_name
            print
        print

    def render(self):
        self.print_assignments()
        self.print_comments()
        self.print_reported()
        self.print_ppa_activity()

def get_config(opts):
    config = {}
    FORMAT = "1.0"
    laika_config = ConfigParser.ConfigParser()
    laika_config.read(os.path.expanduser("~/.laikarc"))

    def get_bzrlplogin():
        path = os.path.expanduser('~/.bazaar/bazaar.conf')
        if not os.path.exists(path):
            return None
        else:
            cfg = ConfigParser.ConfigParser()
            cfg.read(os.path.expanduser('~/.bazaar/bazaar.conf'))
            return cfg.get('DEFAULT', 'launchpad_username')
    def default_user():
        return get_bzrlplogin() or os.getenv("USER")
    def default_window():
        return 8
    def default_bugpattern():
        return 'https://launchpad.net/bugs/$bugid'
    def default_ppas():
        return ''

    for opt in opts:
        try:
            config[opt] = laika_config.get(FORMAT, opt)
        except:
            config[opt] = locals()["default_%s" % opt]()

    return config

def main():
    opts = ['user', 'window', 'bugpattern', 'ppas']
    config = get_config(opts)

    parser = OptionParser()
    parser.add_option('-u', '--user', dest='user',
        default=config['user'],
        help='Specify the Launchpad user id.  '
             'Defaults to session username.')
    parser.add_option('-w', '--window', dest='window', type='int',
        default=config['window'],
        help='Number of days of past activity to look for.  '
             'Defaults to 8 days')
    parser.add_option('-b', '--bug-pattern', dest='bugpattern', type='string',
        default=config['bugpattern'],
        help='Use this string when displaying a bug reference.  '
             'Occurrences of $bugid will be replaced by the actual bug number.  '
             'Defaults to \'https://launchpad.net/bugs/$bugid\'')
    parser.add_option('--ppas', dest='ppas',
        default=config['ppas'],
        help='Search for activity in specified PPAs')

    options, arguments = parser.parse_args()

    report = Report(options.user, options.window, options.bugpattern,
                    options.ppas)
    report.render()

if __name__ == "__main__":
    main()