Merge lp://staging/~ted/lptools/review-changes into lp://staging/~dobey/lptools/trunk
- review-changes
- Merge into trunk
Proposed by
Ted Gould
Status: | Merged |
---|---|
Merge reported by: | dobey |
Merged at revision: | not available |
Proposed branch: | lp://staging/~ted/lptools/review-changes |
Merge into: | lp://staging/~dobey/lptools/trunk |
Diff against target: | None lines |
To merge this branch: | bzr merge lp://staging/~ted/lptools/review-changes |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jelmer Vernooij (community) | Approve | ||
dobey | Pending | ||
Review via email:
|
This proposal supersedes a proposal from 2009-08-05.
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Robert Collins (lifeless) wrote : | # |
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Robert Collins (lifeless) wrote : | # |
Rodney, ping? This would be really good to land.
I can imagine splitting out the Debian directory (in fact I think it would be appropriate to split it out), but the setup.py and so on are definitely appropriate to land.
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Robert Collins (lifeless) wrote : | # |
Bah, ignore the mention of Debian in that comment, branch confusion.
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Jelmer Vernooij (jelmer) : | # |
review:
Approve
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Jelmer Vernooij (jelmer) wrote : | # |
Ted, can you perhaps resubmit this, but targetted for lp:lptools?
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Jelmer Vernooij (jelmer) wrote : | # |
Thanks, merged! Launchpad won't recognize this MP as being merged, as it was proposed against Dobey's branch rather than against lp:lptools.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'review-notifier' |
2 | --- review-notifier 2009-08-05 18:18:15 +0000 |
3 | +++ review-notifier 2009-09-12 02:57:50 +0000 |
4 | @@ -25,22 +25,160 @@ |
5 | |
6 | import pynotify |
7 | |
8 | +from ConfigParser import ConfigParser |
9 | import os |
10 | import sys |
11 | +import subprocess |
12 | |
13 | from xdg.BaseDirectory import xdg_cache_home |
14 | +from xdg.BaseDirectory import xdg_config_home |
15 | |
16 | from launchpadlib.launchpad import Launchpad, EDGE_SERVICE_ROOT |
17 | from launchpadlib.credentials import Credentials |
18 | |
19 | +import indicate |
20 | +from time import time |
21 | + |
22 | ICON_NAME = "bzr-icon-64" |
23 | |
24 | +class Preferences(object): |
25 | + |
26 | + def __init__(self): |
27 | + self.filename = os.path.join(xdg_config_home, "lptools", |
28 | + "lptools.conf") |
29 | + self.config = ConfigParser() |
30 | + self.config.read(self.filename) |
31 | + if not os.path.isdir(os.path.dirname(self.filename)): |
32 | + os.makedirs(os.path.dirname(self.filename)) |
33 | + |
34 | + if not self.config.has_section("lptools"): |
35 | + self.config.add_section("lptools") |
36 | + |
37 | + if self.config.has_option("lptools", "projects"): |
38 | + self.projects = self.config.get("lptools", |
39 | + "projects").split(",") |
40 | + else: |
41 | + self.projects = [] |
42 | + |
43 | + if self.config.has_option("lptools", "server"): |
44 | + self.api_server = self.config.get("lptools", "server") |
45 | + else: |
46 | + self.api_server = EDGE_SERVICE_ROOT |
47 | + |
48 | + # gtk.ListStore for the dialog |
49 | + self.store = None |
50 | + self.dialog = self.__build_dialog() |
51 | + |
52 | + def __build_dialog(self): |
53 | + dialog = gtk.Dialog() |
54 | + dialog.set_title("Pending Reviews Preferences") |
55 | + dialog.set_destroy_with_parent(True) |
56 | + dialog.set_has_separator(False) |
57 | + dialog.set_default_size(240, 320) |
58 | + |
59 | + area = dialog.get_content_area() |
60 | + |
61 | + vbox = gtk.VBox(spacing=6) |
62 | + vbox.set_border_width(12) |
63 | + area.add(vbox) |
64 | + vbox.show() |
65 | + |
66 | + label = gtk.Label("<b>%s</b>" % "_Projects") |
67 | + label.set_use_underline(True) |
68 | + label.set_use_markup(True) |
69 | + label.set_alignment(0.0, 0.5) |
70 | + vbox.pack_start(label, expand=False, fill=False) |
71 | + label.show() |
72 | + |
73 | + hbox = gtk.HBox(spacing=12) |
74 | + vbox.pack_start(hbox, expand=True, fill=True) |
75 | + hbox.show() |
76 | + |
77 | + misc = gtk.Label() |
78 | + hbox.pack_start(misc, expand=False, fill=False) |
79 | + misc.show() |
80 | + |
81 | + scrollwin = gtk.ScrolledWindow() |
82 | + scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) |
83 | + hbox.pack_start(scrollwin, expand=True, fill=True) |
84 | + scrollwin.show() |
85 | + |
86 | + self.store = gtk.ListStore(str) |
87 | + |
88 | + view = gtk.TreeView(self.store) |
89 | + label.set_mnemonic_widget(view) |
90 | + view.set_headers_visible(False) |
91 | + scrollwin.add(view) |
92 | + view.show() |
93 | + |
94 | + cell = gtk.CellRendererText() |
95 | + cell.set_property("editable", True) |
96 | + cell.connect("editing_started", self.__edit_started) |
97 | + cell.connect("edited", self.__edit_finished) |
98 | + col = gtk.TreeViewColumn("Project", cell, markup=0) |
99 | + view.append_column(col) |
100 | + |
101 | + dialog.connect("close", self.__dialog_closed, 0) |
102 | + dialog.connect("response", self.__dialog_closed) |
103 | + |
104 | + return dialog |
105 | + |
106 | + def __edit_started(self, cell, editable, path): |
107 | + return |
108 | + |
109 | + def __edit_finished(self, sell, path, text): |
110 | + if text == "Click here to add a project...": |
111 | + return |
112 | + treeiter = self.store.get_iter_from_string(path) |
113 | + label = "<i>%s</i>" % "Click here to add a project..." |
114 | + self.store.set(treeiter, 0, label) |
115 | + self.projects.append(text) |
116 | + self.store.append([text,]) |
117 | + |
118 | + def __dialog_closed(self, dialog, response): |
119 | + dialog.hide() |
120 | + if len(self.projects) > 0: |
121 | + self.config.set("lptools", "projects", |
122 | + ",".join(self.projects)) |
123 | + with open(self.filename, "w+b") as f: |
124 | + self.config.write(f) |
125 | + |
126 | + def show_dialog(self, parent): |
127 | + if not self.dialog.get_transient_for(): |
128 | + self.dialog.set_transient_for(parent) |
129 | + self.store.clear() |
130 | + text = "<i>%s</i>" % "Click here to add a project..." |
131 | + self.store.append([text,]) |
132 | + if len(self.projects) != 0: |
133 | + for project in self.projects: |
134 | + self.store.append([project,]) |
135 | + self.dialog.run() |
136 | + |
137 | +def server_display (server): |
138 | + ret = subprocess.call(["./review-list"]) |
139 | + if ret != 0: |
140 | + sys.stderr.write("Failed to run './review-list'\n") |
141 | + |
142 | +def indicator_display (indicator): |
143 | + name = indicator.get_property("name") |
144 | + url = "http://code.launchpad.net/" + name + "/+activereviews" |
145 | + ret = subprocess.call(["xdg-open", url]) |
146 | + if ret != 0: |
147 | + sys.stderr.write("Failed to run 'xdg-open %s'\n" % url) |
148 | + |
149 | class Main(object): |
150 | |
151 | def __init__(self): |
152 | self.id = 0 |
153 | self.cached_candidates = {} |
154 | |
155 | + self.indicators = { } |
156 | + server = indicate.indicate_server_ref_default() |
157 | + server.set_type("message.instant") |
158 | + server.set_desktop_file(os.path.join(os.getcwd(), "review-tools.desktop")) |
159 | + server.connect("server-display", server_display) |
160 | + server.show() |
161 | + |
162 | self.cachedir = os.path.join(xdg_cache_home, "review-notifier") |
163 | credsfile = os.path.join(self.cachedir, "credentials") |
164 | |
165 | @@ -61,103 +199,131 @@ |
166 | |
167 | self.me = self.launchpad.me |
168 | |
169 | - print "Allo, %s" % self.me.name |
170 | - |
171 | - self.projects = [] |
172 | - |
173 | - for arg in sys.argv: |
174 | - if not arg.endswith("review-notifier"): |
175 | - self.projects.append(arg) |
176 | + self.project_idle_ids = {} |
177 | + |
178 | + self.config = Preferences() |
179 | + |
180 | + if len(self.config.projects) == 0: |
181 | + print "No Projects specified" |
182 | + sys.exit(1) |
183 | + |
184 | + for project in self.config.projects: |
185 | + ind = indicate.Indicator() |
186 | + ind.set_property("name", project) |
187 | + ind.set_property("count", "%d" % 0) |
188 | + ind.connect("user-display", indicator_display) |
189 | + ind.hide() |
190 | + self.indicators[project] = ind |
191 | |
192 | pynotify.init("Review Notifier") |
193 | self.timeout() |
194 | |
195 | def timeout(self): |
196 | - for project in self.projects: |
197 | - lp_project = None |
198 | - lp_focus = None |
199 | - try: |
200 | - lp_project = self.launchpad.projects[project] |
201 | - focus = lp_project.development_focus.branch |
202 | - except AttributeError: |
203 | - print "Project %s has no development focus." % project |
204 | - continue |
205 | - except KeyError: |
206 | - print "Project %s not found." % project |
207 | - continue |
208 | - |
209 | - if not focus: |
210 | - print "Project %s has no development focus." % project |
211 | - continue |
212 | - |
213 | - trunk = focus |
214 | - |
215 | - if trunk.landing_candidates: |
216 | - for c in trunk.landing_candidates: |
217 | - c_name = c.source_branch.unique_name |
218 | - status = None |
219 | - try: |
220 | - status = self.cached_candidates[c_name] |
221 | - except KeyError: |
222 | - status = None |
223 | - |
224 | - # If the proposal hasn't changed, get on with it |
225 | - if status and status == c.queue_status: |
226 | - continue |
227 | - |
228 | - self.cached_candidates[c_name] = c.queue_status |
229 | - |
230 | - n = pynotify.Notification("Review Notification") |
231 | - updated = False |
232 | - |
233 | - # Source and target branch URIs |
234 | - source = c.source_branch.display_name |
235 | - target = c.target_branch.display_name |
236 | - |
237 | - if c.queue_status == "Needs review": |
238 | - # First time we see the branch |
239 | - n.update("Branch Proposal", |
240 | - "%s has proposed merging %s into %s." % ( |
241 | - c.registrant.display_name, source, target), |
242 | - ICON_NAME) |
243 | - updated = True |
244 | - elif c.queue_status == "Approved": |
245 | - # Branch was approved |
246 | - n.update("Branch Approval", |
247 | - "%s was approved for merging into %s." % ( |
248 | - source, target), |
249 | - ICON_NAME) |
250 | - udpated = True |
251 | - elif c.queue_status == "Rejected": |
252 | - # Branch was rejected |
253 | - n.update("Branch Rejected", |
254 | - """The proposal to merge %s into %s has been rejected.""" % ( |
255 | - source, target), |
256 | - ICON_NAME) |
257 | - updated = True |
258 | - elif c.queue_status == "Merged": |
259 | - # Code has landed in the target branch |
260 | - n.update("Branch Merged", |
261 | - "%s has been merged into %s." % (source, |
262 | - target), |
263 | - ICON_NAME) |
264 | - updated = True |
265 | - else: |
266 | - print "%s status is %s." % (source, c.queue_status) |
267 | - |
268 | - if updated: |
269 | - n.set_urgency(pynotify.URGENCY_LOW) |
270 | - n.show() |
271 | - else: |
272 | - n.close() |
273 | + for project in self.config.projects: |
274 | + self.project_idle_ids[project] = gobject.idle_add(self.project_idle, project) |
275 | |
276 | return True |
277 | |
278 | + def project_idle (self, project): |
279 | + lp_project = None |
280 | + lp_focus = None |
281 | + try: |
282 | + lp_project = self.launchpad.projects[project] |
283 | + focus = lp_project.development_focus.branch |
284 | + except AttributeError: |
285 | + print "Project %s has no development focus." % project |
286 | + return False |
287 | + except KeyError: |
288 | + print "Project %s not found." % project |
289 | + return False |
290 | + |
291 | + if not focus: |
292 | + print "Project %s has no development focus." % project |
293 | + return False |
294 | + |
295 | + trunk = focus |
296 | + |
297 | + if trunk.landing_candidates: |
298 | + self.indicators[project].show() |
299 | + for c in trunk.landing_candidates: |
300 | + gobject.idle_add(self.landing_idle, project, c) |
301 | + else: |
302 | + self.indicators[project].hide() |
303 | + |
304 | + return False |
305 | + |
306 | + def landing_idle (self, project, c): |
307 | + c_name = c.source_branch.unique_name |
308 | + status = None |
309 | + try: |
310 | + status = self.cached_candidates[c_name] |
311 | + except KeyError: |
312 | + status = None |
313 | + |
314 | + # If the proposal hasn't changed, get on with it |
315 | + if status and status == c.queue_status: |
316 | + return False |
317 | + |
318 | + self.cached_candidates[c_name] = c.queue_status |
319 | + |
320 | + n = pynotify.Notification("Review Notification") |
321 | + updated = False |
322 | + |
323 | + # Source and target branch URIs |
324 | + source = c.source_branch.display_name |
325 | + target = c.target_branch.display_name |
326 | + |
327 | + if c.queue_status == "Needs review": |
328 | + # First time we see the branch |
329 | + n.update("Branch Proposal", |
330 | + "%s has proposed merging %s into %s." % ( |
331 | + c.registrant.display_name, source, target), |
332 | + ICON_NAME) |
333 | + updated = True |
334 | + elif c.queue_status == "Approved": |
335 | + # Branch was approved |
336 | + n.update("Branch Approval", |
337 | + "%s was approved for merging into %s." % ( |
338 | + source, target), |
339 | + ICON_NAME) |
340 | + udpated = True |
341 | + elif c.queue_status == "Rejected": |
342 | + # Branch was rejected |
343 | + n.update("Branch Rejected", |
344 | + """The proposal to merge %s into %s has been rejected.""" % ( |
345 | + source, target), |
346 | + ICON_NAME) |
347 | + updated = True |
348 | + elif c.queue_status == "Merged": |
349 | + # Code has landed in the target branch |
350 | + n.update("Branch Merged", |
351 | + "%s has been merged into %s." % (source, |
352 | + target), |
353 | + ICON_NAME) |
354 | + updated = True |
355 | + else: |
356 | + print "%s status is %s." % (source, c.queue_status) |
357 | + |
358 | + if updated: |
359 | + n.set_urgency(pynotify.URGENCY_LOW) |
360 | + n.set_hint("x-canonical-append", "allow") |
361 | + n.show() |
362 | + self.indicators[project].set_property_time("time", time()) |
363 | + else: |
364 | + n.close() |
365 | + |
366 | def run(self): |
367 | self.id = gobject.timeout_add_seconds(5 * 60, self.timeout) |
368 | gtk.main() |
369 | |
370 | |
371 | if __name__ == "__main__": |
372 | - foo = Main() |
373 | - foo.run() |
374 | + gobject.threads_init() |
375 | + gtk.gdk.threads_init() |
376 | + try: |
377 | + foo = Main() |
378 | + gtk.gdk.threads_enter() |
379 | + foo.run() |
380 | + gtk.gdk.threads_leave() |
381 | + except KeyboardInterrupt: |
382 | + gtk.main_quit() |
383 | |
384 | === added file 'review-tools.desktop' |
385 | --- review-tools.desktop 1970-01-01 00:00:00 +0000 |
386 | +++ review-tools.desktop 2009-08-04 23:32:09 +0000 |
387 | @@ -0,0 +1,10 @@ |
388 | +[Desktop Entry] |
389 | +Encoding=UTF-8 |
390 | +Name=Launchpad Reviews |
391 | +GenericName=Code Review Notifier |
392 | +Comment=Notify when new code reviews appear |
393 | +Exec=review-list |
394 | +StartupNotify=true |
395 | +Terminal=false |
396 | +Type=Application |
397 | +Categories=Network; |
30 + self.config. read(self. filename) isdir(os. path.dirname( self.filename) ): os.path. dirname( self.filename) )
31 + if not os.path.
32 + os.makedirs(
perhaps the read should come after the mkdir?
The call to ./review-list looks buggy: the cwd could be anything; surely you want to run 'review-list', or alternatively $libdir/ review- list, if review-list isn't meant to be user executable.
Also, tests woud be good ;)