Gentoo Archives: gentoo-commits

From: "Paweł Hajdan" <phajdan.jr@g.o>
To: gentoo-commits@l.g.o
Subject: [gentoo-commits] proj/arch-tools:new-pybugz commit in: third_party/pybugz-0.9.3/man/, /, third_party/pybugz-0.9.3/contrib/, ...
Date: Wed, 30 May 2012 14:35:27
Message-Id: 1338388480.520c9782541d2e3fa509b1a2d470889a6d26bef7.phajdan.jr@gentoo
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 +)