1 |
commit: 520c9782541d2e3fa509b1a2d470889a6d26bef7 |
2 |
Author: Pawel Hajdan, Jr <phajdan.jr <AT> gentoo <DOT> org> |
3 |
AuthorDate: Wed May 30 14:34:40 2012 +0000 |
4 |
Commit: Paweł Hajdan <phajdan.jr <AT> gentoo <DOT> org> |
5 |
CommitDate: Wed May 30 14:34:40 2012 +0000 |
6 |
URL: http://git.overlays.gentoo.org/gitweb/?p=proj/arch-tools.git;a=commit;h=520c9782 |
7 |
|
8 |
Make bugzilla-viewer and maintainer-timeout work |
9 |
|
10 |
by bundling old pybugz. |
11 |
|
12 |
--- |
13 |
bugzilla-viewer.py | 2 + |
14 |
maintainer-timeout.py | 4 + |
15 |
stabilization-candidates.py | 35 +- |
16 |
third_party/pybugz-0.9.3/LICENSE | 340 +++++++++ |
17 |
third_party/pybugz-0.9.3/README | 107 +++ |
18 |
third_party/pybugz-0.9.3/bin/bugz | 393 ++++++++++ |
19 |
third_party/pybugz-0.9.3/bugz/__init__.py | 31 + |
20 |
third_party/pybugz-0.9.3/bugz/bugzilla.py | 862 ++++++++++++++++++++++ |
21 |
third_party/pybugz-0.9.3/bugz/cli.py | 607 +++++++++++++++ |
22 |
third_party/pybugz-0.9.3/bugz/config.py | 229 ++++++ |
23 |
third_party/pybugz-0.9.3/bugzrc.example | 25 + |
24 |
third_party/pybugz-0.9.3/contrib/bash-completion | 66 ++ |
25 |
third_party/pybugz-0.9.3/contrib/zsh-completion | 158 ++++ |
26 |
third_party/pybugz-0.9.3/man/bugz.1 | 41 + |
27 |
third_party/pybugz-0.9.3/setup.py | 15 + |
28 |
15 files changed, 2901 insertions(+), 14 deletions(-) |
29 |
|
30 |
diff --git a/bugzilla-viewer.py b/bugzilla-viewer.py |
31 |
index 8a1e131..76daabf 100755 |
32 |
--- a/bugzilla-viewer.py |
33 |
+++ b/bugzilla-viewer.py |
34 |
@@ -12,6 +12,8 @@ import sys |
35 |
import textwrap |
36 |
import xml.etree |
37 |
|
38 |
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'third_party', 'pybugz-0.9.3')) |
39 |
+ |
40 |
import bugz.bugzilla |
41 |
import portage.versions |
42 |
|
43 |
|
44 |
diff --git a/maintainer-timeout.py b/maintainer-timeout.py |
45 |
index c825f5d..6287bec 100755 |
46 |
--- a/maintainer-timeout.py |
47 |
+++ b/maintainer-timeout.py |
48 |
@@ -4,6 +4,10 @@ |
49 |
|
50 |
import datetime |
51 |
import optparse |
52 |
+import os.path |
53 |
+import sys |
54 |
+ |
55 |
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'third_party', 'pybugz-0.9.3')) |
56 |
|
57 |
import bugz.bugzilla |
58 |
import portage.versions |
59 |
|
60 |
diff --git a/stabilization-candidates.py b/stabilization-candidates.py |
61 |
index 04b6dee..7989a84 100755 |
62 |
--- a/stabilization-candidates.py |
63 |
+++ b/stabilization-candidates.py |
64 |
@@ -51,7 +51,7 @@ if __name__ == "__main__": |
65 |
best_stable = portage.versions.best(portage.portdb.match(cp)) |
66 |
if not best_stable: |
67 |
continue |
68 |
- print 'Working on %s...' % cp |
69 |
+ print 'Working on %s...' % cp, |
70 |
candidates = [] |
71 |
for cpv in portage.portdb.cp_list(cp): |
72 |
# Only consider higher versions than best stable. |
73 |
@@ -79,6 +79,7 @@ if __name__ == "__main__": |
74 |
|
75 |
candidates.append(cpv) |
76 |
if not candidates: |
77 |
+ print 'no candidates' |
78 |
continue |
79 |
|
80 |
candidates.sort(key=portage.versions.cpv_sort_key()) |
81 |
@@ -94,9 +95,11 @@ if __name__ == "__main__": |
82 |
regex = '\*%s \((.*)\)' % re.escape(pv) |
83 |
match = re.search(regex, changelog_file.read()) |
84 |
if not match: |
85 |
+ print 'error parsing ChangeLog' |
86 |
continue |
87 |
changelog_date = datetime.datetime.strptime(match.group(1), '%d %b %Y') |
88 |
if now - changelog_date < datetime.timedelta(days=options.days): |
89 |
+ print 'not old enough' |
90 |
continue |
91 |
|
92 |
keywords = portage.db["/"]["porttree"].dbapi.aux_get(best_candidate, ['KEYWORDS'])[0] |
93 |
@@ -106,6 +109,22 @@ if __name__ == "__main__": |
94 |
missing_arch = True |
95 |
break |
96 |
if missing_arch: |
97 |
+ print 'not keyworded ~arch' |
98 |
+ continue |
99 |
+ |
100 |
+ # Do not risk trying to stabilize a package with known bugs. |
101 |
+ params = {} |
102 |
+ params['summary'] = [cp]; |
103 |
+ bugs = bugzilla.Bug.search(params) |
104 |
+ if len(bugs['bugs']): |
105 |
+ print 'has bugs' |
106 |
+ continue |
107 |
+ |
108 |
+ # Protection against filing a stabilization bug twice. |
109 |
+ params['summary'] = [best_candidate] |
110 |
+ bugs = bugzilla.Bug.search(params) |
111 |
+ if len(bugs['bugs']): |
112 |
+ print 'version has closed bugs' |
113 |
continue |
114 |
|
115 |
cvs_path = os.path.join(options.repo, cp) |
116 |
@@ -124,6 +143,7 @@ if __name__ == "__main__": |
117 |
subprocess.check_output(["repoman", "manifest"], cwd=cvs_path) |
118 |
subprocess.check_output(["repoman", "full"], cwd=cvs_path) |
119 |
except subprocess.CalledProcessError: |
120 |
+ print 'repoman error' |
121 |
continue |
122 |
finally: |
123 |
f = open(ebuild_path, "w") |
124 |
@@ -133,19 +153,6 @@ if __name__ == "__main__": |
125 |
f.write(manifest_contents) |
126 |
f.close() |
127 |
|
128 |
- # Do not risk trying to stabilize a package with known bugs. |
129 |
- params = {} |
130 |
- params['summary'] = [cp]; |
131 |
- bugs = bugzilla.Bug.search(params) |
132 |
- if len(bugs['bugs']): |
133 |
- continue |
134 |
- |
135 |
- # Protection against filing a stabilization bug twice. |
136 |
- params['summary'] = [best_candidate] |
137 |
- bugs = bugzilla.Bug.search(params) |
138 |
- if len(bugs['bugs']): |
139 |
- continue |
140 |
- |
141 |
metadata = MetaDataXML(os.path.join(cvs_path, 'metadata.xml'), '/usr/portage/metadata/herds.xml') |
142 |
maintainer_split = metadata.format_maintainer_string().split(' ', 1) |
143 |
maintainer = maintainer_split[0] |
144 |
|
145 |
diff --git a/third_party/pybugz-0.9.3/LICENSE b/third_party/pybugz-0.9.3/LICENSE |
146 |
new file mode 100644 |
147 |
index 0000000..3912109 |
148 |
--- /dev/null |
149 |
+++ b/third_party/pybugz-0.9.3/LICENSE |
150 |
@@ -0,0 +1,340 @@ |
151 |
+ GNU GENERAL PUBLIC LICENSE |
152 |
+ Version 2, June 1991 |
153 |
+ |
154 |
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc. |
155 |
+ 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
156 |
+ Everyone is permitted to copy and distribute verbatim copies |
157 |
+ of this license document, but changing it is not allowed. |
158 |
+ |
159 |
+ Preamble |
160 |
+ |
161 |
+ The licenses for most software are designed to take away your |
162 |
+freedom to share and change it. By contrast, the GNU General Public |
163 |
+License is intended to guarantee your freedom to share and change free |
164 |
+software--to make sure the software is free for all its users. This |
165 |
+General Public License applies to most of the Free Software |
166 |
+Foundation's software and to any other program whose authors commit to |
167 |
+using it. (Some other Free Software Foundation software is covered by |
168 |
+the GNU Library General Public License instead.) You can apply it to |
169 |
+your programs, too. |
170 |
+ |
171 |
+ When we speak of free software, we are referring to freedom, not |
172 |
+price. Our General Public Licenses are designed to make sure that you |
173 |
+have the freedom to distribute copies of free software (and charge for |
174 |
+this service if you wish), that you receive source code or can get it |
175 |
+if you want it, that you can change the software or use pieces of it |
176 |
+in new free programs; and that you know you can do these things. |
177 |
+ |
178 |
+ To protect your rights, we need to make restrictions that forbid |
179 |
+anyone to deny you these rights or to ask you to surrender the rights. |
180 |
+These restrictions translate to certain responsibilities for you if you |
181 |
+distribute copies of the software, or if you modify it. |
182 |
+ |
183 |
+ For example, if you distribute copies of such a program, whether |
184 |
+gratis or for a fee, you must give the recipients all the rights that |
185 |
+you have. You must make sure that they, too, receive or can get the |
186 |
+source code. And you must show them these terms so they know their |
187 |
+rights. |
188 |
+ |
189 |
+ We protect your rights with two steps: (1) copyright the software, and |
190 |
+(2) offer you this license which gives you legal permission to copy, |
191 |
+distribute and/or modify the software. |
192 |
+ |
193 |
+ Also, for each author's protection and ours, we want to make certain |
194 |
+that everyone understands that there is no warranty for this free |
195 |
+software. If the software is modified by someone else and passed on, we |
196 |
+want its recipients to know that what they have is not the original, so |
197 |
+that any problems introduced by others will not reflect on the original |
198 |
+authors' reputations. |
199 |
+ |
200 |
+ Finally, any free program is threatened constantly by software |
201 |
+patents. We wish to avoid the danger that redistributors of a free |
202 |
+program will individually obtain patent licenses, in effect making the |
203 |
+program proprietary. To prevent this, we have made it clear that any |
204 |
+patent must be licensed for everyone's free use or not licensed at all. |
205 |
+ |
206 |
+ The precise terms and conditions for copying, distribution and |
207 |
+modification follow. |
208 |
+ |
209 |
+ GNU GENERAL PUBLIC LICENSE |
210 |
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION |
211 |
+ |
212 |
+ 0. This License applies to any program or other work which contains |
213 |
+a notice placed by the copyright holder saying it may be distributed |
214 |
+under the terms of this General Public License. The "Program", below, |
215 |
+refers to any such program or work, and a "work based on the Program" |
216 |
+means either the Program or any derivative work under copyright law: |
217 |
+that is to say, a work containing the Program or a portion of it, |
218 |
+either verbatim or with modifications and/or translated into another |
219 |
+language. (Hereinafter, translation is included without limitation in |
220 |
+the term "modification".) Each licensee is addressed as "you". |
221 |
+ |
222 |
+Activities other than copying, distribution and modification are not |
223 |
+covered by this License; they are outside its scope. The act of |
224 |
+running the Program is not restricted, and the output from the Program |
225 |
+is covered only if its contents constitute a work based on the |
226 |
+Program (independent of having been made by running the Program). |
227 |
+Whether that is true depends on what the Program does. |
228 |
+ |
229 |
+ 1. You may copy and distribute verbatim copies of the Program's |
230 |
+source code as you receive it, in any medium, provided that you |
231 |
+conspicuously and appropriately publish on each copy an appropriate |
232 |
+copyright notice and disclaimer of warranty; keep intact all the |
233 |
+notices that refer to this License and to the absence of any warranty; |
234 |
+and give any other recipients of the Program a copy of this License |
235 |
+along with the Program. |
236 |
+ |
237 |
+You may charge a fee for the physical act of transferring a copy, and |
238 |
+you may at your option offer warranty protection in exchange for a fee. |
239 |
+ |
240 |
+ 2. You may modify your copy or copies of the Program or any portion |
241 |
+of it, thus forming a work based on the Program, and copy and |
242 |
+distribute such modifications or work under the terms of Section 1 |
243 |
+above, provided that you also meet all of these conditions: |
244 |
+ |
245 |
+ a) You must cause the modified files to carry prominent notices |
246 |
+ stating that you changed the files and the date of any change. |
247 |
+ |
248 |
+ b) You must cause any work that you distribute or publish, that in |
249 |
+ whole or in part contains or is derived from the Program or any |
250 |
+ part thereof, to be licensed as a whole at no charge to all third |
251 |
+ parties under the terms of this License. |
252 |
+ |
253 |
+ c) If the modified program normally reads commands interactively |
254 |
+ when run, you must cause it, when started running for such |
255 |
+ interactive use in the most ordinary way, to print or display an |
256 |
+ announcement including an appropriate copyright notice and a |
257 |
+ notice that there is no warranty (or else, saying that you provide |
258 |
+ a warranty) and that users may redistribute the program under |
259 |
+ these conditions, and telling the user how to view a copy of this |
260 |
+ License. (Exception: if the Program itself is interactive but |
261 |
+ does not normally print such an announcement, your work based on |
262 |
+ the Program is not required to print an announcement.) |
263 |
+ |
264 |
+These requirements apply to the modified work as a whole. If |
265 |
+identifiable sections of that work are not derived from the Program, |
266 |
+and can be reasonably considered independent and separate works in |
267 |
+themselves, then this License, and its terms, do not apply to those |
268 |
+sections when you distribute them as separate works. But when you |
269 |
+distribute the same sections as part of a whole which is a work based |
270 |
+on the Program, the distribution of the whole must be on the terms of |
271 |
+this License, whose permissions for other licensees extend to the |
272 |
+entire whole, and thus to each and every part regardless of who wrote it. |
273 |
+ |
274 |
+Thus, it is not the intent of this section to claim rights or contest |
275 |
+your rights to work written entirely by you; rather, the intent is to |
276 |
+exercise the right to control the distribution of derivative or |
277 |
+collective works based on the Program. |
278 |
+ |
279 |
+In addition, mere aggregation of another work not based on the Program |
280 |
+with the Program (or with a work based on the Program) on a volume of |
281 |
+a storage or distribution medium does not bring the other work under |
282 |
+the scope of this License. |
283 |
+ |
284 |
+ 3. You may copy and distribute the Program (or a work based on it, |
285 |
+under Section 2) in object code or executable form under the terms of |
286 |
+Sections 1 and 2 above provided that you also do one of the following: |
287 |
+ |
288 |
+ a) Accompany it with the complete corresponding machine-readable |
289 |
+ source code, which must be distributed under the terms of Sections |
290 |
+ 1 and 2 above on a medium customarily used for software interchange; or, |
291 |
+ |
292 |
+ b) Accompany it with a written offer, valid for at least three |
293 |
+ years, to give any third party, for a charge no more than your |
294 |
+ cost of physically performing source distribution, a complete |
295 |
+ machine-readable copy of the corresponding source code, to be |
296 |
+ distributed under the terms of Sections 1 and 2 above on a medium |
297 |
+ customarily used for software interchange; or, |
298 |
+ |
299 |
+ c) Accompany it with the information you received as to the offer |
300 |
+ to distribute corresponding source code. (This alternative is |
301 |
+ allowed only for noncommercial distribution and only if you |
302 |
+ received the program in object code or executable form with such |
303 |
+ an offer, in accord with Subsection b above.) |
304 |
+ |
305 |
+The source code for a work means the preferred form of the work for |
306 |
+making modifications to it. For an executable work, complete source |
307 |
+code means all the source code for all modules it contains, plus any |
308 |
+associated interface definition files, plus the scripts used to |
309 |
+control compilation and installation of the executable. However, as a |
310 |
+special exception, the source code distributed need not include |
311 |
+anything that is normally distributed (in either source or binary |
312 |
+form) with the major components (compiler, kernel, and so on) of the |
313 |
+operating system on which the executable runs, unless that component |
314 |
+itself accompanies the executable. |
315 |
+ |
316 |
+If distribution of executable or object code is made by offering |
317 |
+access to copy from a designated place, then offering equivalent |
318 |
+access to copy the source code from the same place counts as |
319 |
+distribution of the source code, even though third parties are not |
320 |
+compelled to copy the source along with the object code. |
321 |
+ |
322 |
+ 4. You may not copy, modify, sublicense, or distribute the Program |
323 |
+except as expressly provided under this License. Any attempt |
324 |
+otherwise to copy, modify, sublicense or distribute the Program is |
325 |
+void, and will automatically terminate your rights under this License. |
326 |
+However, parties who have received copies, or rights, from you under |
327 |
+this License will not have their licenses terminated so long as such |
328 |
+parties remain in full compliance. |
329 |
+ |
330 |
+ 5. You are not required to accept this License, since you have not |
331 |
+signed it. However, nothing else grants you permission to modify or |
332 |
+distribute the Program or its derivative works. These actions are |
333 |
+prohibited by law if you do not accept this License. Therefore, by |
334 |
+modifying or distributing the Program (or any work based on the |
335 |
+Program), you indicate your acceptance of this License to do so, and |
336 |
+all its terms and conditions for copying, distributing or modifying |
337 |
+the Program or works based on it. |
338 |
+ |
339 |
+ 6. Each time you redistribute the Program (or any work based on the |
340 |
+Program), the recipient automatically receives a license from the |
341 |
+original licensor to copy, distribute or modify the Program subject to |
342 |
+these terms and conditions. You may not impose any further |
343 |
+restrictions on the recipients' exercise of the rights granted herein. |
344 |
+You are not responsible for enforcing compliance by third parties to |
345 |
+this License. |
346 |
+ |
347 |
+ 7. If, as a consequence of a court judgment or allegation of patent |
348 |
+infringement or for any other reason (not limited to patent issues), |
349 |
+conditions are imposed on you (whether by court order, agreement or |
350 |
+otherwise) that contradict the conditions of this License, they do not |
351 |
+excuse you from the conditions of this License. If you cannot |
352 |
+distribute so as to satisfy simultaneously your obligations under this |
353 |
+License and any other pertinent obligations, then as a consequence you |
354 |
+may not distribute the Program at all. For example, if a patent |
355 |
+license would not permit royalty-free redistribution of the Program by |
356 |
+all those who receive copies directly or indirectly through you, then |
357 |
+the only way you could satisfy both it and this License would be to |
358 |
+refrain entirely from distribution of the Program. |
359 |
+ |
360 |
+If any portion of this section is held invalid or unenforceable under |
361 |
+any particular circumstance, the balance of the section is intended to |
362 |
+apply and the section as a whole is intended to apply in other |
363 |
+circumstances. |
364 |
+ |
365 |
+It is not the purpose of this section to induce you to infringe any |
366 |
+patents or other property right claims or to contest validity of any |
367 |
+such claims; this section has the sole purpose of protecting the |
368 |
+integrity of the free software distribution system, which is |
369 |
+implemented by public license practices. Many people have made |
370 |
+generous contributions to the wide range of software distributed |
371 |
+through that system in reliance on consistent application of that |
372 |
+system; it is up to the author/donor to decide if he or she is willing |
373 |
+to distribute software through any other system and a licensee cannot |
374 |
+impose that choice. |
375 |
+ |
376 |
+This section is intended to make thoroughly clear what is believed to |
377 |
+be a consequence of the rest of this License. |
378 |
+ |
379 |
+ 8. If the distribution and/or use of the Program is restricted in |
380 |
+certain countries either by patents or by copyrighted interfaces, the |
381 |
+original copyright holder who places the Program under this License |
382 |
+may add an explicit geographical distribution limitation excluding |
383 |
+those countries, so that distribution is permitted only in or among |
384 |
+countries not thus excluded. In such case, this License incorporates |
385 |
+the limitation as if written in the body of this License. |
386 |
+ |
387 |
+ 9. The Free Software Foundation may publish revised and/or new versions |
388 |
+of the General Public License from time to time. Such new versions will |
389 |
+be similar in spirit to the present version, but may differ in detail to |
390 |
+address new problems or concerns. |
391 |
+ |
392 |
+Each version is given a distinguishing version number. If the Program |
393 |
+specifies a version number of this License which applies to it and "any |
394 |
+later version", you have the option of following the terms and conditions |
395 |
+either of that version or of any later version published by the Free |
396 |
+Software Foundation. If the Program does not specify a version number of |
397 |
+this License, you may choose any version ever published by the Free Software |
398 |
+Foundation. |
399 |
+ |
400 |
+ 10. If you wish to incorporate parts of the Program into other free |
401 |
+programs whose distribution conditions are different, write to the author |
402 |
+to ask for permission. For software which is copyrighted by the Free |
403 |
+Software Foundation, write to the Free Software Foundation; we sometimes |
404 |
+make exceptions for this. Our decision will be guided by the two goals |
405 |
+of preserving the free status of all derivatives of our free software and |
406 |
+of promoting the sharing and reuse of software generally. |
407 |
+ |
408 |
+ NO WARRANTY |
409 |
+ |
410 |
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY |
411 |
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN |
412 |
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES |
413 |
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED |
414 |
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
415 |
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS |
416 |
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE |
417 |
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, |
418 |
+REPAIR OR CORRECTION. |
419 |
+ |
420 |
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING |
421 |
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR |
422 |
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, |
423 |
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING |
424 |
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED |
425 |
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY |
426 |
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER |
427 |
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE |
428 |
+POSSIBILITY OF SUCH DAMAGES. |
429 |
+ |
430 |
+ END OF TERMS AND CONDITIONS |
431 |
+ |
432 |
+ How to Apply These Terms to Your New Programs |
433 |
+ |
434 |
+ If you develop a new program, and you want it to be of the greatest |
435 |
+possible use to the public, the best way to achieve this is to make it |
436 |
+free software which everyone can redistribute and change under these terms. |
437 |
+ |
438 |
+ To do so, attach the following notices to the program. It is safest |
439 |
+to attach them to the start of each source file to most effectively |
440 |
+convey the exclusion of warranty; and each file should have at least |
441 |
+the "copyright" line and a pointer to where the full notice is found. |
442 |
+ |
443 |
+ <one line to give the program's name and a brief idea of what it does.> |
444 |
+ Copyright (C) <year> <name of author> |
445 |
+ |
446 |
+ This program is free software; you can redistribute it and/or modify |
447 |
+ it under the terms of the GNU General Public License as published by |
448 |
+ the Free Software Foundation; either version 2 of the License, or |
449 |
+ (at your option) any later version. |
450 |
+ |
451 |
+ This program is distributed in the hope that it will be useful, |
452 |
+ but WITHOUT ANY WARRANTY; without even the implied warranty of |
453 |
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
454 |
+ GNU General Public License for more details. |
455 |
+ |
456 |
+ You should have received a copy of the GNU General Public License |
457 |
+ along with this program; if not, write to the Free Software |
458 |
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
459 |
+ |
460 |
+ |
461 |
+Also add information on how to contact you by electronic and paper mail. |
462 |
+ |
463 |
+If the program is interactive, make it output a short notice like this |
464 |
+when it starts in an interactive mode: |
465 |
+ |
466 |
+ Gnomovision version 69, Copyright (C) year name of author |
467 |
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. |
468 |
+ This is free software, and you are welcome to redistribute it |
469 |
+ under certain conditions; type `show c' for details. |
470 |
+ |
471 |
+The hypothetical commands `show w' and `show c' should show the appropriate |
472 |
+parts of the General Public License. Of course, the commands you use may |
473 |
+be called something other than `show w' and `show c'; they could even be |
474 |
+mouse-clicks or menu items--whatever suits your program. |
475 |
+ |
476 |
+You should also get your employer (if you work as a programmer) or your |
477 |
+school, if any, to sign a "copyright disclaimer" for the program, if |
478 |
+necessary. Here is a sample; alter the names: |
479 |
+ |
480 |
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program |
481 |
+ `Gnomovision' (which makes passes at compilers) written by James Hacker. |
482 |
+ |
483 |
+ <signature of Ty Coon>, 1 April 1989 |
484 |
+ Ty Coon, President of Vice |
485 |
+ |
486 |
+This General Public License does not permit incorporating your program into |
487 |
+proprietary programs. If your program is a subroutine library, you may |
488 |
+consider it more useful to permit linking proprietary applications with the |
489 |
+library. If this is what you want to do, use the GNU Library General |
490 |
+Public License instead of this License. |
491 |
|
492 |
diff --git a/third_party/pybugz-0.9.3/README b/third_party/pybugz-0.9.3/README |
493 |
new file mode 100644 |
494 |
index 0000000..423566d |
495 |
--- /dev/null |
496 |
+++ b/third_party/pybugz-0.9.3/README |
497 |
@@ -0,0 +1,107 @@ |
498 |
+PyBugz - Python Bugzilla Interface |
499 |
+---------------------------------- |
500 |
+ |
501 |
+Bugzilla has a very inefficient user interface, so I've written a |
502 |
+command line utility to interact with it. This is mainly done to help |
503 |
+me with closing bugs on Gentoo Bugzilla by grabbing patches, ebuilds |
504 |
+and so on. |
505 |
+ |
506 |
+Author |
507 |
+------ |
508 |
+Alastair Tse <alastair@×××××××.net>. Copyright (c) 2006 under GPL-2. |
509 |
+ |
510 |
+Features |
511 |
+-------- |
512 |
+* Searching bugzilla |
513 |
+* Listing details of a bug including comments and attachments |
514 |
+* Downloading/viewing attachments from bugzilla |
515 |
+* Posting bugs, comments, and making changes to an existing bug. |
516 |
+* Adding attachments to a bug. |
517 |
+ |
518 |
+Configuration File |
519 |
+------------------ |
520 |
+ |
521 |
+pybugz supports a configuration file which allows you to define settings |
522 |
+for multiple bugzilla connections then refer to them by name from the |
523 |
+command line. The default is for this file to be named .bugzrc and |
524 |
+stored in your home directory. An example of this file and the settings |
525 |
+is included in this distribution as bugzrc.example. |
526 |
+ |
527 |
+Usage/Workflow |
528 |
+-------------- |
529 |
+ |
530 |
+PyBugz comes with a command line interface called "bugz". It's |
531 |
+operation is similar in style to cvs/svn where a subcommand is |
532 |
+required for operation. |
533 |
+ |
534 |
+To explain how it works, I will use a typical workflow for Gentoo |
535 |
+development. |
536 |
+ |
537 |
+1) Searching bugzilla for bugs I can fix, I'll run the command: |
538 |
+--------------------------------------------------------------- |
539 |
+ |
540 |
+$ bugz search "version bump" --assigned liquidx@g.o |
541 |
+ |
542 |
+ * Using http://bugs.gentoo.org/ .. |
543 |
+ * Searching for "version bump" ordered by "number" |
544 |
+ 101968 liquidx net-im/msnlib version bump |
545 |
+ 125468 liquidx version bump for dev-libs/g-wrap-1.9.6 |
546 |
+ 130608 liquidx app-dicts/stardict version bump: 2.4.7 |
547 |
+ |
548 |
+2) Narrow down on bug #101968, I can execute: |
549 |
+--------------------------------------------- |
550 |
+ |
551 |
+$ bugz get 101968 |
552 |
+ |
553 |
+ * Using http://bugs.gentoo.org/ .. |
554 |
+ * Getting bug 130608 .. |
555 |
+Title : app-dicts/stardict version bump: 2.4.7 |
556 |
+Assignee : liquidx@g.o |
557 |
+Reported : 2006-04-20 07:36 PST |
558 |
+Updated : 2006-05-29 23:18:12 PST |
559 |
+Status : NEW |
560 |
+URL : http://stardict.sf.net |
561 |
+Severity : enhancement |
562 |
+Reporter : dushistov@××××.ru |
563 |
+Priority : P2 |
564 |
+Comments : 3 |
565 |
+Attachments : 1 |
566 |
+ |
567 |
+[ATTACH] [87844] [stardict 2.4.7 ebuild] |
568 |
+ |
569 |
+[Comment #1] dushistov@××××.ru : 2006-04-20 07:36 PST |
570 |
+... |
571 |
+ |
572 |
+3) Now this bug has an attachment submitted by the user, so I can |
573 |
+ easily pull that attachment in: |
574 |
+----------------------------------------------------------------- |
575 |
+ |
576 |
+$ bugz attachment 87844 |
577 |
+ |
578 |
+ * Using http://bugs.gentoo.org/ .. |
579 |
+ * Getting attachment 87844 |
580 |
+ * Saving attachment: "stardict-2.4.7.ebuild" |
581 |
+ |
582 |
+4) If the ebuild is suitable, we can commit it using our normal |
583 |
+ repoman tools, and close the bug. |
584 |
+--------------------------------------------------------------- |
585 |
+ |
586 |
+$ bugz modify 130608 --fixed -c "Thanks for the ebuild. Committed to |
587 |
+ portage" |
588 |
+ |
589 |
+or if we find that the bug is invalid, we can close it by using: |
590 |
+ |
591 |
+$ bugz modify 130608 --invalid -c "Not reproducable" |
592 |
+ |
593 |
+Other options |
594 |
+------------- |
595 |
+ |
596 |
+There is extensive help in `bugz --help` and `bugz <subcommand> |
597 |
+--help` for additional options. |
598 |
+ |
599 |
+bugz.py can be easily adapted for other bugzillas by changing |
600 |
+BugzConfig to match the configuration of your target |
601 |
+bugzilla. However, I haven't spent much time on using it with other |
602 |
+bugzillas out there. If you do have changes that will make it easier, |
603 |
+please let me know. |
604 |
+ |
605 |
|
606 |
diff --git a/third_party/pybugz-0.9.3/bin/bugz b/third_party/pybugz-0.9.3/bin/bugz |
607 |
new file mode 100755 |
608 |
index 0000000..9d29bdd |
609 |
--- /dev/null |
610 |
+++ b/third_party/pybugz-0.9.3/bin/bugz |
611 |
@@ -0,0 +1,393 @@ |
612 |
+#!/usr/bin/python |
613 |
+ |
614 |
+import argparse |
615 |
+import ConfigParser |
616 |
+import locale |
617 |
+import os |
618 |
+import sys |
619 |
+import traceback |
620 |
+ |
621 |
+from bugz import __version__ |
622 |
+from bugz.cli import BugzError, PrettyBugz |
623 |
+from bugz.config import config |
624 |
+ |
625 |
+def make_attach_parser(subparsers): |
626 |
+ attach_parser = subparsers.add_parser('attach', |
627 |
+ help = 'attach file to a bug') |
628 |
+ attach_parser.add_argument('bugid', |
629 |
+ help = 'the ID of the bug where the file should be attached') |
630 |
+ attach_parser.add_argument('filename', |
631 |
+ help = 'the name of the file to attach') |
632 |
+ attach_parser.add_argument('-c', '--content-type', |
633 |
+ default='text/plain', |
634 |
+ help = 'mimetype of the file (default: text/plain)') |
635 |
+ attach_parser.add_argument('-d', '--description', |
636 |
+ help = 'a description of the attachment.') |
637 |
+ attach_parser.add_argument('-p', '--patch', |
638 |
+ action='store_true', |
639 |
+ help = 'attachment is a patch') |
640 |
+ attach_parser.set_defaults(func = PrettyBugz.attach) |
641 |
+ |
642 |
+def make_attachment_parser(subparsers): |
643 |
+ attachment_parser = subparsers.add_parser('attachment', |
644 |
+ help = 'get an attachment from bugzilla') |
645 |
+ attachment_parser.add_argument('attachid', |
646 |
+ help = 'the ID of the attachment') |
647 |
+ attachment_parser.add_argument('-v', '--view', |
648 |
+ action="store_true", |
649 |
+ default = False, |
650 |
+ help = 'print attachment rather than save') |
651 |
+ attachment_parser.set_defaults(func = PrettyBugz.attachment) |
652 |
+ |
653 |
+def make_get_parser(subparsers): |
654 |
+ get_parser = subparsers.add_parser('get', |
655 |
+ help = 'get a bug from bugzilla') |
656 |
+ get_parser.add_argument('bugid', |
657 |
+ help = 'the ID of the bug to retrieve.') |
658 |
+ get_parser.add_argument("-a", "--no-attachments", |
659 |
+ action="store_false", |
660 |
+ default = True, |
661 |
+ help = 'do not show attachments', |
662 |
+ dest = 'attachments') |
663 |
+ get_parser.add_argument("-n", "--no-comments", |
664 |
+ action="store_false", |
665 |
+ default = True, |
666 |
+ help = 'do not show comments', |
667 |
+ dest = 'comments') |
668 |
+ get_parser.set_defaults(func = PrettyBugz.get) |
669 |
+ |
670 |
+def make_modify_parser(subparsers): |
671 |
+ modify_parser = subparsers.add_parser('modify', |
672 |
+ help = 'modify a bug (eg. post a comment)') |
673 |
+ modify_parser.add_argument('bugid', |
674 |
+ help = 'the ID of the bug to modify') |
675 |
+ modify_parser.add_argument('-a', '--assigned-to', |
676 |
+ help = 'change assignee for this bug') |
677 |
+ modify_parser.add_argument('-C', '--comment-editor', |
678 |
+ action='store_true', |
679 |
+ help = 'add comment via default editor') |
680 |
+ modify_parser.add_argument('-F', '--comment-from', |
681 |
+ help = 'add comment from file. If -C is also specified, the editor will be opened with this file as its contents.') |
682 |
+ modify_parser.add_argument('-c', '--comment', |
683 |
+ help = 'add comment from command line') |
684 |
+ modify_parser.add_argument('-d', '--duplicate', |
685 |
+ type = int, |
686 |
+ default = 0, |
687 |
+ help = 'this bug is a duplicate') |
688 |
+ modify_parser.add_argument('-k', '--keywords', |
689 |
+ help = 'set bug keywords'), |
690 |
+ modify_parser.add_argument('--priority', |
691 |
+ choices=config.choices['priority'].values(), |
692 |
+ help = 'change the priority for this bug') |
693 |
+ modify_parser.add_argument('-r', '--resolution', |
694 |
+ choices=config.choices['resolution'].values(), |
695 |
+ help = 'set new resolution (only if status = RESOLVED)') |
696 |
+ modify_parser.add_argument('-s', '--status', |
697 |
+ choices=config.choices['status'].values(), |
698 |
+ help = 'set new status of bug (eg. RESOLVED)') |
699 |
+ modify_parser.add_argument('-S', '--severity', |
700 |
+ choices=config.choices['severity'], |
701 |
+ help = 'set severity for this bug') |
702 |
+ modify_parser.add_argument('-t', '--title', |
703 |
+ help = 'set title of bug') |
704 |
+ modify_parser.add_argument('-U', '--url', |
705 |
+ help = 'set URL field of bug') |
706 |
+ modify_parser.add_argument('-w', '--whiteboard', |
707 |
+ help = 'set Status whiteboard'), |
708 |
+ modify_parser.add_argument('--add-cc', |
709 |
+ action = 'append', |
710 |
+ help = 'add an email to the CC list') |
711 |
+ modify_parser.add_argument('--remove-cc', |
712 |
+ action = 'append', |
713 |
+ help = 'remove an email from the CC list') |
714 |
+ modify_parser.add_argument('--add-dependson', |
715 |
+ action = 'append', |
716 |
+ help = 'add a bug to the depends list') |
717 |
+ modify_parser.add_argument('--remove-dependson', |
718 |
+ action = 'append', |
719 |
+ help = 'remove a bug from the depends list') |
720 |
+ modify_parser.add_argument('--add-blocked', |
721 |
+ action = 'append', |
722 |
+ help = 'add a bug to the blocked list') |
723 |
+ modify_parser.add_argument('--remove-blocked', |
724 |
+ action = 'append', |
725 |
+ help = 'remove a bug from the blocked list') |
726 |
+ modify_parser.add_argument('--component', |
727 |
+ help = 'change the component for this bug') |
728 |
+ modify_parser.add_argument('--fixed', |
729 |
+ action='store_true', |
730 |
+ help = 'mark bug as RESOLVED, FIXED') |
731 |
+ modify_parser.add_argument('--invalid', |
732 |
+ action='store_true', |
733 |
+ help = 'mark bug as RESOLVED, INVALID') |
734 |
+ modify_parser.set_defaults(func = PrettyBugz.modify) |
735 |
+ |
736 |
+def make_namedcmd_parser(subparsers): |
737 |
+ namedcmd_parser = subparsers.add_parser('namedcmd', |
738 |
+ help = 'run a stored search') |
739 |
+ namedcmd_parser.add_argument('command', |
740 |
+ help = 'the name of the stored search') |
741 |
+ namedcmd_parser.add_argument('--show-status', |
742 |
+ action = 'store_true', |
743 |
+ help = 'show status of bugs') |
744 |
+ namedcmd_parser.add_argument('--show-url', |
745 |
+ action = 'store_true', |
746 |
+ help = 'show bug id as a url') |
747 |
+ namedcmd_parser.set_defaults(func = PrettyBugz.namedcmd) |
748 |
+ |
749 |
+def make_post_parser(subparsers): |
750 |
+ post_parser = subparsers.add_parser('post', |
751 |
+ help = 'post a new bug into bugzilla') |
752 |
+ post_parser.add_argument('--product', |
753 |
+ help = 'product') |
754 |
+ post_parser.add_argument('--component', |
755 |
+ help = 'component') |
756 |
+ post_parser.add_argument('--prodversion', |
757 |
+ help = 'version of the product') |
758 |
+ post_parser.add_argument('-t', '--title', |
759 |
+ help = 'title of bug') |
760 |
+ post_parser.add_argument('-d', '--description', |
761 |
+ help = 'description of the bug') |
762 |
+ post_parser.add_argument('-F' , '--description-from', |
763 |
+ help = 'description from contents of file') |
764 |
+ post_parser.add_argument('--append-command', |
765 |
+ help = 'append the output of a command to the description') |
766 |
+ post_parser.add_argument('-a', '--assigned-to', |
767 |
+ help = 'assign bug to someone other than the default assignee') |
768 |
+ post_parser.add_argument('--cc', |
769 |
+ help = 'add a list of emails to CC list') |
770 |
+ post_parser.add_argument('-U', '--url', |
771 |
+ help = 'URL associated with the bug') |
772 |
+ post_parser.add_argument('--depends-on', |
773 |
+ help = 'add a list of bug dependencies', |
774 |
+ dest='dependson') |
775 |
+ post_parser.add_argument('--blocked', |
776 |
+ help = 'add a list of blocker bugs') |
777 |
+ post_parser.add_argument('-k', '--keywords', |
778 |
+ help = 'list of bugzilla keywords') |
779 |
+ post_parser.add_argument('--batch', |
780 |
+ action="store_true", |
781 |
+ help = 'do not prompt for any values') |
782 |
+ post_parser.add_argument('--default-confirm', |
783 |
+ choices = ['y','Y','n','N'], |
784 |
+ default = 'y', |
785 |
+ help = 'default answer to confirmation question') |
786 |
+ post_parser.add_argument('--priority', |
787 |
+ choices=config.choices['priority'].values(), |
788 |
+ help = 'set priority for the new bug') |
789 |
+ post_parser.add_argument('-S', '--severity', |
790 |
+ choices=config.choices['severity'], |
791 |
+ help = 'set the severity for the new bug') |
792 |
+ post_parser.set_defaults(func = PrettyBugz.post) |
793 |
+ |
794 |
+def make_search_parser(subparsers): |
795 |
+ search_parser = subparsers.add_parser('search', |
796 |
+ help = 'search for bugs in bugzilla') |
797 |
+ search_parser.add_argument('terms', |
798 |
+ nargs='*', |
799 |
+ help = 'strings to search for in title or body') |
800 |
+ search_parser.add_argument('-o', '--order', |
801 |
+ choices = config.choices['order'].keys(), |
802 |
+ default = 'number', |
803 |
+ help = 'display bugs in this order') |
804 |
+ search_parser.add_argument('-a', '--assigned-to', |
805 |
+ help = 'email the bug is assigned to') |
806 |
+ search_parser.add_argument('-r', '--reporter', |
807 |
+ help = 'email the bug was reported by') |
808 |
+ search_parser.add_argument('--cc', |
809 |
+ help = 'restrict by CC email address') |
810 |
+ search_parser.add_argument('--commenter', |
811 |
+ help = 'email that commented the bug') |
812 |
+ search_parser.add_argument('-s', '--status', |
813 |
+ action='append', |
814 |
+ help = 'restrict by status (one or more, use all for all statuses)') |
815 |
+ search_parser.add_argument('--severity', |
816 |
+ action='append', |
817 |
+ choices = config.choices['severity'], |
818 |
+ help = 'restrict by severity (one or more)') |
819 |
+ search_parser.add_argument('--priority', |
820 |
+ action='append', |
821 |
+ choices = config.choices['priority'].values(), |
822 |
+ help = 'restrict by priority (one or more)') |
823 |
+ search_parser.add_argument('-c', '--comments', |
824 |
+ action='store_true', |
825 |
+ default=None, |
826 |
+ help = 'search comments instead of title') |
827 |
+ search_parser.add_argument('--product', |
828 |
+ action='append', |
829 |
+ help = 'restrict by product (one or more)') |
830 |
+ search_parser.add_argument('-C', '--component', |
831 |
+ action='append', |
832 |
+ help = 'restrict by component (1 or more)') |
833 |
+ search_parser.add_argument('-k', '--keywords', |
834 |
+ help = 'restrict by keywords') |
835 |
+ search_parser.add_argument('-w', '--whiteboard', |
836 |
+ help = 'status whiteboard') |
837 |
+ search_parser.add_argument('--show-status', |
838 |
+ action = 'store_true', |
839 |
+ help='show status of bugs') |
840 |
+ search_parser.add_argument('--show-url', |
841 |
+ action = 'store_true', |
842 |
+ help='show bug id as a url.') |
843 |
+ search_parser.set_defaults(func = PrettyBugz.search) |
844 |
+ |
845 |
+def make_parser(): |
846 |
+ parser = argparse.ArgumentParser( |
847 |
+ epilog = 'use -h after a sub-command for sub-command specific help') |
848 |
+ parser.add_argument('--config-file', |
849 |
+ help = 'read an alternate configuration file') |
850 |
+ parser.add_argument('--connection', |
851 |
+ help = 'use [connection] section of your configuration file') |
852 |
+ parser.add_argument('-b', '--base', |
853 |
+ help = 'base URL of Bugzilla') |
854 |
+ parser.add_argument('-u', '--user', |
855 |
+ help = 'username for commands requiring authentication') |
856 |
+ parser.add_argument('-p', '--password', |
857 |
+ help = 'password for commands requiring authentication') |
858 |
+ parser.add_argument('-H', '--httpuser', |
859 |
+ help = 'username for basic http auth') |
860 |
+ parser.add_argument('-P', '--httppassword', |
861 |
+ help = 'password for basic http auth') |
862 |
+ parser.add_argument('-f', '--forget', |
863 |
+ action='store_true', |
864 |
+ help = 'forget login after execution') |
865 |
+ parser.add_argument('-q', '--quiet', |
866 |
+ action='store_true', |
867 |
+ help = 'quiet mode') |
868 |
+ parser.add_argument('--columns', |
869 |
+ type = int, |
870 |
+ help = 'maximum number of columns output should use') |
871 |
+ parser.add_argument('--encoding', |
872 |
+ help = 'output encoding (default: utf-8).') |
873 |
+ parser.add_argument('--skip-auth', |
874 |
+ action='store_true', |
875 |
+ help = 'skip Authentication.') |
876 |
+ parser.add_argument('--version', |
877 |
+ action='version', |
878 |
+ help='show program version and exit', |
879 |
+ version='%(prog)s ' + __version__) |
880 |
+ subparsers = parser.add_subparsers(help = 'help for sub-commands') |
881 |
+ make_attach_parser(subparsers) |
882 |
+ make_attachment_parser(subparsers) |
883 |
+ make_get_parser(subparsers) |
884 |
+ make_modify_parser(subparsers) |
885 |
+ make_namedcmd_parser(subparsers) |
886 |
+ make_post_parser(subparsers) |
887 |
+ make_search_parser(subparsers) |
888 |
+ return parser |
889 |
+ |
890 |
+def config_option(parser, get, section, option): |
891 |
+ if parser.has_option(section, option): |
892 |
+ try: |
893 |
+ if get(section, option) != '': |
894 |
+ return get(section, option) |
895 |
+ else: |
896 |
+ print " ! Error: "+option+" is not set" |
897 |
+ sys.exit(1) |
898 |
+ except ValueError as e: |
899 |
+ print " ! Error: option "+option+" is not in the right format: "+str(e) |
900 |
+ sys.exit(1) |
901 |
+ |
902 |
+def get_config(args, bugz): |
903 |
+ config_file = getattr(args, 'config_file') |
904 |
+ if config_file is None: |
905 |
+ config_file = '~/.bugzrc' |
906 |
+ section = getattr(args, 'connection') |
907 |
+ parser = ConfigParser.ConfigParser() |
908 |
+ config_file_name = os.path.expanduser(config_file) |
909 |
+ |
910 |
+ # try to open config file |
911 |
+ try: |
912 |
+ file = open(config_file_name) |
913 |
+ except IOError: |
914 |
+ if getattr(args, 'config_file') is not None: |
915 |
+ print " ! Error: Can't find user configuration file: "+config_file_name |
916 |
+ sys.exit(1) |
917 |
+ else: |
918 |
+ return bugz |
919 |
+ |
920 |
+ # try to parse config file |
921 |
+ try: |
922 |
+ parser.readfp(file) |
923 |
+ sections = parser.sections() |
924 |
+ except ConfigParser.ParsingError as e: |
925 |
+ print " ! Error: Can't parse user configuration file: "+str(e) |
926 |
+ sys.exit(1) |
927 |
+ |
928 |
+ # parse a specific section |
929 |
+ if section in sections: |
930 |
+ bugz['base'] = config_option(parser, parser.get, section, "base") |
931 |
+ bugz['user'] = config_option(parser, parser.get, section, "user") |
932 |
+ bugz['password'] = config_option(parser, parser.get, section, "password") |
933 |
+ bugz['httpuser'] = config_option(parser, parser.get, section, "httpuser") |
934 |
+ bugz['httppassword'] = config_option(parser, parser.get, section, |
935 |
+ "httppassword") |
936 |
+ bugz['forget'] = config_option(parser, parser.getboolean, section, |
937 |
+ "forget") |
938 |
+ bugz['columns'] = config_option(parser, parser.getint, section, |
939 |
+ "columns") |
940 |
+ bugz['encoding'] = config_option(parser, parser.get, section, |
941 |
+ "encoding") |
942 |
+ bugz['quiet'] = config_option(parser, parser.getboolean, section, |
943 |
+ "quiet") |
944 |
+ elif section is not None: |
945 |
+ print " ! Error: Can't find section ["+section+"] in configuration file" |
946 |
+ sys.exit(1) |
947 |
+ |
948 |
+ return bugz |
949 |
+ |
950 |
+def get_kwds(args, bugz, cmd): |
951 |
+ global_attrs = ['user', 'password', 'httpuser', 'httppassword', 'forget', |
952 |
+ 'base', 'columns', 'encoding', 'quiet', 'skip_auth'] |
953 |
+ skip_attrs = ['config_file', 'connection', 'func'] |
954 |
+ for attr in dir(args): |
955 |
+ if attr[0] == '_' or attr in skip_attrs: |
956 |
+ continue |
957 |
+ elif attr in global_attrs: |
958 |
+ if attr not in bugz or getattr(args,attr): |
959 |
+ bugz[attr] = getattr(args,attr) |
960 |
+ else: |
961 |
+ cmd[attr] = getattr(args,attr) |
962 |
+ |
963 |
+def main(): |
964 |
+ parser = make_parser() |
965 |
+ |
966 |
+ # parse options |
967 |
+ args = parser.parse_args() |
968 |
+ bugz_kwds = {} |
969 |
+ get_config(args, bugz_kwds) |
970 |
+ cmd_kwds = {} |
971 |
+ get_kwds(args, bugz_kwds, cmd_kwds) |
972 |
+ if bugz_kwds['base'] is None: |
973 |
+ bugz_kwds['base'] = 'https://bugs.gentoo.org' |
974 |
+ if bugz_kwds['columns'] is None: |
975 |
+ bugz_kwds['columns'] = 0 |
976 |
+ |
977 |
+ try: |
978 |
+ bugz = PrettyBugz(**bugz_kwds) |
979 |
+ args.func(bugz, **cmd_kwds) |
980 |
+ |
981 |
+ except BugzError, e: |
982 |
+ print ' ! Error: %s' % e |
983 |
+ sys.exit(-1) |
984 |
+ |
985 |
+ except TypeError, e: |
986 |
+ print ' ! Error: Incorrect number of arguments supplied' |
987 |
+ print |
988 |
+ traceback.print_exc() |
989 |
+ sys.exit(-1) |
990 |
+ |
991 |
+ except RuntimeError, e: |
992 |
+ print ' ! Error: %s' % e |
993 |
+ sys.exit(-1) |
994 |
+ |
995 |
+ except KeyboardInterrupt: |
996 |
+ print |
997 |
+ print 'Stopped.' |
998 |
+ sys.exit(-1) |
999 |
+ |
1000 |
+ except: |
1001 |
+ raise |
1002 |
+ |
1003 |
+if __name__ == "__main__": |
1004 |
+ main() |
1005 |
|
1006 |
diff --git a/third_party/pybugz-0.9.3/bugz/__init__.py b/third_party/pybugz-0.9.3/bugz/__init__.py |
1007 |
new file mode 100644 |
1008 |
index 0000000..f5a11a4 |
1009 |
--- /dev/null |
1010 |
+++ b/third_party/pybugz-0.9.3/bugz/__init__.py |
1011 |
@@ -0,0 +1,31 @@ |
1012 |
+#!/usr/bin/env python |
1013 |
+ |
1014 |
+""" |
1015 |
+Python Bugzilla Interface |
1016 |
+ |
1017 |
+Simple command-line interface to bugzilla to allow: |
1018 |
+ - searching |
1019 |
+ - getting bug info |
1020 |
+ - saving attachments |
1021 |
+ |
1022 |
+Requirements |
1023 |
+------------ |
1024 |
+ - Python 2.5 or later |
1025 |
+ |
1026 |
+Classes |
1027 |
+------- |
1028 |
+ - Bugz - Pythonic interface to Bugzilla |
1029 |
+ - PrettyBugz - Command line interface to Bugzilla |
1030 |
+ |
1031 |
+""" |
1032 |
+ |
1033 |
+__version__ = '0.9.3' |
1034 |
+__author__ = 'Alastair Tse <http://www.liquidx.net/>' |
1035 |
+__contributors__ = ['Santiago M. Mola <cooldwind@×××××.com', |
1036 |
+ 'William Hubbs <w.d.hubbs@×××××.com'] |
1037 |
+__revision__ = '$Id: $' |
1038 |
+__license__ = """Copyright (c) 2006, Alastair Tse, All rights reserved. |
1039 |
+This following source code is licensed under the GPL v2 License.""" |
1040 |
+ |
1041 |
+CONFIG_FILE = '.bugz' |
1042 |
+ |
1043 |
|
1044 |
diff --git a/third_party/pybugz-0.9.3/bugz/bugzilla.py b/third_party/pybugz-0.9.3/bugz/bugzilla.py |
1045 |
new file mode 100644 |
1046 |
index 0000000..957598e |
1047 |
--- /dev/null |
1048 |
+++ b/third_party/pybugz-0.9.3/bugz/bugzilla.py |
1049 |
@@ -0,0 +1,862 @@ |
1050 |
+#!/usr/bin/env python |
1051 |
+ |
1052 |
+import base64 |
1053 |
+import csv |
1054 |
+import getpass |
1055 |
+import locale |
1056 |
+import mimetypes |
1057 |
+import os |
1058 |
+import re |
1059 |
+import sys |
1060 |
+ |
1061 |
+from cookielib import LWPCookieJar, CookieJar |
1062 |
+from cStringIO import StringIO |
1063 |
+from urlparse import urlsplit, urljoin |
1064 |
+from urllib import urlencode, quote |
1065 |
+from urllib2 import build_opener, HTTPCookieProcessor, Request |
1066 |
+ |
1067 |
+from config import config |
1068 |
+ |
1069 |
+from xml.etree import ElementTree |
1070 |
+ |
1071 |
+COOKIE_FILE = '.bugz_cookie' |
1072 |
+ |
1073 |
+# |
1074 |
+# Return a string truncated to the given length if it is longer. |
1075 |
+# |
1076 |
+ |
1077 |
+def ellipsis(text, length): |
1078 |
+ if len(text) > length: |
1079 |
+ return text[:length-4] + "..." |
1080 |
+ else: |
1081 |
+ return text |
1082 |
+ |
1083 |
+# |
1084 |
+# HTTP file uploads in Python |
1085 |
+# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306 |
1086 |
+# |
1087 |
+ |
1088 |
+def post_multipart(host, selector, fields, files): |
1089 |
+ """ |
1090 |
+ Post fields and files to an http host as multipart/form-data. |
1091 |
+ fields is a sequence of (name, value) elements for regular form fields. |
1092 |
+ files is a sequence of (name, filename, value) elements for data to be uploaded as files |
1093 |
+ Return the server's response page. |
1094 |
+ """ |
1095 |
+ content_type, body = encode_multipart_formdata(fields, files) |
1096 |
+ h = httplib.HTTP(host) |
1097 |
+ h.putrequest('POST', selector) |
1098 |
+ h.putheader('content-type', content_type) |
1099 |
+ h.putheader('content-length', str(len(body))) |
1100 |
+ h.endheaders() |
1101 |
+ h.send(body) |
1102 |
+ errcode, errmsg, headers = h.getreply() |
1103 |
+ return h.file.read() |
1104 |
+ |
1105 |
+def encode_multipart_formdata(fields, files): |
1106 |
+ """ |
1107 |
+ fields is a sequence of (name, value) elements for regular form fields. |
1108 |
+ files is a sequence of (name, filename, value) elements for data to be uploaded as files |
1109 |
+ Return (content_type, body) ready for httplib.HTTP instance |
1110 |
+ """ |
1111 |
+ BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$' |
1112 |
+ CRLF = '\r\n' |
1113 |
+ L = [] |
1114 |
+ for (key, value) in fields: |
1115 |
+ L.append('--' + BOUNDARY) |
1116 |
+ L.append('Content-Disposition: form-data; name="%s"' % key) |
1117 |
+ L.append('') |
1118 |
+ L.append(value) |
1119 |
+ for (key, filename, value) in files: |
1120 |
+ L.append('--' + BOUNDARY) |
1121 |
+ L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) |
1122 |
+ L.append('Content-Type: %s' % get_content_type(filename)) |
1123 |
+ L.append('') |
1124 |
+ L.append(value) |
1125 |
+ L.append('--' + BOUNDARY + '--') |
1126 |
+ L.append('') |
1127 |
+ body = CRLF.join(L) |
1128 |
+ content_type = 'multipart/form-data; boundary=%s' % BOUNDARY |
1129 |
+ return content_type, body |
1130 |
+ |
1131 |
+def get_content_type(filename): |
1132 |
+ return mimetypes.guess_type(filename)[0] or 'application/octet-stream' |
1133 |
+ |
1134 |
+# |
1135 |
+# Override the behaviour of elementtree and allow us to |
1136 |
+# force the encoding to utf-8 |
1137 |
+# Not needed in Python 2.7, since ElementTree.XMLTreeBuilder uses the forced |
1138 |
+# encoding. |
1139 |
+# |
1140 |
+ |
1141 |
+class ForcedEncodingXMLTreeBuilder(ElementTree.XMLTreeBuilder): |
1142 |
+ def __init__(self, html = 0, target = None, encoding = None): |
1143 |
+ try: |
1144 |
+ from xml.parsers import expat |
1145 |
+ except ImportError: |
1146 |
+ raise ImportError( |
1147 |
+ "No module named expat; use SimpleXMLTreeBuilder instead" |
1148 |
+ ) |
1149 |
+ self._parser = parser = expat.ParserCreate(encoding, "}") |
1150 |
+ if target is None: |
1151 |
+ target = ElementTree.TreeBuilder() |
1152 |
+ self._target = target |
1153 |
+ self._names = {} # name memo cache |
1154 |
+ # callbacks |
1155 |
+ parser.DefaultHandlerExpand = self._default |
1156 |
+ parser.StartElementHandler = self._start |
1157 |
+ parser.EndElementHandler = self._end |
1158 |
+ parser.CharacterDataHandler = self._data |
1159 |
+ # let expat do the buffering, if supported |
1160 |
+ try: |
1161 |
+ self._parser.buffer_text = 1 |
1162 |
+ except AttributeError: |
1163 |
+ pass |
1164 |
+ # use new-style attribute handling, if supported |
1165 |
+ try: |
1166 |
+ self._parser.ordered_attributes = 1 |
1167 |
+ self._parser.specified_attributes = 1 |
1168 |
+ parser.StartElementHandler = self._start_list |
1169 |
+ except AttributeError: |
1170 |
+ pass |
1171 |
+ encoding = None |
1172 |
+ if not parser.returns_unicode: |
1173 |
+ encoding = "utf-8" |
1174 |
+ # target.xml(encoding, None) |
1175 |
+ self._doctype = None |
1176 |
+ self.entity = {} |
1177 |
+ |
1178 |
+# |
1179 |
+# Real bugzilla interface |
1180 |
+# |
1181 |
+ |
1182 |
+class Bugz: |
1183 |
+ """ Converts sane method calls to Bugzilla HTTP requests. |
1184 |
+ |
1185 |
+ @ivar base: base url of bugzilla. |
1186 |
+ @ivar user: username for authenticated operations. |
1187 |
+ @ivar password: password for authenticated operations |
1188 |
+ @ivar cookiejar: for authenticated sessions so we only auth once. |
1189 |
+ @ivar forget: forget user/password after session. |
1190 |
+ @ivar authenticated: is this session authenticated already |
1191 |
+ """ |
1192 |
+ |
1193 |
+ def __init__(self, base, user = None, password = None, forget = False, |
1194 |
+ skip_auth = False, httpuser = None, httppassword = None ): |
1195 |
+ """ |
1196 |
+ {user} and {password} will be prompted if an action needs them |
1197 |
+ and they are not supplied. |
1198 |
+ |
1199 |
+ if {forget} is set, the login cookie will be destroyed on quit. |
1200 |
+ |
1201 |
+ @param base: base url of the bugzilla |
1202 |
+ @type base: string |
1203 |
+ @keyword user: username for authenticated actions. |
1204 |
+ @type user: string |
1205 |
+ @keyword password: password for authenticated actions. |
1206 |
+ @type password: string |
1207 |
+ @keyword forget: forget login session after termination. |
1208 |
+ @type forget: bool |
1209 |
+ @keyword skip_auth: do not authenticate |
1210 |
+ @type skip_auth: bool |
1211 |
+ """ |
1212 |
+ self.base = base |
1213 |
+ scheme, self.host, self.path, query, frag = urlsplit(self.base) |
1214 |
+ self.authenticated = False |
1215 |
+ self.forget = forget |
1216 |
+ |
1217 |
+ if not self.forget: |
1218 |
+ try: |
1219 |
+ cookie_file = os.path.join(os.environ['HOME'], COOKIE_FILE) |
1220 |
+ self.cookiejar = LWPCookieJar(cookie_file) |
1221 |
+ if forget: |
1222 |
+ try: |
1223 |
+ self.cookiejar.load() |
1224 |
+ self.cookiejar.clear() |
1225 |
+ self.cookiejar.save() |
1226 |
+ os.chmod(self.cookiejar.filename, 0600) |
1227 |
+ except IOError: |
1228 |
+ pass |
1229 |
+ except KeyError: |
1230 |
+ self.warn('Unable to save session cookies in %s' % cookie_file) |
1231 |
+ self.cookiejar = CookieJar(cookie_file) |
1232 |
+ else: |
1233 |
+ self.cookiejar = CookieJar() |
1234 |
+ |
1235 |
+ self.opener = build_opener(HTTPCookieProcessor(self.cookiejar)) |
1236 |
+ self.user = user |
1237 |
+ self.password = password |
1238 |
+ self.httpuser = httpuser |
1239 |
+ self.httppassword = httppassword |
1240 |
+ self.skip_auth = skip_auth |
1241 |
+ |
1242 |
+ def log(self, status_msg): |
1243 |
+ """Default logging handler. Expected to be overridden by |
1244 |
+ the UI implementing subclass. |
1245 |
+ |
1246 |
+ @param status_msg: status message to print |
1247 |
+ @type status_msg: string |
1248 |
+ """ |
1249 |
+ return |
1250 |
+ |
1251 |
+ def warn(self, warn_msg): |
1252 |
+ """Default logging handler. Expected to be overridden by |
1253 |
+ the UI implementing subclass. |
1254 |
+ |
1255 |
+ @param status_msg: status message to print |
1256 |
+ @type status_msg: string |
1257 |
+ """ |
1258 |
+ return |
1259 |
+ |
1260 |
+ def get_input(self, prompt): |
1261 |
+ """Default input handler. Expected to be override by the |
1262 |
+ UI implementing subclass. |
1263 |
+ |
1264 |
+ @param prompt: Prompt message |
1265 |
+ @type prompt: string |
1266 |
+ """ |
1267 |
+ return '' |
1268 |
+ |
1269 |
+ def auth(self): |
1270 |
+ """Authenticate a session. |
1271 |
+ """ |
1272 |
+ # check if we need to authenticate |
1273 |
+ if self.authenticated: |
1274 |
+ return |
1275 |
+ |
1276 |
+ # try seeing if we really need to request login |
1277 |
+ if not self.forget: |
1278 |
+ try: |
1279 |
+ self.cookiejar.load() |
1280 |
+ except IOError: |
1281 |
+ pass |
1282 |
+ |
1283 |
+ req_url = urljoin(self.base, config.urls['auth']) |
1284 |
+ req_url += '?GoAheadAndLogIn=1' |
1285 |
+ req = Request(req_url, None, config.headers) |
1286 |
+ if self.httpuser and self.httppassword: |
1287 |
+ base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1] |
1288 |
+ req.add_header("Authorization", "Basic %s" % base64string) |
1289 |
+ resp = self.opener.open(req) |
1290 |
+ re_request_login = re.compile(r'<title>.*Log in to .*</title>') |
1291 |
+ if not re_request_login.search(resp.read()): |
1292 |
+ self.log('Already logged in.') |
1293 |
+ self.authenticated = True |
1294 |
+ return |
1295 |
+ |
1296 |
+ # prompt for username if we were not supplied with it |
1297 |
+ if not self.user: |
1298 |
+ self.log('No username given.') |
1299 |
+ self.user = self.get_input('Username: ') |
1300 |
+ |
1301 |
+ # prompt for password if we were not supplied with it |
1302 |
+ if not self.password: |
1303 |
+ self.log('No password given.') |
1304 |
+ self.password = getpass.getpass() |
1305 |
+ |
1306 |
+ # perform login |
1307 |
+ qparams = config.params['auth'].copy() |
1308 |
+ qparams['Bugzilla_login'] = self.user |
1309 |
+ qparams['Bugzilla_password'] = self.password |
1310 |
+ if not self.forget: |
1311 |
+ qparams['Bugzilla_remember'] = 'on' |
1312 |
+ |
1313 |
+ req_url = urljoin(self.base, config.urls['auth']) |
1314 |
+ req = Request(req_url, urlencode(qparams), config.headers) |
1315 |
+ if self.httpuser and self.httppassword: |
1316 |
+ base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1] |
1317 |
+ req.add_header("Authorization", "Basic %s" % base64string) |
1318 |
+ resp = self.opener.open(req) |
1319 |
+ if resp.info().has_key('Set-Cookie'): |
1320 |
+ self.authenticated = True |
1321 |
+ if not self.forget: |
1322 |
+ self.cookiejar.save() |
1323 |
+ os.chmod(self.cookiejar.filename, 0600) |
1324 |
+ return True |
1325 |
+ else: |
1326 |
+ raise RuntimeError("Failed to login") |
1327 |
+ |
1328 |
+ def extractResults(self, resp): |
1329 |
+ # parse the results into dicts. |
1330 |
+ results = [] |
1331 |
+ columns = [] |
1332 |
+ rows = [] |
1333 |
+ |
1334 |
+ for r in csv.reader(resp): rows.append(r) |
1335 |
+ for field in rows[0]: |
1336 |
+ if config.choices['column_alias'].has_key(field): |
1337 |
+ columns.append(config.choices['column_alias'][field]) |
1338 |
+ else: |
1339 |
+ self.log('Unknown field: ' + field) |
1340 |
+ columns.append(field) |
1341 |
+ for row in rows[1:]: |
1342 |
+ if "Missing Search" in row[0]: |
1343 |
+ self.log('Bugzilla error (Missing search found)') |
1344 |
+ return None |
1345 |
+ fields = {} |
1346 |
+ for i in range(min(len(row), len(columns))): |
1347 |
+ fields[columns[i]] = row[i] |
1348 |
+ results.append(fields) |
1349 |
+ return results |
1350 |
+ |
1351 |
+ def search(self, query, comments = False, order = 'number', |
1352 |
+ assigned_to = None, reporter = None, cc = None, |
1353 |
+ commenter = None, whiteboard = None, keywords = None, |
1354 |
+ status = [], severity = [], priority = [], product = [], |
1355 |
+ component = []): |
1356 |
+ """Search bugzilla for a bug. |
1357 |
+ |
1358 |
+ @param query: query string to search in title or {comments}. |
1359 |
+ @type query: string |
1360 |
+ @param order: what order to returns bugs in. |
1361 |
+ @type order: string |
1362 |
+ |
1363 |
+ @keyword assigned_to: email address which the bug is assigned to. |
1364 |
+ @type assigned_to: string |
1365 |
+ @keyword reporter: email address matching the bug reporter. |
1366 |
+ @type reporter: string |
1367 |
+ @keyword cc: email that is contained in the CC list |
1368 |
+ @type cc: string |
1369 |
+ @keyword commenter: email of a commenter. |
1370 |
+ @type commenter: string |
1371 |
+ |
1372 |
+ @keyword whiteboard: string to search in status whiteboard (gentoo?) |
1373 |
+ @type whiteboard: string |
1374 |
+ @keyword keywords: keyword to search for |
1375 |
+ @type keywords: string |
1376 |
+ |
1377 |
+ @keyword status: bug status to match. default is ['NEW', 'ASSIGNED', |
1378 |
+ 'REOPENED']. |
1379 |
+ @type status: list |
1380 |
+ @keyword severity: severity to match, empty means all. |
1381 |
+ @type severity: list |
1382 |
+ @keyword priority: priority levels to patch, empty means all. |
1383 |
+ @type priority: list |
1384 |
+ @keyword comments: search comments instead of just bug title. |
1385 |
+ @type comments: bool |
1386 |
+ @keyword product: search within products. empty means all. |
1387 |
+ @type product: list |
1388 |
+ @keyword component: search within components. empty means all. |
1389 |
+ @type component: list |
1390 |
+ |
1391 |
+ @return: list of bugs, each bug represented as a dict |
1392 |
+ @rtype: list of dicts |
1393 |
+ """ |
1394 |
+ |
1395 |
+ if not self.authenticated and not self.skip_auth: |
1396 |
+ self.auth() |
1397 |
+ |
1398 |
+ qparams = config.params['list'].copy() |
1399 |
+ if comments: |
1400 |
+ qparams['long_desc'] = query |
1401 |
+ else: |
1402 |
+ qparams['short_desc'] = query |
1403 |
+ |
1404 |
+ qparams['order'] = config.choices['order'].get(order, 'Bug Number') |
1405 |
+ qparams['bug_severity'] = severity or [] |
1406 |
+ qparams['priority'] = priority or [] |
1407 |
+ if status is None: |
1408 |
+ # NEW, ASSIGNED and REOPENED is obsolete as of bugzilla 3.x and has |
1409 |
+ # been removed from bugs.gentoo.org on 2011/05/01 |
1410 |
+ qparams['bug_status'] = ['NEW', 'ASSIGNED', 'REOPENED', 'UNCONFIRMED', 'CONFIRMED', 'IN_PROGRESS'] |
1411 |
+ elif [s.upper() for s in status] == ['ALL']: |
1412 |
+ qparams['bug_status'] = config.choices['status'] |
1413 |
+ else: |
1414 |
+ qparams['bug_status'] = [s.upper() for s in status] |
1415 |
+ qparams['product'] = product or '' |
1416 |
+ qparams['component'] = component or '' |
1417 |
+ qparams['status_whiteboard'] = whiteboard or '' |
1418 |
+ qparams['keywords'] = keywords or '' |
1419 |
+ |
1420 |
+ # hoops to jump through for emails, since there are |
1421 |
+ # only two fields, we have to figure out what combinations |
1422 |
+ # to use if all three are set. |
1423 |
+ unique = list(set([assigned_to, cc, reporter, commenter])) |
1424 |
+ unique = [u for u in unique if u] |
1425 |
+ if len(unique) < 3: |
1426 |
+ for i in range(len(unique)): |
1427 |
+ e = unique[i] |
1428 |
+ n = i + 1 |
1429 |
+ qparams['email%d' % n] = e |
1430 |
+ qparams['emailassigned_to%d' % n] = int(e == assigned_to) |
1431 |
+ qparams['emailreporter%d' % n] = int(e == reporter) |
1432 |
+ qparams['emailcc%d' % n] = int(e == cc) |
1433 |
+ qparams['emaillongdesc%d' % n] = int(e == commenter) |
1434 |
+ else: |
1435 |
+ raise AssertionError('Cannot set assigned_to, cc, and ' |
1436 |
+ 'reporter in the same query') |
1437 |
+ |
1438 |
+ req_params = urlencode(qparams, True) |
1439 |
+ req_url = urljoin(self.base, config.urls['list']) |
1440 |
+ req_url += '?' + req_params |
1441 |
+ req = Request(req_url, None, config.headers) |
1442 |
+ if self.httpuser and self.httppassword: |
1443 |
+ base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1] |
1444 |
+ req.add_header("Authorization", "Basic %s" % base64string) |
1445 |
+ resp = self.opener.open(req) |
1446 |
+ return self.extractResults(resp) |
1447 |
+ |
1448 |
+ def namedcmd(self, cmd): |
1449 |
+ """Run command stored in Bugzilla by name. |
1450 |
+ |
1451 |
+ @return: Result from the stored command. |
1452 |
+ @rtype: list of dicts |
1453 |
+ """ |
1454 |
+ |
1455 |
+ if not self.authenticated and not self.skip_auth: |
1456 |
+ self.auth() |
1457 |
+ |
1458 |
+ qparams = config.params['namedcmd'].copy() |
1459 |
+ # Is there a better way of getting a command with a space in its name |
1460 |
+ # to be encoded as foo%20bar instead of foo+bar or foo%2520bar? |
1461 |
+ qparams['namedcmd'] = quote(cmd) |
1462 |
+ req_params = urlencode(qparams, True) |
1463 |
+ req_params = req_params.replace('%25','%') |
1464 |
+ |
1465 |
+ req_url = urljoin(self.base, config.urls['list']) |
1466 |
+ req_url += '?' + req_params |
1467 |
+ req = Request(req_url, None, config.headers) |
1468 |
+ if self.user and self.password: |
1469 |
+ base64string = base64.encodestring('%s:%s' % (self.user, self.password))[:-1] |
1470 |
+ req.add_header("Authorization", "Basic %s" % base64string) |
1471 |
+ resp = self.opener.open(req) |
1472 |
+ |
1473 |
+ return self.extractResults(resp) |
1474 |
+ |
1475 |
+ def get(self, bugid): |
1476 |
+ """Get an ElementTree representation of a bug. |
1477 |
+ |
1478 |
+ @param bugid: bug id |
1479 |
+ @type bugid: int |
1480 |
+ |
1481 |
+ @rtype: ElementTree |
1482 |
+ """ |
1483 |
+ if not self.authenticated and not self.skip_auth: |
1484 |
+ self.auth() |
1485 |
+ |
1486 |
+ qparams = config.params['show'].copy() |
1487 |
+ qparams['id'] = bugid |
1488 |
+ |
1489 |
+ req_params = urlencode(qparams, True) |
1490 |
+ req_url = urljoin(self.base, config.urls['show']) |
1491 |
+ req_url += '?' + req_params |
1492 |
+ req = Request(req_url, None, config.headers) |
1493 |
+ if self.httpuser and self.httppassword: |
1494 |
+ base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1] |
1495 |
+ req.add_header("Authorization", "Basic %s" % base64string) |
1496 |
+ resp = self.opener.open(req) |
1497 |
+ |
1498 |
+ data = resp.read() |
1499 |
+ # Get rid of control characters. |
1500 |
+ data = re.sub('[\x00-\x08\x0e-\x1f\x0b\x0c]', '', data) |
1501 |
+ fd = StringIO(data) |
1502 |
+ |
1503 |
+ # workaround for ill-defined XML templates in bugzilla 2.20.2 |
1504 |
+ (major_version, minor_version) = \ |
1505 |
+ (sys.version_info[0], sys.version_info[1]) |
1506 |
+ if major_version > 2 or \ |
1507 |
+ (major_version == 2 and minor_version >= 7): |
1508 |
+ # If this is 2.7 or greater, then XMLTreeBuilder |
1509 |
+ # does what we want. |
1510 |
+ parser = ElementTree.XMLParser() |
1511 |
+ else: |
1512 |
+ # Running under Python 2.6, so we need to use our |
1513 |
+ # subclass of XMLTreeBuilder instead. |
1514 |
+ parser = ForcedEncodingXMLTreeBuilder(encoding = 'utf-8') |
1515 |
+ |
1516 |
+ etree = ElementTree.parse(fd, parser) |
1517 |
+ bug = etree.find('.//bug') |
1518 |
+ if bug is not None and bug.attrib.has_key('error'): |
1519 |
+ return None |
1520 |
+ else: |
1521 |
+ return etree |
1522 |
+ |
1523 |
+ def modify(self, bugid, title = None, comment = None, url = None, |
1524 |
+ status = None, resolution = None, |
1525 |
+ assigned_to = None, duplicate = 0, |
1526 |
+ priority = None, severity = None, |
1527 |
+ add_cc = [], remove_cc = [], |
1528 |
+ add_dependson = [], remove_dependson = [], |
1529 |
+ add_blocked = [], remove_blocked = [], |
1530 |
+ whiteboard = None, keywords = None, |
1531 |
+ component = None): |
1532 |
+ """Modify an existing bug |
1533 |
+ |
1534 |
+ @param bugid: bug id |
1535 |
+ @type bugid: int |
1536 |
+ @keyword title: new title for bug |
1537 |
+ @type title: string |
1538 |
+ @keyword comment: comment to add |
1539 |
+ @type comment: string |
1540 |
+ @keyword url: new url |
1541 |
+ @type url: string |
1542 |
+ @keyword status: new status (note, if you are changing it to RESOLVED, you need to set {resolution} as well. |
1543 |
+ @type status: string |
1544 |
+ @keyword resolution: new resolution (if status=RESOLVED) |
1545 |
+ @type resolution: string |
1546 |
+ @keyword assigned_to: email (needs to exist in bugzilla) |
1547 |
+ @type assigned_to: string |
1548 |
+ @keyword duplicate: bug id to duplicate against (if resolution = DUPLICATE) |
1549 |
+ @type duplicate: int |
1550 |
+ @keyword priority: new priority for bug |
1551 |
+ @type priority: string |
1552 |
+ @keyword severity: new severity for bug |
1553 |
+ @type severity: string |
1554 |
+ @keyword add_cc: list of emails to add to the cc list |
1555 |
+ @type add_cc: list of strings |
1556 |
+ @keyword remove_cc: list of emails to remove from cc list |
1557 |
+ @type remove_cc: list of string. |
1558 |
+ @keyword add_dependson: list of bug ids to add to the depend list |
1559 |
+ @type add_dependson: list of strings |
1560 |
+ @keyword remove_dependson: list of bug ids to remove from depend list |
1561 |
+ @type remove_dependson: list of strings |
1562 |
+ @keyword add_blocked: list of bug ids to add to the blocked list |
1563 |
+ @type add_blocked: list of strings |
1564 |
+ @keyword remove_blocked: list of bug ids to remove from blocked list |
1565 |
+ @type remove_blocked: list of strings |
1566 |
+ |
1567 |
+ @keyword whiteboard: set status whiteboard |
1568 |
+ @type whiteboard: string |
1569 |
+ @keyword keywords: set keywords |
1570 |
+ @type keywords: string |
1571 |
+ @keyword component: set component |
1572 |
+ @type component: string |
1573 |
+ |
1574 |
+ @return: list of fields modified. |
1575 |
+ @rtype: list of strings |
1576 |
+ """ |
1577 |
+ if not self.authenticated and not self.skip_auth: |
1578 |
+ self.auth() |
1579 |
+ |
1580 |
+ |
1581 |
+ buginfo = Bugz.get(self, bugid) |
1582 |
+ if not buginfo: |
1583 |
+ return False |
1584 |
+ |
1585 |
+ modified = [] |
1586 |
+ qparams = config.params['modify'].copy() |
1587 |
+ qparams['id'] = bugid |
1588 |
+ # NOTE: knob has been removed in bugzilla 4 and 3? |
1589 |
+ qparams['knob'] = 'none' |
1590 |
+ |
1591 |
+ # copy existing fields |
1592 |
+ FIELDS = ('bug_file_loc', 'bug_severity', 'short_desc', 'bug_status', |
1593 |
+ 'status_whiteboard', 'keywords', 'resolution', |
1594 |
+ 'op_sys', 'priority', 'version', 'target_milestone', |
1595 |
+ 'assigned_to', 'rep_platform', 'product', 'component', 'token') |
1596 |
+ |
1597 |
+ FIELDS_MULTI = ('blocked', 'dependson') |
1598 |
+ |
1599 |
+ for field in FIELDS: |
1600 |
+ try: |
1601 |
+ qparams[field] = buginfo.find('.//%s' % field).text |
1602 |
+ if qparams[field] is None: |
1603 |
+ del qparams[field] |
1604 |
+ except: |
1605 |
+ pass |
1606 |
+ |
1607 |
+ for field in FIELDS_MULTI: |
1608 |
+ qparams[field] = [d.text for d in buginfo.findall('.//%s' % field) |
1609 |
+ if d is not None and d.text is not None] |
1610 |
+ |
1611 |
+ # set 'knob' if we are change the status/resolution |
1612 |
+ # or trying to reassign bug. |
1613 |
+ if status: |
1614 |
+ status = status.upper() |
1615 |
+ if resolution: |
1616 |
+ resolution = resolution.upper() |
1617 |
+ |
1618 |
+ if status and status != qparams['bug_status']: |
1619 |
+ # Bugzilla >= 3.x |
1620 |
+ qparams['bug_status'] = status |
1621 |
+ |
1622 |
+ if status == 'RESOLVED': |
1623 |
+ qparams['knob'] = 'resolve' |
1624 |
+ if resolution: |
1625 |
+ qparams['resolution'] = resolution |
1626 |
+ else: |
1627 |
+ qparams['resolution'] = 'FIXED' |
1628 |
+ |
1629 |
+ modified.append(('status', status)) |
1630 |
+ modified.append(('resolution', qparams['resolution'])) |
1631 |
+ elif status == 'ASSIGNED' or status == 'IN_PROGRESS': |
1632 |
+ qparams['knob'] = 'accept' |
1633 |
+ modified.append(('status', status)) |
1634 |
+ elif status == 'REOPENED': |
1635 |
+ qparams['knob'] = 'reopen' |
1636 |
+ modified.append(('status', status)) |
1637 |
+ elif status == 'VERIFIED': |
1638 |
+ qparams['knob'] = 'verified' |
1639 |
+ modified.append(('status', status)) |
1640 |
+ elif status == 'CLOSED': |
1641 |
+ qparams['knob'] = 'closed' |
1642 |
+ modified.append(('status', status)) |
1643 |
+ elif duplicate: |
1644 |
+ # Bugzilla >= 3.x |
1645 |
+ qparams['bug_status'] = "RESOLVED" |
1646 |
+ qparams['resolution'] = "DUPLICATE" |
1647 |
+ |
1648 |
+ qparams['knob'] = 'duplicate' |
1649 |
+ qparams['dup_id'] = duplicate |
1650 |
+ modified.append(('status', 'RESOLVED')) |
1651 |
+ modified.append(('resolution', 'DUPLICATE')) |
1652 |
+ elif assigned_to: |
1653 |
+ qparams['knob'] = 'reassign' |
1654 |
+ qparams['assigned_to'] = assigned_to |
1655 |
+ modified.append(('assigned_to', assigned_to)) |
1656 |
+ |
1657 |
+ # setup modification of other bits |
1658 |
+ if comment: |
1659 |
+ qparams['comment'] = comment |
1660 |
+ modified.append(('comment', ellipsis(comment, 60))) |
1661 |
+ if title: |
1662 |
+ qparams['short_desc'] = title or '' |
1663 |
+ modified.append(('title', title)) |
1664 |
+ if url is not None: |
1665 |
+ qparams['bug_file_loc'] = url |
1666 |
+ modified.append(('url', url)) |
1667 |
+ if severity is not None: |
1668 |
+ qparams['bug_severity'] = severity |
1669 |
+ modified.append(('severity', severity)) |
1670 |
+ if priority is not None: |
1671 |
+ qparams['priority'] = priority |
1672 |
+ modified.append(('priority', priority)) |
1673 |
+ |
1674 |
+ # cc manipulation |
1675 |
+ if add_cc is not None: |
1676 |
+ qparams['newcc'] = ', '.join(add_cc) |
1677 |
+ modified.append(('newcc', qparams['newcc'])) |
1678 |
+ if remove_cc is not None: |
1679 |
+ qparams['cc'] = remove_cc |
1680 |
+ qparams['removecc'] = 'on' |
1681 |
+ modified.append(('cc', remove_cc)) |
1682 |
+ |
1683 |
+ # bug depend/blocked manipulation |
1684 |
+ changed_dependson = False |
1685 |
+ changed_blocked = False |
1686 |
+ if remove_dependson: |
1687 |
+ for bug_id in remove_dependson: |
1688 |
+ qparams['dependson'].remove(str(bug_id)) |
1689 |
+ changed_dependson = True |
1690 |
+ if remove_blocked: |
1691 |
+ for bug_id in remove_blocked: |
1692 |
+ qparams['blocked'].remove(str(bug_id)) |
1693 |
+ changed_blocked = True |
1694 |
+ if add_dependson: |
1695 |
+ for bug_id in add_dependson: |
1696 |
+ qparams['dependson'].append(str(bug_id)) |
1697 |
+ changed_dependson = True |
1698 |
+ if add_blocked: |
1699 |
+ for bug_id in add_blocked: |
1700 |
+ qparams['blocked'].append(str(bug_id)) |
1701 |
+ changed_blocked = True |
1702 |
+ |
1703 |
+ qparams['dependson'] = ','.join(qparams['dependson']) |
1704 |
+ qparams['blocked'] = ','.join(qparams['blocked']) |
1705 |
+ if changed_dependson: |
1706 |
+ modified.append(('dependson', qparams['dependson'])) |
1707 |
+ if changed_blocked: |
1708 |
+ modified.append(('blocked', qparams['blocked'])) |
1709 |
+ |
1710 |
+ if whiteboard is not None: |
1711 |
+ qparams['status_whiteboard'] = whiteboard |
1712 |
+ modified.append(('status_whiteboard', whiteboard)) |
1713 |
+ if keywords is not None: |
1714 |
+ qparams['keywords'] = keywords |
1715 |
+ modified.append(('keywords', keywords)) |
1716 |
+ if component is not None: |
1717 |
+ qparams['component'] = component |
1718 |
+ modified.append(('component', component)) |
1719 |
+ |
1720 |
+ req_params = urlencode(qparams, True) |
1721 |
+ req_url = urljoin(self.base, config.urls['modify']) |
1722 |
+ req = Request(req_url, req_params, config.headers) |
1723 |
+ if self.httpuser and self.httppassword: |
1724 |
+ base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1] |
1725 |
+ req.add_header("Authorization", "Basic %s" % base64string) |
1726 |
+ |
1727 |
+ try: |
1728 |
+ resp = self.opener.open(req) |
1729 |
+ re_error = re.compile(r'id="error_msg".*>([^<]+)<') |
1730 |
+ error = re_error.search(resp.read()) |
1731 |
+ if error: |
1732 |
+ print error.group(1) |
1733 |
+ return [] |
1734 |
+ return modified |
1735 |
+ except: |
1736 |
+ return [] |
1737 |
+ |
1738 |
+ def attachment(self, attachid): |
1739 |
+ """Get an attachment by attachment_id |
1740 |
+ |
1741 |
+ @param attachid: attachment id |
1742 |
+ @type attachid: int |
1743 |
+ |
1744 |
+ @return: dict with three keys, 'filename', 'size', 'fd' |
1745 |
+ @rtype: dict |
1746 |
+ """ |
1747 |
+ if not self.authenticated and not self.skip_auth: |
1748 |
+ self.auth() |
1749 |
+ |
1750 |
+ qparams = config.params['attach'].copy() |
1751 |
+ qparams['id'] = attachid |
1752 |
+ |
1753 |
+ req_params = urlencode(qparams, True) |
1754 |
+ req_url = urljoin(self.base, config.urls['attach']) |
1755 |
+ req_url += '?' + req_params |
1756 |
+ req = Request(req_url, None, config.headers) |
1757 |
+ if self.httpuser and self.httppassword: |
1758 |
+ base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1] |
1759 |
+ req.add_header("Authorization", "Basic %s" % base64string) |
1760 |
+ resp = self.opener.open(req) |
1761 |
+ |
1762 |
+ try: |
1763 |
+ content_type = resp.info()['Content-type'] |
1764 |
+ namefield = content_type.split(';')[1] |
1765 |
+ filename = re.search(r'name=\"(.*)\"', namefield).group(1) |
1766 |
+ content_length = int(resp.info()['Content-length'], 0) |
1767 |
+ return {'filename': filename, 'size': content_length, 'fd': resp} |
1768 |
+ except: |
1769 |
+ return {} |
1770 |
+ |
1771 |
+ def post(self, product, component, title, description, url = '', assigned_to = '', cc = '', keywords = '', version = '', dependson = '', blocked = '', priority = '', severity = ''): |
1772 |
+ """Post a bug |
1773 |
+ |
1774 |
+ @param product: product where the bug should be placed |
1775 |
+ @type product: string |
1776 |
+ @param component: component where the bug should be placed |
1777 |
+ @type component: string |
1778 |
+ @param title: title of the bug. |
1779 |
+ @type title: string |
1780 |
+ @param description: description of the bug |
1781 |
+ @type description: string |
1782 |
+ @keyword url: optional url to submit with bug |
1783 |
+ @type url: string |
1784 |
+ @keyword assigned_to: optional email to assign bug to |
1785 |
+ @type assigned_to: string. |
1786 |
+ @keyword cc: option list of CC'd emails |
1787 |
+ @type: string |
1788 |
+ @keyword keywords: option list of bugzilla keywords |
1789 |
+ @type: string |
1790 |
+ @keyword version: version of the component |
1791 |
+ @type: string |
1792 |
+ @keyword dependson: bugs this one depends on |
1793 |
+ @type: string |
1794 |
+ @keyword blocked: bugs this one blocks |
1795 |
+ @type: string |
1796 |
+ @keyword priority: priority of this bug |
1797 |
+ @type: string |
1798 |
+ @keyword severity: severity of this bug |
1799 |
+ @type: string |
1800 |
+ |
1801 |
+ @rtype: int |
1802 |
+ @return: the bug number, or 0 if submission failed. |
1803 |
+ """ |
1804 |
+ if not self.authenticated and not self.skip_auth: |
1805 |
+ self.auth() |
1806 |
+ |
1807 |
+ qparams = config.params['post'].copy() |
1808 |
+ qparams['product'] = product |
1809 |
+ qparams['component'] = component |
1810 |
+ qparams['short_desc'] = title |
1811 |
+ qparams['comment'] = description |
1812 |
+ qparams['assigned_to'] = assigned_to |
1813 |
+ qparams['cc'] = cc |
1814 |
+ qparams['bug_file_loc'] = url |
1815 |
+ qparams['dependson'] = dependson |
1816 |
+ qparams['blocked'] = blocked |
1817 |
+ qparams['keywords'] = keywords |
1818 |
+ |
1819 |
+ #XXX: default version is 'unspecified' |
1820 |
+ if version != '': |
1821 |
+ qparams['version'] = version |
1822 |
+ |
1823 |
+ #XXX: default priority is 'Normal' |
1824 |
+ if priority != '': |
1825 |
+ qparams['priority'] = priority |
1826 |
+ |
1827 |
+ #XXX: default severity is 'normal' |
1828 |
+ if severity != '': |
1829 |
+ qparams['bug_severity'] = severity |
1830 |
+ |
1831 |
+ req_params = urlencode(qparams, True) |
1832 |
+ req_url = urljoin(self.base, config.urls['post']) |
1833 |
+ req = Request(req_url, req_params, config.headers) |
1834 |
+ if self.httpuser and self.httppassword: |
1835 |
+ base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1] |
1836 |
+ req.add_header("Authorization", "Basic %s" % base64string) |
1837 |
+ resp = self.opener.open(req) |
1838 |
+ |
1839 |
+ try: |
1840 |
+ re_bug = re.compile(r'(?:\s+)?<title>.*Bug ([0-9]+) Submitted.*</title>') |
1841 |
+ bug_match = re_bug.search(resp.read()) |
1842 |
+ if bug_match: |
1843 |
+ return int(bug_match.group(1)) |
1844 |
+ except: |
1845 |
+ pass |
1846 |
+ |
1847 |
+ return 0 |
1848 |
+ |
1849 |
+ def attach(self, bugid, title, description, filename, |
1850 |
+ content_type = 'text/plain', ispatch = False): |
1851 |
+ """Attach a file to a bug. |
1852 |
+ |
1853 |
+ @param bugid: bug id |
1854 |
+ @type bugid: int |
1855 |
+ @param title: short description of attachment |
1856 |
+ @type title: string |
1857 |
+ @param description: long description of the attachment |
1858 |
+ @type description: string |
1859 |
+ @param filename: filename of the attachment |
1860 |
+ @type filename: string |
1861 |
+ @keywords content_type: mime-type of the attachment |
1862 |
+ @type content_type: string |
1863 |
+ |
1864 |
+ @rtype: bool |
1865 |
+ @return: True if successful, False if not successful. |
1866 |
+ """ |
1867 |
+ if not self.authenticated and not self.skip_auth: |
1868 |
+ self.auth() |
1869 |
+ |
1870 |
+ qparams = config.params['attach_post'].copy() |
1871 |
+ qparams['bugid'] = bugid |
1872 |
+ qparams['description'] = title |
1873 |
+ qparams['comment'] = description |
1874 |
+ if ispatch: |
1875 |
+ qparams['ispatch'] = '1' |
1876 |
+ qparams['contenttypeentry'] = 'text/plain' |
1877 |
+ else: |
1878 |
+ qparams['contenttypeentry'] = content_type |
1879 |
+ |
1880 |
+ filedata = [('data', filename, open(filename).read())] |
1881 |
+ content_type, body = encode_multipart_formdata(qparams.items(), |
1882 |
+ filedata) |
1883 |
+ |
1884 |
+ req_headers = config.headers.copy() |
1885 |
+ req_headers['Content-type'] = content_type |
1886 |
+ req_headers['Content-length'] = len(body) |
1887 |
+ req_url = urljoin(self.base, config.urls['attach_post']) |
1888 |
+ req = Request(req_url, body, req_headers) |
1889 |
+ if self.httpuser and self.httppassword: |
1890 |
+ base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1] |
1891 |
+ req.add_header("Authorization", "Basic %s" % base64string) |
1892 |
+ resp = self.opener.open(req) |
1893 |
+ |
1894 |
+ # TODO: return attachment id and success? |
1895 |
+ try: |
1896 |
+ re_attach = re.compile(r'<title>(.+)</title>') |
1897 |
+ # Bugzilla 3/4 |
1898 |
+ re_attach34 = re.compile(r'Attachment \d+ added to Bug \d+') |
1899 |
+ response = resp.read() |
1900 |
+ attach_match = re_attach.search(response) |
1901 |
+ if attach_match: |
1902 |
+ if attach_match.group(1) == "Changes Submitted" or re_attach34.match(attach_match.group(1)): |
1903 |
+ return True |
1904 |
+ else: |
1905 |
+ return attach_match.group(1) |
1906 |
+ else: |
1907 |
+ return False |
1908 |
+ except: |
1909 |
+ pass |
1910 |
+ |
1911 |
+ return False |
1912 |
|
1913 |
diff --git a/third_party/pybugz-0.9.3/bugz/cli.py b/third_party/pybugz-0.9.3/bugz/cli.py |
1914 |
new file mode 100644 |
1915 |
index 0000000..35bf98e |
1916 |
--- /dev/null |
1917 |
+++ b/third_party/pybugz-0.9.3/bugz/cli.py |
1918 |
@@ -0,0 +1,607 @@ |
1919 |
+#!/usr/bin/env python |
1920 |
+ |
1921 |
+import commands |
1922 |
+import locale |
1923 |
+import os |
1924 |
+import re |
1925 |
+import sys |
1926 |
+import tempfile |
1927 |
+import textwrap |
1928 |
+ |
1929 |
+from urlparse import urljoin |
1930 |
+ |
1931 |
+try: |
1932 |
+ import readline |
1933 |
+except ImportError: |
1934 |
+ readline = None |
1935 |
+ |
1936 |
+from bugzilla import Bugz |
1937 |
+from config import config |
1938 |
+ |
1939 |
+BUGZ_COMMENT_TEMPLATE = \ |
1940 |
+""" |
1941 |
+BUGZ: --------------------------------------------------- |
1942 |
+%s |
1943 |
+BUGZ: Any line beginning with 'BUGZ:' will be ignored. |
1944 |
+BUGZ: --------------------------------------------------- |
1945 |
+""" |
1946 |
+ |
1947 |
+DEFAULT_NUM_COLS = 80 |
1948 |
+ |
1949 |
+# |
1950 |
+# Auxiliary functions |
1951 |
+# |
1952 |
+ |
1953 |
+def raw_input_block(): |
1954 |
+ """ Allows multiple line input until a Ctrl+D is detected. |
1955 |
+ |
1956 |
+ @rtype: string |
1957 |
+ """ |
1958 |
+ target = '' |
1959 |
+ while True: |
1960 |
+ try: |
1961 |
+ line = raw_input() |
1962 |
+ target += line + '\n' |
1963 |
+ except EOFError: |
1964 |
+ return target |
1965 |
+ |
1966 |
+# |
1967 |
+# This function was lifted from Bazaar 1.9. |
1968 |
+# |
1969 |
+def terminal_width(): |
1970 |
+ """Return estimated terminal width.""" |
1971 |
+ if sys.platform == 'win32': |
1972 |
+ return win32utils.get_console_size()[0] |
1973 |
+ width = DEFAULT_NUM_COLS |
1974 |
+ try: |
1975 |
+ import struct, fcntl, termios |
1976 |
+ s = struct.pack('HHHH', 0, 0, 0, 0) |
1977 |
+ x = fcntl.ioctl(1, termios.TIOCGWINSZ, s) |
1978 |
+ width = struct.unpack('HHHH', x)[1] |
1979 |
+ except IOError: |
1980 |
+ pass |
1981 |
+ if width <= 0: |
1982 |
+ try: |
1983 |
+ width = int(os.environ['COLUMNS']) |
1984 |
+ except: |
1985 |
+ pass |
1986 |
+ if width <= 0: |
1987 |
+ width = DEFAULT_NUM_COLS |
1988 |
+ |
1989 |
+ return width |
1990 |
+ |
1991 |
+def launch_editor(initial_text, comment_from = '',comment_prefix = 'BUGZ:'): |
1992 |
+ """Launch an editor with some default text. |
1993 |
+ |
1994 |
+ Lifted from Mercurial 0.9. |
1995 |
+ @rtype: string |
1996 |
+ """ |
1997 |
+ (fd, name) = tempfile.mkstemp("bugz") |
1998 |
+ f = os.fdopen(fd, "w") |
1999 |
+ f.write(comment_from) |
2000 |
+ f.write(initial_text) |
2001 |
+ f.close() |
2002 |
+ |
2003 |
+ editor = (os.environ.get("BUGZ_EDITOR") or |
2004 |
+ os.environ.get("EDITOR")) |
2005 |
+ if editor: |
2006 |
+ result = os.system("%s \"%s\"" % (editor, name)) |
2007 |
+ if result != 0: |
2008 |
+ raise RuntimeError('Unable to launch editor: %s' % editor) |
2009 |
+ |
2010 |
+ new_text = open(name).read() |
2011 |
+ new_text = re.sub('(?m)^%s.*\n' % comment_prefix, '', new_text) |
2012 |
+ os.unlink(name) |
2013 |
+ return new_text |
2014 |
+ |
2015 |
+ return '' |
2016 |
+ |
2017 |
+def block_edit(comment, comment_from = ''): |
2018 |
+ editor = (os.environ.get('BUGZ_EDITOR') or |
2019 |
+ os.environ.get('EDITOR')) |
2020 |
+ |
2021 |
+ if not editor: |
2022 |
+ print comment + ': (Press Ctrl+D to end)' |
2023 |
+ new_text = raw_input_block() |
2024 |
+ return new_text |
2025 |
+ |
2026 |
+ initial_text = '\n'.join(['BUGZ: %s'%line for line in comment.split('\n')]) |
2027 |
+ new_text = launch_editor(BUGZ_COMMENT_TEMPLATE % initial_text, comment_from) |
2028 |
+ |
2029 |
+ if new_text.strip(): |
2030 |
+ return new_text |
2031 |
+ else: |
2032 |
+ return '' |
2033 |
+ |
2034 |
+# |
2035 |
+# Bugz specific exceptions |
2036 |
+# |
2037 |
+ |
2038 |
+class BugzError(Exception): |
2039 |
+ pass |
2040 |
+ |
2041 |
+class PrettyBugz(Bugz): |
2042 |
+ def __init__(self, base, user = None, password =None, forget = False, |
2043 |
+ columns = 0, encoding = '', skip_auth = False, |
2044 |
+ quiet = False, httpuser = None, httppassword = None ): |
2045 |
+ |
2046 |
+ self.quiet = quiet |
2047 |
+ self.columns = columns or terminal_width() |
2048 |
+ |
2049 |
+ Bugz.__init__(self, base, user, password, forget, skip_auth, httpuser, httppassword) |
2050 |
+ |
2051 |
+ self.log("Using %s " % self.base) |
2052 |
+ |
2053 |
+ if not encoding: |
2054 |
+ try: |
2055 |
+ self.enc = locale.getdefaultlocale()[1] |
2056 |
+ except: |
2057 |
+ self.enc = 'utf-8' |
2058 |
+ |
2059 |
+ if not self.enc: |
2060 |
+ self.enc = 'utf-8' |
2061 |
+ else: |
2062 |
+ self.enc = encoding |
2063 |
+ |
2064 |
+ def log(self, status_msg, newline = True): |
2065 |
+ if not self.quiet: |
2066 |
+ if newline: |
2067 |
+ print ' * %s' % status_msg |
2068 |
+ else: |
2069 |
+ print ' * %s' % status_msg, |
2070 |
+ |
2071 |
+ def warn(self, warn_msg): |
2072 |
+ if not self.quiet: |
2073 |
+ print ' ! Warning: %s' % warn_msg |
2074 |
+ |
2075 |
+ def get_input(self, prompt): |
2076 |
+ return raw_input(prompt) |
2077 |
+ |
2078 |
+ def search(self, **kwds): |
2079 |
+ """Performs a search on the bugzilla database with the keywords given on the title (or the body if specified). |
2080 |
+ """ |
2081 |
+ search_term = ' '.join(kwds['terms']).strip() |
2082 |
+ del kwds['terms'] |
2083 |
+ show_status = kwds['show_status'] |
2084 |
+ del kwds['show_status'] |
2085 |
+ show_url = kwds['show_url'] |
2086 |
+ del kwds['show_url'] |
2087 |
+ search_opts = sorted([(opt, val) for opt, val in kwds.items() |
2088 |
+ if val is not None and opt != 'order']) |
2089 |
+ |
2090 |
+ if not (search_term or search_opts): |
2091 |
+ raise BugzError('Please give search terms or options.') |
2092 |
+ |
2093 |
+ if search_term: |
2094 |
+ log_msg = 'Searching for \'%s\' ' % search_term |
2095 |
+ else: |
2096 |
+ log_msg = 'Searching for bugs ' |
2097 |
+ |
2098 |
+ if search_opts: |
2099 |
+ self.log(log_msg + 'with the following options:') |
2100 |
+ for opt, val in search_opts: |
2101 |
+ self.log(' %-20s = %s' % (opt, val)) |
2102 |
+ else: |
2103 |
+ self.log(log_msg) |
2104 |
+ |
2105 |
+ result = Bugz.search(self, search_term, **kwds) |
2106 |
+ |
2107 |
+ if result is None: |
2108 |
+ raise RuntimeError('Failed to perform search') |
2109 |
+ |
2110 |
+ if len(result) == 0: |
2111 |
+ self.log('No bugs found.') |
2112 |
+ return |
2113 |
+ |
2114 |
+ self.listbugs(result, show_url, show_status) |
2115 |
+ |
2116 |
+ def namedcmd(self, command, show_status=False, show_url=False): |
2117 |
+ """Run a command stored in Bugzilla by name.""" |
2118 |
+ log_msg = 'Running namedcmd \'%s\''%command |
2119 |
+ result = Bugz.namedcmd(self, command) |
2120 |
+ if result is None: |
2121 |
+ raise RuntimeError('Failed to run command\nWrong namedcmd perhaps?') |
2122 |
+ |
2123 |
+ if len(result) == 0: |
2124 |
+ self.log('No result from command') |
2125 |
+ return |
2126 |
+ |
2127 |
+ self.listbugs(result, show_url, show_status) |
2128 |
+ |
2129 |
+ def get(self, bugid, comments = True, attachments = True): |
2130 |
+ """ Fetch bug details given the bug id """ |
2131 |
+ self.log('Getting bug %s ..' % bugid) |
2132 |
+ |
2133 |
+ result = Bugz.get(self, bugid) |
2134 |
+ |
2135 |
+ if result is None: |
2136 |
+ raise RuntimeError('Bug %s not found' % bugid) |
2137 |
+ |
2138 |
+ # Print out all the fields below by extract the text |
2139 |
+ # directly from the tag, and just ignore if we don't |
2140 |
+ # see the tag. |
2141 |
+ FIELDS = ( |
2142 |
+ ('short_desc', 'Title'), |
2143 |
+ ('assigned_to', 'Assignee'), |
2144 |
+ ('creation_ts', 'Reported'), |
2145 |
+ ('delta_ts', 'Updated'), |
2146 |
+ ('bug_status', 'Status'), |
2147 |
+ ('resolution', 'Resolution'), |
2148 |
+ ('bug_file_loc', 'URL'), |
2149 |
+ ('bug_severity', 'Severity'), |
2150 |
+ ('priority', 'Priority'), |
2151 |
+ ('reporter', 'Reporter'), |
2152 |
+ ) |
2153 |
+ |
2154 |
+ MORE_FIELDS = ( |
2155 |
+ ('product', 'Product'), |
2156 |
+ ('component', 'Component'), |
2157 |
+ ('status_whiteboard', 'Whiteboard'), |
2158 |
+ ('keywords', 'Keywords'), |
2159 |
+ ) |
2160 |
+ |
2161 |
+ for field, name in FIELDS + MORE_FIELDS: |
2162 |
+ try: |
2163 |
+ value = result.find('.//%s' % field).text |
2164 |
+ if value is None: |
2165 |
+ continue |
2166 |
+ except AttributeError: |
2167 |
+ continue |
2168 |
+ print '%-12s: %s' % (name, value.encode(self.enc)) |
2169 |
+ |
2170 |
+ # Print out the cc'ed people |
2171 |
+ cced = result.findall('.//cc') |
2172 |
+ for cc in cced: |
2173 |
+ print '%-12s: %s' % ('CC', cc.text) |
2174 |
+ |
2175 |
+ # print out depends |
2176 |
+ dependson = ', '.join([d.text for d in result.findall('.//dependson')]) |
2177 |
+ blocked = ', '.join([d.text for d in result.findall('.//blocked')]) |
2178 |
+ if dependson: |
2179 |
+ print '%-12s: %s' % ('DependsOn', dependson) |
2180 |
+ if blocked: |
2181 |
+ print '%-12s: %s' % ('Blocked', blocked) |
2182 |
+ |
2183 |
+ bug_comments = result.findall('.//long_desc') |
2184 |
+ bug_attachments = result.findall('.//attachment') |
2185 |
+ |
2186 |
+ print '%-12s: %d' % ('Comments', len(bug_comments)) |
2187 |
+ print '%-12s: %d' % ('Attachments', len(bug_attachments)) |
2188 |
+ print |
2189 |
+ |
2190 |
+ if attachments: |
2191 |
+ for attachment in bug_attachments: |
2192 |
+ aid = attachment.find('.//attachid').text |
2193 |
+ desc = attachment.find('.//desc').text |
2194 |
+ when = attachment.find('.//date').text |
2195 |
+ print '[Attachment] [%s] [%s]' % (aid, desc.encode(self.enc)) |
2196 |
+ |
2197 |
+ if comments: |
2198 |
+ i = 0 |
2199 |
+ wrapper = textwrap.TextWrapper(width = self.columns) |
2200 |
+ for comment in bug_comments: |
2201 |
+ try: |
2202 |
+ who = comment.find('.//who').text.encode(self.enc) |
2203 |
+ except AttributeError: |
2204 |
+ # Novell doesn't use 'who' on xml |
2205 |
+ who = "" |
2206 |
+ when = comment.find('.//bug_when').text.encode(self.enc) |
2207 |
+ what = comment.find('.//thetext').text |
2208 |
+ print '\n[Comment #%d] %s : %s' % (i, who, when) |
2209 |
+ print '-' * (self.columns - 1) |
2210 |
+ |
2211 |
+ if what is None: |
2212 |
+ what = '' |
2213 |
+ |
2214 |
+ # print wrapped version |
2215 |
+ for line in what.split('\n'): |
2216 |
+ if len(line) < self.columns: |
2217 |
+ print line.encode(self.enc) |
2218 |
+ else: |
2219 |
+ for shortline in wrapper.wrap(line): |
2220 |
+ print shortline.encode(self.enc) |
2221 |
+ i += 1 |
2222 |
+ print |
2223 |
+ |
2224 |
+ def post(self, product = None, component = None, |
2225 |
+ title = None, description = None, assigned_to = None, |
2226 |
+ cc = None, url = None, keywords = None, |
2227 |
+ description_from = None, prodversion = None, append_command = None, |
2228 |
+ dependson = None, blocked = None, batch = False, |
2229 |
+ default_confirm = 'y', priority = None, severity = None): |
2230 |
+ """Post a new bug""" |
2231 |
+ |
2232 |
+ # load description from file if possible |
2233 |
+ if description_from: |
2234 |
+ try: |
2235 |
+ description = open(description_from, 'r').read() |
2236 |
+ except IOError, e: |
2237 |
+ raise BugzError('Unable to read from file: %s: %s' % \ |
2238 |
+ (description_from, e)) |
2239 |
+ |
2240 |
+ if not batch: |
2241 |
+ self.log('Press Ctrl+C at any time to abort.') |
2242 |
+ |
2243 |
+ # |
2244 |
+ # Check all bug fields. |
2245 |
+ # XXX: We use "if not <field>" for mandatory fields |
2246 |
+ # and "if <field> is None" for optional ones. |
2247 |
+ # |
2248 |
+ |
2249 |
+ # check for product |
2250 |
+ if not product: |
2251 |
+ while not product or len(product) < 1: |
2252 |
+ product = self.get_input('Enter product: ') |
2253 |
+ else: |
2254 |
+ self.log('Enter product: %s' % product) |
2255 |
+ |
2256 |
+ # check for component |
2257 |
+ if not component: |
2258 |
+ while not component or len(component) < 1: |
2259 |
+ component = self.get_input('Enter component: ') |
2260 |
+ else: |
2261 |
+ self.log('Enter component: %s' % component) |
2262 |
+ |
2263 |
+ # check for version |
2264 |
+ # FIXME: This default behaviour is not too nice. |
2265 |
+ if prodversion is None: |
2266 |
+ prodversion = self.get_input('Enter version (default: unspecified): ') |
2267 |
+ else: |
2268 |
+ self.log('Enter version: %s' % prodversion) |
2269 |
+ |
2270 |
+ # check for default severity |
2271 |
+ if severity is None: |
2272 |
+ severity_msg ='Enter severity (eg. normal) (optional): ' |
2273 |
+ severity = self.get_input(severity_msg) |
2274 |
+ else: |
2275 |
+ self.log('Enter severity (optional): %s' % severity) |
2276 |
+ |
2277 |
+ # fixme: hw platform |
2278 |
+ # fixme: os |
2279 |
+ # fixme: milestone |
2280 |
+ |
2281 |
+ # check for default priority |
2282 |
+ if priority is None: |
2283 |
+ priority_msg ='Enter priority (eg. Normal) (optional): ' |
2284 |
+ priority = self.get_input(priority_msg) |
2285 |
+ else: |
2286 |
+ self.log('Enter priority (optional): %s' % priority) |
2287 |
+ |
2288 |
+ # fixme: status |
2289 |
+ |
2290 |
+ # check for default assignee |
2291 |
+ if assigned_to is None: |
2292 |
+ assigned_msg ='Enter assignee (eg. liquidx@g.o) (optional): ' |
2293 |
+ assigned_to = self.get_input(assigned_msg) |
2294 |
+ else: |
2295 |
+ self.log('Enter assignee (optional): %s' % assigned_to) |
2296 |
+ |
2297 |
+ # check for CC list |
2298 |
+ if cc is None: |
2299 |
+ cc_msg = 'Enter a CC list (comma separated) (optional): ' |
2300 |
+ cc = self.get_input(cc_msg) |
2301 |
+ else: |
2302 |
+ self.log('Enter a CC list (optional): %s' % cc) |
2303 |
+ |
2304 |
+ # check for optional URL |
2305 |
+ if url is None: |
2306 |
+ url = self.get_input('Enter URL (optional): ') |
2307 |
+ else: |
2308 |
+ self.log('Enter URL (optional): %s' % url) |
2309 |
+ |
2310 |
+ # check for title |
2311 |
+ if not title: |
2312 |
+ while not title or len(title) < 1: |
2313 |
+ title = self.get_input('Enter title: ') |
2314 |
+ else: |
2315 |
+ self.log('Enter title: %s' % title) |
2316 |
+ |
2317 |
+ # check for description |
2318 |
+ if not description: |
2319 |
+ description = block_edit('Enter bug description: ') |
2320 |
+ else: |
2321 |
+ self.log('Enter bug description: %s' % description) |
2322 |
+ |
2323 |
+ if append_command is None: |
2324 |
+ append_command = self.get_input('Append the output of the following command (leave blank for none): ') |
2325 |
+ else: |
2326 |
+ self.log('Append command (optional): %s' % append_command) |
2327 |
+ |
2328 |
+ # check for Keywords list |
2329 |
+ if keywords is None: |
2330 |
+ kwd_msg = 'Enter a Keywords list (comma separated) (optional): ' |
2331 |
+ keywords = self.get_input(kwd_msg) |
2332 |
+ else: |
2333 |
+ self.log('Enter a Keywords list (optional): %s' % keywords) |
2334 |
+ |
2335 |
+ # check for bug dependencies |
2336 |
+ if dependson is None: |
2337 |
+ dependson_msg = 'Enter a list of bug dependencies (comma separated) (optional): ' |
2338 |
+ dependson = self.get_input(dependson_msg) |
2339 |
+ else: |
2340 |
+ self.log('Enter a list of bug dependencies (optional): %s' % dependson) |
2341 |
+ |
2342 |
+ # check for blocker bugs |
2343 |
+ if blocked is None: |
2344 |
+ blocked_msg = 'Enter a list of blocker bugs (comma separated) (optional): ' |
2345 |
+ blocked = self.get_input(blocked_msg) |
2346 |
+ else: |
2347 |
+ self.log('Enter a list of blocker bugs (optional): %s' % blocked) |
2348 |
+ |
2349 |
+ # fixme: groups |
2350 |
+ # append the output from append_command to the description |
2351 |
+ if append_command is not None and append_command != '': |
2352 |
+ append_command_output = commands.getoutput(append_command) |
2353 |
+ description = description + '\n\n' + '$ ' + append_command + '\n' + append_command_output |
2354 |
+ |
2355 |
+ # raise an exception if mandatory fields are not specified. |
2356 |
+ if product is None: |
2357 |
+ raise RuntimeError('Product not specified') |
2358 |
+ if component is None: |
2359 |
+ raise RuntimeError('Component not specified') |
2360 |
+ if title is None: |
2361 |
+ raise RuntimeError('Title not specified') |
2362 |
+ if description is None: |
2363 |
+ raise RuntimeError('Description not specified') |
2364 |
+ |
2365 |
+ # set optional fields to their defaults if they are not set. |
2366 |
+ if prodversion is None: |
2367 |
+ prodversion = '' |
2368 |
+ if priority is None: |
2369 |
+ priority = '' |
2370 |
+ if severity is None: |
2371 |
+ severity = '' |
2372 |
+ if assigned_to is None: |
2373 |
+ assigned_to = '' |
2374 |
+ if cc is None: |
2375 |
+ cc = '' |
2376 |
+ if url is None: |
2377 |
+ url = '' |
2378 |
+ if keywords is None: |
2379 |
+ keywords = '' |
2380 |
+ if dependson is None: |
2381 |
+ dependson = '' |
2382 |
+ if blocked is None: |
2383 |
+ blocked = '' |
2384 |
+ |
2385 |
+ # print submission confirmation |
2386 |
+ print '-' * (self.columns - 1) |
2387 |
+ print 'Product : ' + product |
2388 |
+ print 'Component : ' + component |
2389 |
+ print 'Version : ' + prodversion |
2390 |
+ print 'severity : ' + severity |
2391 |
+ # fixme: hardware |
2392 |
+ # fixme: OS |
2393 |
+ # fixme: Milestone |
2394 |
+ print 'priority : ' + priority |
2395 |
+ # fixme: status |
2396 |
+ print 'Assigned to : ' + assigned_to |
2397 |
+ print 'CC : ' + cc |
2398 |
+ print 'URL : ' + url |
2399 |
+ print 'Title : ' + title |
2400 |
+ print 'Description : ' + description |
2401 |
+ print 'Keywords : ' + keywords |
2402 |
+ print 'Depends on : ' + dependson |
2403 |
+ print 'Blocks : ' + blocked |
2404 |
+ # fixme: groups |
2405 |
+ print '-' * (self.columns - 1) |
2406 |
+ |
2407 |
+ if not batch: |
2408 |
+ if default_confirm in ['Y','y']: |
2409 |
+ confirm = raw_input('Confirm bug submission (Y/n)? ') |
2410 |
+ else: |
2411 |
+ confirm = raw_input('Confirm bug submission (y/N)? ') |
2412 |
+ if len(confirm) < 1: |
2413 |
+ confirm = default_confirm |
2414 |
+ if confirm[0] not in ('y', 'Y'): |
2415 |
+ self.log('Submission aborted') |
2416 |
+ return |
2417 |
+ |
2418 |
+ result = Bugz.post(self, product, component, title, description, url, assigned_to, cc, keywords, prodversion, dependson, blocked, priority, severity) |
2419 |
+ if result is not None and result != 0: |
2420 |
+ self.log('Bug %d submitted' % result) |
2421 |
+ else: |
2422 |
+ raise RuntimeError('Failed to submit bug') |
2423 |
+ |
2424 |
+ def modify(self, bugid, **kwds): |
2425 |
+ """Modify an existing bug (eg. adding a comment or changing resolution.)""" |
2426 |
+ if 'comment_from' in kwds: |
2427 |
+ if kwds['comment_from']: |
2428 |
+ try: |
2429 |
+ kwds['comment'] = open(kwds['comment_from'], 'r').read() |
2430 |
+ except IOError, e: |
2431 |
+ raise BugzError('Failed to get read from file: %s: %s' % \ |
2432 |
+ (comment_from, e)) |
2433 |
+ |
2434 |
+ if 'comment_editor' in kwds: |
2435 |
+ if kwds['comment_editor']: |
2436 |
+ kwds['comment'] = block_edit('Enter comment:', kwds['comment']) |
2437 |
+ del kwds['comment_editor'] |
2438 |
+ |
2439 |
+ del kwds['comment_from'] |
2440 |
+ |
2441 |
+ if 'comment_editor' in kwds: |
2442 |
+ if kwds['comment_editor']: |
2443 |
+ kwds['comment'] = block_edit('Enter comment:') |
2444 |
+ del kwds['comment_editor'] |
2445 |
+ |
2446 |
+ if kwds['fixed']: |
2447 |
+ kwds['status'] = 'RESOLVED' |
2448 |
+ kwds['resolution'] = 'FIXED' |
2449 |
+ del kwds['fixed'] |
2450 |
+ |
2451 |
+ if kwds['invalid']: |
2452 |
+ kwds['status'] = 'RESOLVED' |
2453 |
+ kwds['resolution'] = 'INVALID' |
2454 |
+ del kwds['invalid'] |
2455 |
+ result = Bugz.modify(self, bugid, **kwds) |
2456 |
+ if not result: |
2457 |
+ raise RuntimeError('Failed to modify bug') |
2458 |
+ else: |
2459 |
+ self.log('Modified bug %s with the following fields:' % bugid) |
2460 |
+ for field, value in result: |
2461 |
+ self.log(' %-12s: %s' % (field, value)) |
2462 |
+ |
2463 |
+ def attachment(self, attachid, view = False): |
2464 |
+ """ Download or view an attachment given the id.""" |
2465 |
+ self.log('Getting attachment %s' % attachid) |
2466 |
+ |
2467 |
+ result = Bugz.attachment(self, attachid) |
2468 |
+ if not result: |
2469 |
+ raise RuntimeError('Unable to get attachment') |
2470 |
+ |
2471 |
+ action = {True:'Viewing', False:'Saving'} |
2472 |
+ self.log('%s attachment: "%s"' % (action[view], result['filename'])) |
2473 |
+ safe_filename = os.path.basename(re.sub(r'\.\.', '', |
2474 |
+ result['filename'])) |
2475 |
+ |
2476 |
+ if view: |
2477 |
+ print result['fd'].read() |
2478 |
+ else: |
2479 |
+ if os.path.exists(result['filename']): |
2480 |
+ raise RuntimeError('Filename already exists') |
2481 |
+ |
2482 |
+ open(safe_filename, 'wb').write(result['fd'].read()) |
2483 |
+ |
2484 |
+ def attach(self, bugid, filename, content_type = 'text/plain', patch = False, description = None): |
2485 |
+ """ Attach a file to a bug given a filename. """ |
2486 |
+ if not os.path.exists(filename): |
2487 |
+ raise BugzError('File not found: %s' % filename) |
2488 |
+ if not description: |
2489 |
+ description = block_edit('Enter description (optional)') |
2490 |
+ result = Bugz.attach(self, bugid, filename, description, filename, |
2491 |
+ content_type, patch) |
2492 |
+ if result == True: |
2493 |
+ self.log("'%s' has been attached to bug %s" % (filename, bugid)) |
2494 |
+ else: |
2495 |
+ reason = "" |
2496 |
+ if result and result != False: |
2497 |
+ reason = "\nreason: %s" % result |
2498 |
+ raise RuntimeError("Failed to attach '%s' to bug %s%s" % (filename, |
2499 |
+ bugid, reason)) |
2500 |
+ |
2501 |
+ def listbugs(self, buglist, show_url=False, show_status=False): |
2502 |
+ x = '' |
2503 |
+ if re.search("/$", self.base) is None: |
2504 |
+ x = '/' |
2505 |
+ for row in buglist: |
2506 |
+ bugid = row['bugid'] |
2507 |
+ if show_url: |
2508 |
+ bugid = '%s%s%s?id=%s'%(self.base, x, config.urls['show'], bugid) |
2509 |
+ status = row['status'] |
2510 |
+ desc = row['desc'] |
2511 |
+ line = '%s' % (bugid) |
2512 |
+ if show_status: |
2513 |
+ line = '%s %s' % (line, status) |
2514 |
+ if row.has_key('assignee'): # Novell does not have 'assignee' field |
2515 |
+ assignee = row['assignee'].split('@')[0] |
2516 |
+ line = '%s %-20s' % (line, assignee) |
2517 |
+ |
2518 |
+ line = '%s %s' % (line, desc) |
2519 |
+ |
2520 |
+ try: |
2521 |
+ print line.encode(self.enc)[:self.columns] |
2522 |
+ except UnicodeDecodeError: |
2523 |
+ print line[:self.columns] |
2524 |
+ |
2525 |
+ self.log("%i bug(s) found." % len(buglist)) |
2526 |
|
2527 |
diff --git a/third_party/pybugz-0.9.3/bugz/config.py b/third_party/pybugz-0.9.3/bugz/config.py |
2528 |
new file mode 100644 |
2529 |
index 0000000..5ca48c3 |
2530 |
--- /dev/null |
2531 |
+++ b/third_party/pybugz-0.9.3/bugz/config.py |
2532 |
@@ -0,0 +1,229 @@ |
2533 |
+#!/usr/bin/env python |
2534 |
+ |
2535 |
+from bugz import __version__ |
2536 |
+import csv |
2537 |
+import locale |
2538 |
+ |
2539 |
+BUGZ_USER_AGENT = 'PyBugz/%s +http://www.github.com/williamh/pybugz/' % __version__ |
2540 |
+ |
2541 |
+class BugzConfig: |
2542 |
+ urls = { |
2543 |
+ 'auth': 'index.cgi', |
2544 |
+ 'list': 'buglist.cgi', |
2545 |
+ 'show': 'show_bug.cgi', |
2546 |
+ 'attach': 'attachment.cgi', |
2547 |
+ 'post': 'post_bug.cgi', |
2548 |
+ 'modify': 'process_bug.cgi', |
2549 |
+ 'attach_post': 'attachment.cgi', |
2550 |
+ } |
2551 |
+ |
2552 |
+ headers = { |
2553 |
+ 'Accept': '*/*', |
2554 |
+ 'User-agent': BUGZ_USER_AGENT, |
2555 |
+ } |
2556 |
+ |
2557 |
+ params = { |
2558 |
+ 'auth': { |
2559 |
+ "Bugzilla_login": "", |
2560 |
+ "Bugzilla_password": "", |
2561 |
+ "GoAheadAndLogIn": "1", |
2562 |
+ }, |
2563 |
+ |
2564 |
+ 'post': { |
2565 |
+ 'product': '', |
2566 |
+ 'version': 'unspecified', |
2567 |
+ 'component': '', |
2568 |
+ 'short_desc': '', |
2569 |
+ 'comment': '', |
2570 |
+# 'rep_platform': 'All', |
2571 |
+# 'op_sys': 'Linux', |
2572 |
+ }, |
2573 |
+ |
2574 |
+ 'attach': { |
2575 |
+ 'id':'' |
2576 |
+ }, |
2577 |
+ |
2578 |
+ 'attach_post': { |
2579 |
+ 'action': 'insert', |
2580 |
+ 'ispatch': '', |
2581 |
+ 'contenttypemethod': 'manual', |
2582 |
+ 'bugid': '', |
2583 |
+ 'description': '', |
2584 |
+ 'contenttypeentry': 'text/plain', |
2585 |
+ 'comment': '', |
2586 |
+ }, |
2587 |
+ |
2588 |
+ 'show': { |
2589 |
+ 'id': '', |
2590 |
+ 'ctype': 'xml' |
2591 |
+ }, |
2592 |
+ |
2593 |
+ 'list': { |
2594 |
+ 'query_format': 'advanced', |
2595 |
+ 'short_desc_type': 'allwordssubstr', |
2596 |
+ 'short_desc': '', |
2597 |
+ 'long_desc_type': 'substring', |
2598 |
+ 'long_desc' : '', |
2599 |
+ 'bug_file_loc_type': 'allwordssubstr', |
2600 |
+ 'bug_file_loc': '', |
2601 |
+ 'status_whiteboard_type': 'allwordssubstr', |
2602 |
+ 'status_whiteboard': '', |
2603 |
+ # NEW, ASSIGNED and REOPENED is obsolete as of bugzilla 3.x and has |
2604 |
+ # been removed from bugs.gentoo.org on 2011/05/01 |
2605 |
+ 'bug_status': ['NEW', 'ASSIGNED', 'REOPENED', 'UNCONFIRMED', 'CONFIRMED', 'IN_PROGRESS'], |
2606 |
+ 'bug_severity': [], |
2607 |
+ 'priority': [], |
2608 |
+ 'emaillongdesc1': '1', |
2609 |
+ 'emailassigned_to1':'1', |
2610 |
+ 'emailtype1': 'substring', |
2611 |
+ 'email1': '', |
2612 |
+ 'emaillongdesc2': '1', |
2613 |
+ 'emailassigned_to2':'1', |
2614 |
+ 'emailreporter2':'1', |
2615 |
+ 'emailcc2':'1', |
2616 |
+ 'emailtype2':'substring', |
2617 |
+ 'email2':'', |
2618 |
+ 'bugidtype':'include', |
2619 |
+ 'bug_id':'', |
2620 |
+ 'chfieldfrom':'', |
2621 |
+ 'chfieldto':'Now', |
2622 |
+ 'chfieldvalue':'', |
2623 |
+ 'cmdtype':'doit', |
2624 |
+ 'order': 'Bug Number', |
2625 |
+ 'field0-0-0':'noop', |
2626 |
+ 'type0-0-0':'noop', |
2627 |
+ 'value0-0-0':'', |
2628 |
+ 'ctype':'csv', |
2629 |
+ }, |
2630 |
+ |
2631 |
+ 'modify': { |
2632 |
+ # 'delta_ts': '%Y-%m-%d %H:%M:%S', |
2633 |
+ 'longdesclength': '1', |
2634 |
+ 'id': '', |
2635 |
+ 'newcc': '', |
2636 |
+ 'removecc': '', # remove selected cc's if set |
2637 |
+ 'cc': '', # only if there are already cc's |
2638 |
+ 'bug_file_loc': '', |
2639 |
+ 'bug_severity': '', |
2640 |
+ 'bug_status': '', |
2641 |
+ 'op_sys': '', |
2642 |
+ 'priority': '', |
2643 |
+ 'version': '', |
2644 |
+ 'target_milestone': '', |
2645 |
+ 'rep_platform': '', |
2646 |
+ 'product':'', |
2647 |
+ 'component': '', |
2648 |
+ 'short_desc': '', |
2649 |
+ 'status_whiteboard': '', |
2650 |
+ 'keywords': '', |
2651 |
+ 'dependson': '', |
2652 |
+ 'blocked': '', |
2653 |
+ 'knob': ('none', 'assigned', 'resolve', 'duplicate', 'reassign'), |
2654 |
+ 'resolution': '', # only valid for knob=resolve |
2655 |
+ 'dup_id': '', # only valid for knob=duplicate |
2656 |
+ 'assigned_to': '',# only valid for knob=reassign |
2657 |
+ 'form_name': 'process_bug', |
2658 |
+ 'comment':'' |
2659 |
+ }, |
2660 |
+ |
2661 |
+ 'namedcmd': { |
2662 |
+ 'cmdtype' : 'runnamed', |
2663 |
+ 'namedcmd' : '', |
2664 |
+ 'ctype':'csv' |
2665 |
+ } |
2666 |
+ } |
2667 |
+ |
2668 |
+ choices = { |
2669 |
+ 'status': { |
2670 |
+ 'unconfirmed': 'UNCONFIRMED', |
2671 |
+ 'confirmed': 'CONFIRMED', |
2672 |
+ 'new': 'NEW', |
2673 |
+ 'assigned': 'ASSIGNED', |
2674 |
+ 'in_progress': 'IN_PROGRESS', |
2675 |
+ 'reopened': 'REOPENED', |
2676 |
+ 'resolved': 'RESOLVED', |
2677 |
+ 'verified': 'VERIFIED', |
2678 |
+ 'closed': 'CLOSED' |
2679 |
+ }, |
2680 |
+ |
2681 |
+ 'order': { |
2682 |
+ 'number' : 'Bug Number', |
2683 |
+ 'assignee': 'Assignee', |
2684 |
+ 'importance': 'Importance', |
2685 |
+ 'date': 'Last Changed' |
2686 |
+ }, |
2687 |
+ |
2688 |
+ 'columns': [ |
2689 |
+ 'bugid', |
2690 |
+ 'alias', |
2691 |
+ 'severity', |
2692 |
+ 'priority', |
2693 |
+ 'arch', |
2694 |
+ 'assignee', |
2695 |
+ 'status', |
2696 |
+ 'resolution', |
2697 |
+ 'desc' |
2698 |
+ ], |
2699 |
+ |
2700 |
+ 'column_alias': { |
2701 |
+ 'bug_id': 'bugid', |
2702 |
+ 'alias': 'alias', |
2703 |
+ 'bug_severity': 'severity', |
2704 |
+ 'priority': 'priority', |
2705 |
+ 'op_sys': 'arch', #XXX: Gentoo specific? |
2706 |
+ 'assigned_to': 'assignee', |
2707 |
+ 'assigned_to_realname': 'assignee', #XXX: Distinguish from assignee? |
2708 |
+ 'bug_status': 'status', |
2709 |
+ 'resolution': 'resolution', |
2710 |
+ 'short_desc': 'desc', |
2711 |
+ 'short_short_desc': 'desc', |
2712 |
+ }, |
2713 |
+ # Novell: bug_id,"bug_severity","priority","op_sys","bug_status","resolution","short_desc" |
2714 |
+ # Gentoo: bug_id,"bug_severity","priority","op_sys","assigned_to","bug_status","resolution","short_short_desc" |
2715 |
+ # Redhat: bug_id,"alias","bug_severity","priority","rep_platform","assigned_to","bug_status","resolution","short_short_desc" |
2716 |
+ # Mandriva: 'bug_id', 'bug_severity', 'priority', 'assigned_to_realname', 'bug_status', 'resolution', 'keywords', 'short_desc' |
2717 |
+ |
2718 |
+ 'resolution': { |
2719 |
+ 'fixed': 'FIXED', |
2720 |
+ 'invalid': 'INVALID', |
2721 |
+ 'wontfix': 'WONTFIX', |
2722 |
+ 'lated': 'LATER', |
2723 |
+ 'remind': 'REMIND', |
2724 |
+ 'worksforme': 'WORKSFORME', |
2725 |
+ 'cantfix': 'CANTFIX', |
2726 |
+ 'needinfo': 'NEEDINFO', |
2727 |
+ 'test-request': 'TEST-REQUEST', |
2728 |
+ 'upstream': 'UPSTREAM', |
2729 |
+ 'duplicate': 'DUPLICATE', |
2730 |
+ }, |
2731 |
+ |
2732 |
+ 'severity': [ |
2733 |
+ 'blocker', |
2734 |
+ 'critical', |
2735 |
+ 'major', |
2736 |
+ 'normal', |
2737 |
+ 'minor', |
2738 |
+ 'trivial', |
2739 |
+ 'enhancement', |
2740 |
+ 'QA', |
2741 |
+ ], |
2742 |
+ |
2743 |
+ 'priority': { |
2744 |
+ 1:'Highest', |
2745 |
+ 2:'High', |
2746 |
+ 3:'Normal', |
2747 |
+ 4:'Low', |
2748 |
+ 5:'Lowest', |
2749 |
+ } |
2750 |
+ |
2751 |
+ } |
2752 |
+ |
2753 |
+# |
2754 |
+# Global configuration |
2755 |
+# |
2756 |
+ |
2757 |
+try: |
2758 |
+ config |
2759 |
+except NameError: |
2760 |
+ config = BugzConfig() |
2761 |
+ |
2762 |
|
2763 |
diff --git a/third_party/pybugz-0.9.3/bugzrc.example b/third_party/pybugz-0.9.3/bugzrc.example |
2764 |
new file mode 100644 |
2765 |
index 0000000..3be9006 |
2766 |
--- /dev/null |
2767 |
+++ b/third_party/pybugz-0.9.3/bugzrc.example |
2768 |
@@ -0,0 +1,25 @@ |
2769 |
+# |
2770 |
+# bugzrc.example - an example configuration file for pybugz |
2771 |
+# |
2772 |
+# This file consists of sections which define parameters for each |
2773 |
+# bugzilla you plan to use. |
2774 |
+# |
2775 |
+# Each section begins with a name in square brackets. This is also the |
2776 |
+# name that should be used with the --connection parameter to the bugz |
2777 |
+# command. |
2778 |
+# |
2779 |
+# Each section of this file consists of lines in the form: |
2780 |
+# key: value |
2781 |
+# as listed below. |
2782 |
+# |
2783 |
+# [sectionname] |
2784 |
+# base: http://my.project.com/bugzilla/ |
2785 |
+# user: xyz@×××.org |
2786 |
+# password: secret2 |
2787 |
+# httpuser: xyz |
2788 |
+# httppassword: secret2 |
2789 |
+# forget: True |
2790 |
+# columns: 80 |
2791 |
+# encoding: utf-8 |
2792 |
+# quiet: True |
2793 |
+ |
2794 |
|
2795 |
diff --git a/third_party/pybugz-0.9.3/contrib/bash-completion b/third_party/pybugz-0.9.3/contrib/bash-completion |
2796 |
new file mode 100644 |
2797 |
index 0000000..4edaf63 |
2798 |
--- /dev/null |
2799 |
+++ b/third_party/pybugz-0.9.3/contrib/bash-completion |
2800 |
@@ -0,0 +1,66 @@ |
2801 |
+# |
2802 |
+# Bash completion support for bugz |
2803 |
+# |
2804 |
+_bugz() { |
2805 |
+ local cur prev commands opts |
2806 |
+ commands="attach attachment get help modify namedcmd post search" |
2807 |
+ opts="--version -h --help --skip-auth -f --forget --encoding -q --quiet |
2808 |
+ -b --base -u --user -H --httpuser -p --password --columns |
2809 |
+ -P --httppassword" |
2810 |
+ COMPREPLY=() |
2811 |
+ cur="${COMP_WORDS[COMP_CWORD]}" |
2812 |
+ if [[ $COMP_CWORD -eq 1 ]]; then |
2813 |
+ if [[ "$cur" == -* ]]; then |
2814 |
+ COMPREPLY=( $( compgen -W '--help -h --version' -- $cur ) ) |
2815 |
+ else |
2816 |
+ COMPREPLY=( $( compgen -W "$commands" -- $cur ) ) |
2817 |
+ fi |
2818 |
+ else |
2819 |
+ prev="${COMP_WORDS[COMP_CWORD-1]}" |
2820 |
+ command="${COMP_WORDS[1]}" |
2821 |
+ case ${command} in |
2822 |
+ attach) |
2823 |
+ opts="${opts} -d --description -c --content_type" |
2824 |
+ ;; |
2825 |
+ attachment) |
2826 |
+ opts="${opts} -v --view" |
2827 |
+ ;; |
2828 |
+ get) |
2829 |
+ opts="${opts} -n --no-comments" |
2830 |
+ ;; |
2831 |
+ modify) |
2832 |
+ opts="${opts} |
2833 |
+ -c --comment -s --status -F --comment-from |
2834 |
+ --fixed -S --severity -t --title -U --url |
2835 |
+ -w --whiteboard --add-dependson --invalid |
2836 |
+ --add-blocked --priority --remove-cc -d --duplicate |
2837 |
+ --remove-dependson -a --assigned-to -k --keywords |
2838 |
+ --add-cc -C --comment-editor -r --resolution |
2839 |
+ --remove-blocked" |
2840 |
+ ;; |
2841 |
+ namedcmd) |
2842 |
+ opts="${opts} --show-url --show-status" |
2843 |
+ ;; |
2844 |
+ post) |
2845 |
+ opts="${opts} |
2846 |
+ --product -d --description -t --title |
2847 |
+ --append-command -S --severity --depends-on --component |
2848 |
+ --batch --prodversion --default-confirm --priority |
2849 |
+ -F --description-from -U --url -a --assigned-to |
2850 |
+ -k --keywords --cc --blocked" |
2851 |
+ ;; |
2852 |
+ search) |
2853 |
+ opts="${opts} |
2854 |
+ -s --status --show-url --product -w --whiteboard |
2855 |
+ --severity -r --reporter --cc --commenter |
2856 |
+ -C --component -c --comments --priority |
2857 |
+ -a --assigned-to -k --keywords -o --order --show-status" |
2858 |
+ ;; |
2859 |
+ *) |
2860 |
+ ;; |
2861 |
+ esac |
2862 |
+ COMPREPLY=( $( compgen -W "$opts" -- $cur ) ) |
2863 |
+ fi |
2864 |
+ return 0 |
2865 |
+} |
2866 |
+complete -F _bugz bugz |
2867 |
|
2868 |
diff --git a/third_party/pybugz-0.9.3/contrib/zsh-completion b/third_party/pybugz-0.9.3/contrib/zsh-completion |
2869 |
new file mode 100644 |
2870 |
index 0000000..c88ebff |
2871 |
--- /dev/null |
2872 |
+++ b/third_party/pybugz-0.9.3/contrib/zsh-completion |
2873 |
@@ -0,0 +1,158 @@ |
2874 |
+#compdef bugz |
2875 |
+# Copyright 2009 Ingmar Vanhassel <ingmar@×××××××.org> |
2876 |
+# vim: set et sw=2 sts=2 ts=2 ft=zsh : |
2877 |
+ |
2878 |
+_bugz() { |
2879 |
+ local -a _bugz_options _bugz_commands |
2880 |
+ local cmd |
2881 |
+ |
2882 |
+ _bugz_options=( |
2883 |
+ '(-b --base)'{-b,--base}'[bugzilla base URL]:bugzilla url: ' |
2884 |
+ '(-u --user)'{-u,--user}'[user name (if required)]:user name:_users' |
2885 |
+ '(-p --password)'{-p,--password}'[password (if required)]:password: ' |
2886 |
+ '(-H --httpuser)'{-H,--httpuser}'[basic http auth user name (if required)]:user name:_users' |
2887 |
+ '(-P --httppassword)'{-P,--httppassword}'[basic http auth password (if required)]:password: ' |
2888 |
+ '(-f --forget)'{-f,--forget}'[do not remember authentication]' |
2889 |
+ '--columns[number of columns to use when displaying output]:number: ' |
2890 |
+ '--skip-auth[do not authenticate]' |
2891 |
+ '(-q --quiet)'{-q,--quiet}'[do not display status messages]' |
2892 |
+ ) |
2893 |
+ _bugz_commands=( |
2894 |
+ 'attach:attach file to a bug' |
2895 |
+ 'attachment:get an attachment from bugzilla' |
2896 |
+ 'get:get a bug from bugzilla' |
2897 |
+ 'help:display subcommands' |
2898 |
+ 'modify:modify a bug (eg. post a comment)' |
2899 |
+ 'namedcmd:run a stored search' |
2900 |
+ 'post:post a new bug into bugzilla' |
2901 |
+ 'search:search for bugs in bugzilla' |
2902 |
+ ) |
2903 |
+ |
2904 |
+ for (( i=1; i <= ${CURRENT}; i++ )); do |
2905 |
+ cmd=${_bugz_commands[(r)${words[${i}]}:*]%%:*} |
2906 |
+ (( ${#cmd} )) && break |
2907 |
+ done |
2908 |
+ |
2909 |
+ if (( ${#cmd} )); then |
2910 |
+ local curcontext="${curcontext%:*:*}:bugz-${cmd}:" |
2911 |
+ |
2912 |
+ while [[ ${words[1]} != ${cmd} ]]; do |
2913 |
+ (( CURRENT-- )) |
2914 |
+ shift words |
2915 |
+ done |
2916 |
+ |
2917 |
+ _call_function ret _bugz_cmd_${cmd} |
2918 |
+ return ret |
2919 |
+ else |
2920 |
+ _arguments -s : $_bugz_options |
2921 |
+ _describe -t commands 'commands' _bugz_commands |
2922 |
+ fi |
2923 |
+} |
2924 |
+ |
2925 |
+(( ${+functions[_bugz_cmd_attach]} )) || |
2926 |
+_bugz_cmd_attach() |
2927 |
+{ |
2928 |
+ _arguments -s : \ |
2929 |
+ '(--content_type= -c)'{--content_type=,-c}'[mimetype of the file]:MIME-Type:_mime_types' \ |
2930 |
+ '(--description= -d)'{--description=,-d}'[a description of the attachment]:description: ' \ |
2931 |
+ '--help[show help message and exit]' |
2932 |
+} |
2933 |
+ |
2934 |
+(( ${+functions[_bugz_cmd_attachment]} )) || |
2935 |
+_bugz_cmd_attachment() |
2936 |
+{ |
2937 |
+ _arguments -s : \ |
2938 |
+ '--help[show help message and exit]' \ |
2939 |
+ '(--view -v)'{--view,-v}'[print attachment rather than save]' |
2940 |
+} |
2941 |
+ |
2942 |
+ |
2943 |
+(( ${+functions[_bugz_cmd_get]} )) || |
2944 |
+_bugz_cmd_get() |
2945 |
+{ |
2946 |
+ _arguments -s : \ |
2947 |
+ '--help[show help message and exit]' \ |
2948 |
+ '(--no-comments -n)'{--no-comments,-n}'[do not show comments]' |
2949 |
+} |
2950 |
+ |
2951 |
+(( ${+functions[_bugz_cmd_modify]} )) || |
2952 |
+_bugz_cmd_modify() |
2953 |
+{ |
2954 |
+ _arguments -s : \ |
2955 |
+ '--add-blocked=[add a bug to the blocked list]:bug: ' \ |
2956 |
+ '--add-dependson=[add a bug to the depends list]:bug: ' \ |
2957 |
+ '--add-cc=[add an email to CC list]:email: ' \ |
2958 |
+ '(--assigned-to= -a)'{--assigned-to=,-a}'[assign bug to someone other than the default assignee]:assignee: ' \ |
2959 |
+ '(--comment= -c)'{--comment=,-c}'[add comment to bug]:Comment: ' \ |
2960 |
+ '(--comment-editor -C)'{--comment-editor,-C}'[add comment via default EDITOR]' \ |
2961 |
+ '(--comment-from= -F)'{--comment-from=,-F}'[add comment from file]:file:_files' \ |
2962 |
+ '(--duplicate= -d)'{--duplicate=,-d}'[mark bug as a duplicate of bug number]:bug: ' \ |
2963 |
+ '--fixed[mark bug as RESOLVED, FIXED]' \ |
2964 |
+ '--help[show help message and exit]' \ |
2965 |
+ '--invalid[mark bug as RESOLVED, INVALID]' \ |
2966 |
+ '(--keywords= -k)'{--keywords=,-k}'[list of bugzilla keywords]:keywords: ' \ |
2967 |
+ '--priority=[set the priority field of the bug]:priority: ' \ |
2968 |
+ '(--resolution= -r)'{--resolution=,-r}'[set new resolution (only if status = RESOLVED)]' \ |
2969 |
+ '--remove-cc=[remove an email from the CC list]:email: ' \ |
2970 |
+ '--remove-dependson=[remove a bug from the depends list]:bug: ' \ |
2971 |
+ '--remove-blocked=[remove a bug from the blocked list]:bug: ' \ |
2972 |
+ '(--severity= -S)'{--severity=,-S}'[set severity of the bug]:severity: ' \ |
2973 |
+ '(--status -s=)'{--status=,-s}'[set new status of bug (eg. RESOLVED)]:status: ' \ |
2974 |
+ '(--title= -t)'{--title=,-t}'[set title of the bug]:title: ' \ |
2975 |
+ '(--url= -U)'{--url=,-u}'[set URL field of the bug]:URL: ' \ |
2976 |
+ '(--whiteboard= -w)'{--whiteboard=,-w}'[set status whiteboard]:status whiteboard: ' |
2977 |
+} |
2978 |
+ |
2979 |
+(( ${+functions[_bugz_cmd_namedcmd]} )) || |
2980 |
+_bugz_cmd_namedcmd() |
2981 |
+{ |
2982 |
+ _arguments -s : \ |
2983 |
+ '--show-status[show bug status]' |
2984 |
+ '--show-url[show bug ID as url]' |
2985 |
+} |
2986 |
+ |
2987 |
+(( ${+functions[_bugz_cmd_post]} )) || |
2988 |
+_bugz_cmd_post() |
2989 |
+{ |
2990 |
+ _arguments -s : \ |
2991 |
+ '(--assigned-to= -a)'{--assigned-to=,-a}'[assign bug to someone other than the default assignee]:assignee: ' \ |
2992 |
+ '--batch[work in batch mode, non-interactively]' \ |
2993 |
+ '--blocked[add a list of blocker bugs]:blockers: ' \ |
2994 |
+ '--cc=[add a list of emails to cc list]:email(s): ' \ |
2995 |
+ '--commenter[email of a commenter]:email: ' \ |
2996 |
+ '--depends-on[add a list of bug dependencies]:dependencies: ' \ |
2997 |
+ '(--description= -d)'{--description=,-d}'[description of the bug]:description: ' \ |
2998 |
+ '(--description-from= -F)'{--description-from=,-f}'[description from contents of a file]:file:_files' \ |
2999 |
+ '--help[show help message and exit]' \ |
3000 |
+ '(--keywords= -k)'{--keywords=,-k}'[list of bugzilla keywords]:keywords: ' \ |
3001 |
+ '(--append-command)--no-append-command[do not append command output]' \ |
3002 |
+ '(--title= -t)'{--title=,-t}'[title of your bug]:title: ' \ |
3003 |
+ '(--url= -U)'{--url=,-U}'[URL associated with the bug]:url: ' \ |
3004 |
+ '--priority[priority of this bug]:priority: ' \ |
3005 |
+ '--severity[severity of this bug]:severity: ' |
3006 |
+} |
3007 |
+ |
3008 |
+(( ${+functions[_bugz_cmd_search]} )) || |
3009 |
+_bugz_cmd_search() |
3010 |
+{ |
3011 |
+ # TODO --component,--status,--product,--priority can be specified multiple times |
3012 |
+ _arguments -s : \ |
3013 |
+ '(--assigned-to= -a)'{--assigned-to=,-a}'[the email adress the bug is assigned to]:email: ' \ |
3014 |
+ '--cc=[restrict by CC email address]:email: ' \ |
3015 |
+ '(--comments -c)'{--comments,-c}'[search comments instead of title]:comment: ' \ |
3016 |
+ '(--component= -C)'{--component=,-C}'[restrict by component]:component: ' \ |
3017 |
+ '--help[show help message and exit]' \ |
3018 |
+ '(--keywords= -k)'{--keywords=,-k}'[bug keywords]:keywords: ' \ |
3019 |
+ '--severity=[restrict by severity]:severity: ' \ |
3020 |
+ '--show-status[show bug status]' \ |
3021 |
+ '--show-url[show bug ID as url]' \ |
3022 |
+ '(--status= -s)'{--status=,-s}'[bug status]:status: ' \ |
3023 |
+ '(--order= -o)'{--order=,-o}'[sort by]:order:((number\:"bug number" assignee\:"assignee field" importance\:"importance field" date\:"last changed"))' \ |
3024 |
+ '--priority=[restrict by priority]:priority: ' \ |
3025 |
+ '--product=[restrict by product]:product: ' \ |
3026 |
+ '(--reporter= -r)'{--reporter=,-r}'[email of the reporter]:email: ' \ |
3027 |
+ '(--whiteboard= -w)'{--whiteboard=,-w}'[status whiteboard]:status whiteboard: ' |
3028 |
+} |
3029 |
+ |
3030 |
+_bugz |
3031 |
+ |
3032 |
|
3033 |
diff --git a/third_party/pybugz-0.9.3/man/bugz.1 b/third_party/pybugz-0.9.3/man/bugz.1 |
3034 |
new file mode 100644 |
3035 |
index 0000000..628eae9 |
3036 |
--- /dev/null |
3037 |
+++ b/third_party/pybugz-0.9.3/man/bugz.1 |
3038 |
@@ -0,0 +1,41 @@ |
3039 |
+.\" Hey, Emacs! This is an -*- nroff -*- source file. |
3040 |
+.\" Copyright (c) 2011 William Hubbs |
3041 |
+.\" This is free software; see the GNU General Public Licence version 2 |
3042 |
+.\" or later for copying conditions. There is NO warranty. |
3043 |
+.TH bugz 1 "17 Feb 2011" "0.9.0" |
3044 |
+.nh |
3045 |
+.SH NAME |
3046 |
+bugz \(em command line interface to bugzilla |
3047 |
+.SH SYNOPSIS |
3048 |
+.B bugz |
3049 |
+[ |
3050 |
+.B global options |
3051 |
+] |
3052 |
+.B subcommand |
3053 |
+[ |
3054 |
+.B subcommand options |
3055 |
+] |
3056 |
+.\" .SH OPTIONS |
3057 |
+.\" .TP |
3058 |
+.\" .B \-o value, \-\^\-long=value |
3059 |
+.\" Describe the option. |
3060 |
+.SH DESCRIPTION |
3061 |
+Bugz is a cprogram which gives you access to the features of the |
3062 |
+bugzilla bug tracking system from the command line. |
3063 |
+.PP |
3064 |
+This man page is a stub; the bugs program has extensive built in help. |
3065 |
+.B bugz -h |
3066 |
+will show the help for the global options and |
3067 |
+.B bugz [subcommand] -h |
3068 |
+will show the help for a specific subcommand. |
3069 |
+.SH BUGS |
3070 |
+.PP |
3071 |
+The home page of this project is http://www.github.com/williamh/pybugz. |
3072 |
+Bugs should be reported to the bug tracker there. |
3073 |
+.\" .SH SEE ALSO |
3074 |
+.\" .PP |
3075 |
+.SH AUTHOR |
3076 |
+.PP |
3077 |
+The original author is Alastair Tse <alastair@×××××××.net>. |
3078 |
+The current maintainer is William Hubbs <w.d.hubbs@×××××.com>. William |
3079 |
+also wrote this man page. |
3080 |
|
3081 |
diff --git a/third_party/pybugz-0.9.3/setup.py b/third_party/pybugz-0.9.3/setup.py |
3082 |
new file mode 100644 |
3083 |
index 0000000..9a51e44 |
3084 |
--- /dev/null |
3085 |
+++ b/third_party/pybugz-0.9.3/setup.py |
3086 |
@@ -0,0 +1,15 @@ |
3087 |
+from bugz import __version__ |
3088 |
+from distutils.core import setup |
3089 |
+ |
3090 |
+setup( |
3091 |
+ name = 'pybugz', |
3092 |
+ version = __version__, |
3093 |
+ description = 'python interface to bugzilla', |
3094 |
+ author = 'Alastair Tse', |
3095 |
+ author_email = 'alastair@×××××××.net', |
3096 |
+ url = 'http://www.liquidx.net/pybuggz', |
3097 |
+ license = "GPL-2", |
3098 |
+ platforms = ['any'], |
3099 |
+ packages = ['bugz'], |
3100 |
+ scripts = ['bin/bugz'], |
3101 |
+) |