summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPawel Hajdan, Jr <phajdan.jr@gentoo.org>2012-05-30 16:34:40 +0200
committerPawel Hajdan, Jr <phajdan.jr@gentoo.org>2012-05-30 16:34:40 +0200
commit520c9782541d2e3fa509b1a2d470889a6d26bef7 (patch)
tree61ee7afa1b00bdc49d59e65a500ff5229f68b0c7
parentProcess stabilization candidates incrementally, (diff)
downloadarch-tools-520c9782541d2e3fa509b1a2d470889a6d26bef7.tar.gz
arch-tools-520c9782541d2e3fa509b1a2d470889a6d26bef7.tar.bz2
arch-tools-520c9782541d2e3fa509b1a2d470889a6d26bef7.zip
Make bugzilla-viewer and maintainer-timeout work
by bundling old pybugz.
-rwxr-xr-xbugzilla-viewer.py2
-rwxr-xr-xmaintainer-timeout.py4
-rwxr-xr-xstabilization-candidates.py35
-rw-r--r--third_party/pybugz-0.9.3/LICENSE340
-rw-r--r--third_party/pybugz-0.9.3/README107
-rwxr-xr-xthird_party/pybugz-0.9.3/bin/bugz393
-rw-r--r--third_party/pybugz-0.9.3/bugz/__init__.py31
-rw-r--r--third_party/pybugz-0.9.3/bugz/bugzilla.py862
-rw-r--r--third_party/pybugz-0.9.3/bugz/cli.py607
-rw-r--r--third_party/pybugz-0.9.3/bugz/config.py229
-rw-r--r--third_party/pybugz-0.9.3/bugzrc.example25
-rw-r--r--third_party/pybugz-0.9.3/contrib/bash-completion66
-rw-r--r--third_party/pybugz-0.9.3/contrib/zsh-completion158
-rw-r--r--third_party/pybugz-0.9.3/man/bugz.141
-rw-r--r--third_party/pybugz-0.9.3/setup.py15
15 files changed, 2901 insertions, 14 deletions
diff --git a/bugzilla-viewer.py b/bugzilla-viewer.py
index 8a1e131..76daabf 100755
--- a/bugzilla-viewer.py
+++ b/bugzilla-viewer.py
@@ -12,6 +12,8 @@ import sys
import textwrap
import xml.etree
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'third_party', 'pybugz-0.9.3'))
+
import bugz.bugzilla
import portage.versions
diff --git a/maintainer-timeout.py b/maintainer-timeout.py
index c825f5d..6287bec 100755
--- a/maintainer-timeout.py
+++ b/maintainer-timeout.py
@@ -4,6 +4,10 @@
import datetime
import optparse
+import os.path
+import sys
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'third_party', 'pybugz-0.9.3'))
import bugz.bugzilla
import portage.versions
diff --git a/stabilization-candidates.py b/stabilization-candidates.py
index 04b6dee..7989a84 100755
--- a/stabilization-candidates.py
+++ b/stabilization-candidates.py
@@ -51,7 +51,7 @@ if __name__ == "__main__":
best_stable = portage.versions.best(portage.portdb.match(cp))
if not best_stable:
continue
- print 'Working on %s...' % cp
+ print 'Working on %s...' % cp,
candidates = []
for cpv in portage.portdb.cp_list(cp):
# Only consider higher versions than best stable.
@@ -79,6 +79,7 @@ if __name__ == "__main__":
candidates.append(cpv)
if not candidates:
+ print 'no candidates'
continue
candidates.sort(key=portage.versions.cpv_sort_key())
@@ -94,9 +95,11 @@ if __name__ == "__main__":
regex = '\*%s \((.*)\)' % re.escape(pv)
match = re.search(regex, changelog_file.read())
if not match:
+ print 'error parsing ChangeLog'
continue
changelog_date = datetime.datetime.strptime(match.group(1), '%d %b %Y')
if now - changelog_date < datetime.timedelta(days=options.days):
+ print 'not old enough'
continue
keywords = portage.db["/"]["porttree"].dbapi.aux_get(best_candidate, ['KEYWORDS'])[0]
@@ -106,6 +109,22 @@ if __name__ == "__main__":
missing_arch = True
break
if missing_arch:
+ print 'not keyworded ~arch'
+ continue
+
+ # Do not risk trying to stabilize a package with known bugs.
+ params = {}
+ params['summary'] = [cp];
+ bugs = bugzilla.Bug.search(params)
+ if len(bugs['bugs']):
+ print 'has bugs'
+ continue
+
+ # Protection against filing a stabilization bug twice.
+ params['summary'] = [best_candidate]
+ bugs = bugzilla.Bug.search(params)
+ if len(bugs['bugs']):
+ print 'version has closed bugs'
continue
cvs_path = os.path.join(options.repo, cp)
@@ -124,6 +143,7 @@ if __name__ == "__main__":
subprocess.check_output(["repoman", "manifest"], cwd=cvs_path)
subprocess.check_output(["repoman", "full"], cwd=cvs_path)
except subprocess.CalledProcessError:
+ print 'repoman error'
continue
finally:
f = open(ebuild_path, "w")
@@ -133,19 +153,6 @@ if __name__ == "__main__":
f.write(manifest_contents)
f.close()
- # Do not risk trying to stabilize a package with known bugs.
- params = {}
- params['summary'] = [cp];
- bugs = bugzilla.Bug.search(params)
- if len(bugs['bugs']):
- continue
-
- # Protection against filing a stabilization bug twice.
- params['summary'] = [best_candidate]
- bugs = bugzilla.Bug.search(params)
- if len(bugs['bugs']):
- continue
-
metadata = MetaDataXML(os.path.join(cvs_path, 'metadata.xml'), '/usr/portage/metadata/herds.xml')
maintainer_split = metadata.format_maintainer_string().split(' ', 1)
maintainer = maintainer_split[0]
diff --git a/third_party/pybugz-0.9.3/LICENSE b/third_party/pybugz-0.9.3/LICENSE
new file mode 100644
index 0000000..3912109
--- /dev/null
+++ b/third_party/pybugz-0.9.3/LICENSE
@@ -0,0 +1,340 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/third_party/pybugz-0.9.3/README b/third_party/pybugz-0.9.3/README
new file mode 100644
index 0000000..423566d
--- /dev/null
+++ b/third_party/pybugz-0.9.3/README
@@ -0,0 +1,107 @@
+PyBugz - Python Bugzilla Interface
+----------------------------------
+
+Bugzilla has a very inefficient user interface, so I've written a
+command line utility to interact with it. This is mainly done to help
+me with closing bugs on Gentoo Bugzilla by grabbing patches, ebuilds
+and so on.
+
+Author
+------
+Alastair Tse <alastair@liquidx.net>. Copyright (c) 2006 under GPL-2.
+
+Features
+--------
+* Searching bugzilla
+* Listing details of a bug including comments and attachments
+* Downloading/viewing attachments from bugzilla
+* Posting bugs, comments, and making changes to an existing bug.
+* Adding attachments to a bug.
+
+Configuration File
+------------------
+
+pybugz supports a configuration file which allows you to define settings
+for multiple bugzilla connections then refer to them by name from the
+command line. The default is for this file to be named .bugzrc and
+stored in your home directory. An example of this file and the settings
+is included in this distribution as bugzrc.example.
+
+Usage/Workflow
+--------------
+
+PyBugz comes with a command line interface called "bugz". It's
+operation is similar in style to cvs/svn where a subcommand is
+required for operation.
+
+To explain how it works, I will use a typical workflow for Gentoo
+development.
+
+1) Searching bugzilla for bugs I can fix, I'll run the command:
+---------------------------------------------------------------
+
+$ bugz search "version bump" --assigned liquidx@gentoo.org
+
+ * Using http://bugs.gentoo.org/ ..
+ * Searching for "version bump" ordered by "number"
+ 101968 liquidx net-im/msnlib version bump
+ 125468 liquidx version bump for dev-libs/g-wrap-1.9.6
+ 130608 liquidx app-dicts/stardict version bump: 2.4.7
+
+2) Narrow down on bug #101968, I can execute:
+---------------------------------------------
+
+$ bugz get 101968
+
+ * Using http://bugs.gentoo.org/ ..
+ * Getting bug 130608 ..
+Title : app-dicts/stardict version bump: 2.4.7
+Assignee : liquidx@gentoo.org
+Reported : 2006-04-20 07:36 PST
+Updated : 2006-05-29 23:18:12 PST
+Status : NEW
+URL : http://stardict.sf.net
+Severity : enhancement
+Reporter : dushistov@mail.ru
+Priority : P2
+Comments : 3
+Attachments : 1
+
+[ATTACH] [87844] [stardict 2.4.7 ebuild]
+
+[Comment #1] dushistov@----.ru : 2006-04-20 07:36 PST
+...
+
+3) Now this bug has an attachment submitted by the user, so I can
+ easily pull that attachment in:
+-----------------------------------------------------------------
+
+$ bugz attachment 87844
+
+ * Using http://bugs.gentoo.org/ ..
+ * Getting attachment 87844
+ * Saving attachment: "stardict-2.4.7.ebuild"
+
+4) If the ebuild is suitable, we can commit it using our normal
+ repoman tools, and close the bug.
+---------------------------------------------------------------
+
+$ bugz modify 130608 --fixed -c "Thanks for the ebuild. Committed to
+ portage"
+
+or if we find that the bug is invalid, we can close it by using:
+
+$ bugz modify 130608 --invalid -c "Not reproducable"
+
+Other options
+-------------
+
+There is extensive help in `bugz --help` and `bugz <subcommand>
+--help` for additional options.
+
+bugz.py can be easily adapted for other bugzillas by changing
+BugzConfig to match the configuration of your target
+bugzilla. However, I haven't spent much time on using it with other
+bugzillas out there. If you do have changes that will make it easier,
+please let me know.
+
diff --git a/third_party/pybugz-0.9.3/bin/bugz b/third_party/pybugz-0.9.3/bin/bugz
new file mode 100755
index 0000000..9d29bdd
--- /dev/null
+++ b/third_party/pybugz-0.9.3/bin/bugz
@@ -0,0 +1,393 @@
+#!/usr/bin/python
+
+import argparse
+import ConfigParser
+import locale
+import os
+import sys
+import traceback
+
+from bugz import __version__
+from bugz.cli import BugzError, PrettyBugz
+from bugz.config import config
+
+def make_attach_parser(subparsers):
+ attach_parser = subparsers.add_parser('attach',
+ help = 'attach file to a bug')
+ attach_parser.add_argument('bugid',
+ help = 'the ID of the bug where the file should be attached')
+ attach_parser.add_argument('filename',
+ help = 'the name of the file to attach')
+ attach_parser.add_argument('-c', '--content-type',
+ default='text/plain',
+ help = 'mimetype of the file (default: text/plain)')
+ attach_parser.add_argument('-d', '--description',
+ help = 'a description of the attachment.')
+ attach_parser.add_argument('-p', '--patch',
+ action='store_true',
+ help = 'attachment is a patch')
+ attach_parser.set_defaults(func = PrettyBugz.attach)
+
+def make_attachment_parser(subparsers):
+ attachment_parser = subparsers.add_parser('attachment',
+ help = 'get an attachment from bugzilla')
+ attachment_parser.add_argument('attachid',
+ help = 'the ID of the attachment')
+ attachment_parser.add_argument('-v', '--view',
+ action="store_true",
+ default = False,
+ help = 'print attachment rather than save')
+ attachment_parser.set_defaults(func = PrettyBugz.attachment)
+
+def make_get_parser(subparsers):
+ get_parser = subparsers.add_parser('get',
+ help = 'get a bug from bugzilla')
+ get_parser.add_argument('bugid',
+ help = 'the ID of the bug to retrieve.')
+ get_parser.add_argument("-a", "--no-attachments",
+ action="store_false",
+ default = True,
+ help = 'do not show attachments',
+ dest = 'attachments')
+ get_parser.add_argument("-n", "--no-comments",
+ action="store_false",
+ default = True,
+ help = 'do not show comments',
+ dest = 'comments')
+ get_parser.set_defaults(func = PrettyBugz.get)
+
+def make_modify_parser(subparsers):
+ modify_parser = subparsers.add_parser('modify',
+ help = 'modify a bug (eg. post a comment)')
+ modify_parser.add_argument('bugid',
+ help = 'the ID of the bug to modify')
+ modify_parser.add_argument('-a', '--assigned-to',
+ help = 'change assignee for this bug')
+ modify_parser.add_argument('-C', '--comment-editor',
+ action='store_true',
+ help = 'add comment via default editor')
+ modify_parser.add_argument('-F', '--comment-from',
+ help = 'add comment from file. If -C is also specified, the editor will be opened with this file as its contents.')
+ modify_parser.add_argument('-c', '--comment',
+ help = 'add comment from command line')
+ modify_parser.add_argument('-d', '--duplicate',
+ type = int,
+ default = 0,
+ help = 'this bug is a duplicate')
+ modify_parser.add_argument('-k', '--keywords',
+ help = 'set bug keywords'),
+ modify_parser.add_argument('--priority',
+ choices=config.choices['priority'].values(),
+ help = 'change the priority for this bug')
+ modify_parser.add_argument('-r', '--resolution',
+ choices=config.choices['resolution'].values(),
+ help = 'set new resolution (only if status = RESOLVED)')
+ modify_parser.add_argument('-s', '--status',
+ choices=config.choices['status'].values(),
+ help = 'set new status of bug (eg. RESOLVED)')
+ modify_parser.add_argument('-S', '--severity',
+ choices=config.choices['severity'],
+ help = 'set severity for this bug')
+ modify_parser.add_argument('-t', '--title',
+ help = 'set title of bug')
+ modify_parser.add_argument('-U', '--url',
+ help = 'set URL field of bug')
+ modify_parser.add_argument('-w', '--whiteboard',
+ help = 'set Status whiteboard'),
+ modify_parser.add_argument('--add-cc',
+ action = 'append',
+ help = 'add an email to the CC list')
+ modify_parser.add_argument('--remove-cc',
+ action = 'append',
+ help = 'remove an email from the CC list')
+ modify_parser.add_argument('--add-dependson',
+ action = 'append',
+ help = 'add a bug to the depends list')
+ modify_parser.add_argument('--remove-dependson',
+ action = 'append',
+ help = 'remove a bug from the depends list')
+ modify_parser.add_argument('--add-blocked',
+ action = 'append',
+ help = 'add a bug to the blocked list')
+ modify_parser.add_argument('--remove-blocked',
+ action = 'append',
+ help = 'remove a bug from the blocked list')
+ modify_parser.add_argument('--component',
+ help = 'change the component for this bug')
+ modify_parser.add_argument('--fixed',
+ action='store_true',
+ help = 'mark bug as RESOLVED, FIXED')
+ modify_parser.add_argument('--invalid',
+ action='store_true',
+ help = 'mark bug as RESOLVED, INVALID')
+ modify_parser.set_defaults(func = PrettyBugz.modify)
+
+def make_namedcmd_parser(subparsers):
+ namedcmd_parser = subparsers.add_parser('namedcmd',
+ help = 'run a stored search')
+ namedcmd_parser.add_argument('command',
+ help = 'the name of the stored search')
+ namedcmd_parser.add_argument('--show-status',
+ action = 'store_true',
+ help = 'show status of bugs')
+ namedcmd_parser.add_argument('--show-url',
+ action = 'store_true',
+ help = 'show bug id as a url')
+ namedcmd_parser.set_defaults(func = PrettyBugz.namedcmd)
+
+def make_post_parser(subparsers):
+ post_parser = subparsers.add_parser('post',
+ help = 'post a new bug into bugzilla')
+ post_parser.add_argument('--product',
+ help = 'product')
+ post_parser.add_argument('--component',
+ help = 'component')
+ post_parser.add_argument('--prodversion',
+ help = 'version of the product')
+ post_parser.add_argument('-t', '--title',
+ help = 'title of bug')
+ post_parser.add_argument('-d', '--description',
+ help = 'description of the bug')
+ post_parser.add_argument('-F' , '--description-from',
+ help = 'description from contents of file')
+ post_parser.add_argument('--append-command',
+ help = 'append the output of a command to the description')
+ post_parser.add_argument('-a', '--assigned-to',
+ help = 'assign bug to someone other than the default assignee')
+ post_parser.add_argument('--cc',
+ help = 'add a list of emails to CC list')
+ post_parser.add_argument('-U', '--url',
+ help = 'URL associated with the bug')
+ post_parser.add_argument('--depends-on',
+ help = 'add a list of bug dependencies',
+ dest='dependson')
+ post_parser.add_argument('--blocked',
+ help = 'add a list of blocker bugs')
+ post_parser.add_argument('-k', '--keywords',
+ help = 'list of bugzilla keywords')
+ post_parser.add_argument('--batch',
+ action="store_true",
+ help = 'do not prompt for any values')
+ post_parser.add_argument('--default-confirm',
+ choices = ['y','Y','n','N'],
+ default = 'y',
+ help = 'default answer to confirmation question')
+ post_parser.add_argument('--priority',
+ choices=config.choices['priority'].values(),
+ help = 'set priority for the new bug')
+ post_parser.add_argument('-S', '--severity',
+ choices=config.choices['severity'],
+ help = 'set the severity for the new bug')
+ post_parser.set_defaults(func = PrettyBugz.post)
+
+def make_search_parser(subparsers):
+ search_parser = subparsers.add_parser('search',
+ help = 'search for bugs in bugzilla')
+ search_parser.add_argument('terms',
+ nargs='*',
+ help = 'strings to search for in title or body')
+ search_parser.add_argument('-o', '--order',
+ choices = config.choices['order'].keys(),
+ default = 'number',
+ help = 'display bugs in this order')
+ search_parser.add_argument('-a', '--assigned-to',
+ help = 'email the bug is assigned to')
+ search_parser.add_argument('-r', '--reporter',
+ help = 'email the bug was reported by')
+ search_parser.add_argument('--cc',
+ help = 'restrict by CC email address')
+ search_parser.add_argument('--commenter',
+ help = 'email that commented the bug')
+ search_parser.add_argument('-s', '--status',
+ action='append',
+ help = 'restrict by status (one or more, use all for all statuses)')
+ search_parser.add_argument('--severity',
+ action='append',
+ choices = config.choices['severity'],
+ help = 'restrict by severity (one or more)')
+ search_parser.add_argument('--priority',
+ action='append',
+ choices = config.choices['priority'].values(),
+ help = 'restrict by priority (one or more)')
+ search_parser.add_argument('-c', '--comments',
+ action='store_true',
+ default=None,
+ help = 'search comments instead of title')
+ search_parser.add_argument('--product',
+ action='append',
+ help = 'restrict by product (one or more)')
+ search_parser.add_argument('-C', '--component',
+ action='append',
+ help = 'restrict by component (1 or more)')
+ search_parser.add_argument('-k', '--keywords',
+ help = 'restrict by keywords')
+ search_parser.add_argument('-w', '--whiteboard',
+ help = 'status whiteboard')
+ search_parser.add_argument('--show-status',
+ action = 'store_true',
+ help='show status of bugs')
+ search_parser.add_argument('--show-url',
+ action = 'store_true',
+ help='show bug id as a url.')
+ search_parser.set_defaults(func = PrettyBugz.search)
+
+def make_parser():
+ parser = argparse.ArgumentParser(
+ epilog = 'use -h after a sub-command for sub-command specific help')
+ parser.add_argument('--config-file',
+ help = 'read an alternate configuration file')
+ parser.add_argument('--connection',
+ help = 'use [connection] section of your configuration file')
+ parser.add_argument('-b', '--base',
+ help = 'base URL of Bugzilla')
+ parser.add_argument('-u', '--user',
+ help = 'username for commands requiring authentication')
+ parser.add_argument('-p', '--password',
+ help = 'password for commands requiring authentication')
+ parser.add_argument('-H', '--httpuser',
+ help = 'username for basic http auth')
+ parser.add_argument('-P', '--httppassword',
+ help = 'password for basic http auth')
+ parser.add_argument('-f', '--forget',
+ action='store_true',
+ help = 'forget login after execution')
+ parser.add_argument('-q', '--quiet',
+ action='store_true',
+ help = 'quiet mode')
+ parser.add_argument('--columns',
+ type = int,
+ help = 'maximum number of columns output should use')
+ parser.add_argument('--encoding',
+ help = 'output encoding (default: utf-8).')
+ parser.add_argument('--skip-auth',
+ action='store_true',
+ help = 'skip Authentication.')
+ parser.add_argument('--version',
+ action='version',
+ help='show program version and exit',
+ version='%(prog)s ' + __version__)
+ subparsers = parser.add_subparsers(help = 'help for sub-commands')
+ make_attach_parser(subparsers)
+ make_attachment_parser(subparsers)
+ make_get_parser(subparsers)
+ make_modify_parser(subparsers)
+ make_namedcmd_parser(subparsers)
+ make_post_parser(subparsers)
+ make_search_parser(subparsers)
+ return parser
+
+def config_option(parser, get, section, option):
+ if parser.has_option(section, option):
+ try:
+ if get(section, option) != '':
+ return get(section, option)
+ else:
+ print " ! Error: "+option+" is not set"
+ sys.exit(1)
+ except ValueError as e:
+ print " ! Error: option "+option+" is not in the right format: "+str(e)
+ sys.exit(1)
+
+def get_config(args, bugz):
+ config_file = getattr(args, 'config_file')
+ if config_file is None:
+ config_file = '~/.bugzrc'
+ section = getattr(args, 'connection')
+ parser = ConfigParser.ConfigParser()
+ config_file_name = os.path.expanduser(config_file)
+
+ # try to open config file
+ try:
+ file = open(config_file_name)
+ except IOError:
+ if getattr(args, 'config_file') is not None:
+ print " ! Error: Can't find user configuration file: "+config_file_name
+ sys.exit(1)
+ else:
+ return bugz
+
+ # try to parse config file
+ try:
+ parser.readfp(file)
+ sections = parser.sections()
+ except ConfigParser.ParsingError as e:
+ print " ! Error: Can't parse user configuration file: "+str(e)
+ sys.exit(1)
+
+ # parse a specific section
+ if section in sections:
+ bugz['base'] = config_option(parser, parser.get, section, "base")
+ bugz['user'] = config_option(parser, parser.get, section, "user")
+ bugz['password'] = config_option(parser, parser.get, section, "password")
+ bugz['httpuser'] = config_option(parser, parser.get, section, "httpuser")
+ bugz['httppassword'] = config_option(parser, parser.get, section,
+ "httppassword")
+ bugz['forget'] = config_option(parser, parser.getboolean, section,
+ "forget")
+ bugz['columns'] = config_option(parser, parser.getint, section,
+ "columns")
+ bugz['encoding'] = config_option(parser, parser.get, section,
+ "encoding")
+ bugz['quiet'] = config_option(parser, parser.getboolean, section,
+ "quiet")
+ elif section is not None:
+ print " ! Error: Can't find section ["+section+"] in configuration file"
+ sys.exit(1)
+
+ return bugz
+
+def get_kwds(args, bugz, cmd):
+ global_attrs = ['user', 'password', 'httpuser', 'httppassword', 'forget',
+ 'base', 'columns', 'encoding', 'quiet', 'skip_auth']
+ skip_attrs = ['config_file', 'connection', 'func']
+ for attr in dir(args):
+ if attr[0] == '_' or attr in skip_attrs:
+ continue
+ elif attr in global_attrs:
+ if attr not in bugz or getattr(args,attr):
+ bugz[attr] = getattr(args,attr)
+ else:
+ cmd[attr] = getattr(args,attr)
+
+def main():
+ parser = make_parser()
+
+ # parse options
+ args = parser.parse_args()
+ bugz_kwds = {}
+ get_config(args, bugz_kwds)
+ cmd_kwds = {}
+ get_kwds(args, bugz_kwds, cmd_kwds)
+ if bugz_kwds['base'] is None:
+ bugz_kwds['base'] = 'https://bugs.gentoo.org'
+ if bugz_kwds['columns'] is None:
+ bugz_kwds['columns'] = 0
+
+ try:
+ bugz = PrettyBugz(**bugz_kwds)
+ args.func(bugz, **cmd_kwds)
+
+ except BugzError, e:
+ print ' ! Error: %s' % e
+ sys.exit(-1)
+
+ except TypeError, e:
+ print ' ! Error: Incorrect number of arguments supplied'
+ print
+ traceback.print_exc()
+ sys.exit(-1)
+
+ except RuntimeError, e:
+ print ' ! Error: %s' % e
+ sys.exit(-1)
+
+ except KeyboardInterrupt:
+ print
+ print 'Stopped.'
+ sys.exit(-1)
+
+ except:
+ raise
+
+if __name__ == "__main__":
+ main()
diff --git a/third_party/pybugz-0.9.3/bugz/__init__.py b/third_party/pybugz-0.9.3/bugz/__init__.py
new file mode 100644
index 0000000..f5a11a4
--- /dev/null
+++ b/third_party/pybugz-0.9.3/bugz/__init__.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+
+"""
+Python Bugzilla Interface
+
+Simple command-line interface to bugzilla to allow:
+ - searching
+ - getting bug info
+ - saving attachments
+
+Requirements
+------------
+ - Python 2.5 or later
+
+Classes
+-------
+ - Bugz - Pythonic interface to Bugzilla
+ - PrettyBugz - Command line interface to Bugzilla
+
+"""
+
+__version__ = '0.9.3'
+__author__ = 'Alastair Tse <http://www.liquidx.net/>'
+__contributors__ = ['Santiago M. Mola <cooldwind@gmail.com',
+ 'William Hubbs <w.d.hubbs@gmail.com']
+__revision__ = '$Id: $'
+__license__ = """Copyright (c) 2006, Alastair Tse, All rights reserved.
+This following source code is licensed under the GPL v2 License."""
+
+CONFIG_FILE = '.bugz'
+
diff --git a/third_party/pybugz-0.9.3/bugz/bugzilla.py b/third_party/pybugz-0.9.3/bugz/bugzilla.py
new file mode 100644
index 0000000..957598e
--- /dev/null
+++ b/third_party/pybugz-0.9.3/bugz/bugzilla.py
@@ -0,0 +1,862 @@
+#!/usr/bin/env python
+
+import base64
+import csv
+import getpass
+import locale
+import mimetypes
+import os
+import re
+import sys
+
+from cookielib import LWPCookieJar, CookieJar
+from cStringIO import StringIO
+from urlparse import urlsplit, urljoin
+from urllib import urlencode, quote
+from urllib2 import build_opener, HTTPCookieProcessor, Request
+
+from config import config
+
+from xml.etree import ElementTree
+
+COOKIE_FILE = '.bugz_cookie'
+
+#
+# Return a string truncated to the given length if it is longer.
+#
+
+def ellipsis(text, length):
+ if len(text) > length:
+ return text[:length-4] + "..."
+ else:
+ return text
+
+#
+# HTTP file uploads in Python
+# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306
+#
+
+def post_multipart(host, selector, fields, files):
+ """
+ Post fields and files to an http host as multipart/form-data.
+ fields is a sequence of (name, value) elements for regular form fields.
+ files is a sequence of (name, filename, value) elements for data to be uploaded as files
+ Return the server's response page.
+ """
+ content_type, body = encode_multipart_formdata(fields, files)
+ h = httplib.HTTP(host)
+ h.putrequest('POST', selector)
+ h.putheader('content-type', content_type)
+ h.putheader('content-length', str(len(body)))
+ h.endheaders()
+ h.send(body)
+ errcode, errmsg, headers = h.getreply()
+ return h.file.read()
+
+def encode_multipart_formdata(fields, files):
+ """
+ fields is a sequence of (name, value) elements for regular form fields.
+ files is a sequence of (name, filename, value) elements for data to be uploaded as files
+ Return (content_type, body) ready for httplib.HTTP instance
+ """
+ BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$'
+ CRLF = '\r\n'
+ L = []
+ for (key, value) in fields:
+ L.append('--' + BOUNDARY)
+ L.append('Content-Disposition: form-data; name="%s"' % key)
+ L.append('')
+ L.append(value)
+ for (key, filename, value) in files:
+ L.append('--' + BOUNDARY)
+ L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename))
+ L.append('Content-Type: %s' % get_content_type(filename))
+ L.append('')
+ L.append(value)
+ L.append('--' + BOUNDARY + '--')
+ L.append('')
+ body = CRLF.join(L)
+ content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
+ return content_type, body
+
+def get_content_type(filename):
+ return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
+
+#
+# Override the behaviour of elementtree and allow us to
+# force the encoding to utf-8
+# Not needed in Python 2.7, since ElementTree.XMLTreeBuilder uses the forced
+# encoding.
+#
+
+class ForcedEncodingXMLTreeBuilder(ElementTree.XMLTreeBuilder):
+ def __init__(self, html = 0, target = None, encoding = None):
+ try:
+ from xml.parsers import expat
+ except ImportError:
+ raise ImportError(
+ "No module named expat; use SimpleXMLTreeBuilder instead"
+ )
+ self._parser = parser = expat.ParserCreate(encoding, "}")
+ if target is None:
+ target = ElementTree.TreeBuilder()
+ self._target = target
+ self._names = {} # name memo cache
+ # callbacks
+ parser.DefaultHandlerExpand = self._default
+ parser.StartElementHandler = self._start
+ parser.EndElementHandler = self._end
+ parser.CharacterDataHandler = self._data
+ # let expat do the buffering, if supported
+ try:
+ self._parser.buffer_text = 1
+ except AttributeError:
+ pass
+ # use new-style attribute handling, if supported
+ try:
+ self._parser.ordered_attributes = 1
+ self._parser.specified_attributes = 1
+ parser.StartElementHandler = self._start_list
+ except AttributeError:
+ pass
+ encoding = None
+ if not parser.returns_unicode:
+ encoding = "utf-8"
+ # target.xml(encoding, None)
+ self._doctype = None
+ self.entity = {}
+
+#
+# Real bugzilla interface
+#
+
+class Bugz:
+ """ Converts sane method calls to Bugzilla HTTP requests.
+
+ @ivar base: base url of bugzilla.
+ @ivar user: username for authenticated operations.
+ @ivar password: password for authenticated operations
+ @ivar cookiejar: for authenticated sessions so we only auth once.
+ @ivar forget: forget user/password after session.
+ @ivar authenticated: is this session authenticated already
+ """
+
+ def __init__(self, base, user = None, password = None, forget = False,
+ skip_auth = False, httpuser = None, httppassword = None ):
+ """
+ {user} and {password} will be prompted if an action needs them
+ and they are not supplied.
+
+ if {forget} is set, the login cookie will be destroyed on quit.
+
+ @param base: base url of the bugzilla
+ @type base: string
+ @keyword user: username for authenticated actions.
+ @type user: string
+ @keyword password: password for authenticated actions.
+ @type password: string
+ @keyword forget: forget login session after termination.
+ @type forget: bool
+ @keyword skip_auth: do not authenticate
+ @type skip_auth: bool
+ """
+ self.base = base
+ scheme, self.host, self.path, query, frag = urlsplit(self.base)
+ self.authenticated = False
+ self.forget = forget
+
+ if not self.forget:
+ try:
+ cookie_file = os.path.join(os.environ['HOME'], COOKIE_FILE)
+ self.cookiejar = LWPCookieJar(cookie_file)
+ if forget:
+ try:
+ self.cookiejar.load()
+ self.cookiejar.clear()
+ self.cookiejar.save()
+ os.chmod(self.cookiejar.filename, 0600)
+ except IOError:
+ pass
+ except KeyError:
+ self.warn('Unable to save session cookies in %s' % cookie_file)
+ self.cookiejar = CookieJar(cookie_file)
+ else:
+ self.cookiejar = CookieJar()
+
+ self.opener = build_opener(HTTPCookieProcessor(self.cookiejar))
+ self.user = user
+ self.password = password
+ self.httpuser = httpuser
+ self.httppassword = httppassword
+ self.skip_auth = skip_auth
+
+ def log(self, status_msg):
+ """Default logging handler. Expected to be overridden by
+ the UI implementing subclass.
+
+ @param status_msg: status message to print
+ @type status_msg: string
+ """
+ return
+
+ def warn(self, warn_msg):
+ """Default logging handler. Expected to be overridden by
+ the UI implementing subclass.
+
+ @param status_msg: status message to print
+ @type status_msg: string
+ """
+ return
+
+ def get_input(self, prompt):
+ """Default input handler. Expected to be override by the
+ UI implementing subclass.
+
+ @param prompt: Prompt message
+ @type prompt: string
+ """
+ return ''
+
+ def auth(self):
+ """Authenticate a session.
+ """
+ # check if we need to authenticate
+ if self.authenticated:
+ return
+
+ # try seeing if we really need to request login
+ if not self.forget:
+ try:
+ self.cookiejar.load()
+ except IOError:
+ pass
+
+ req_url = urljoin(self.base, config.urls['auth'])
+ req_url += '?GoAheadAndLogIn=1'
+ req = Request(req_url, None, config.headers)
+ if self.httpuser and self.httppassword:
+ base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1]
+ req.add_header("Authorization", "Basic %s" % base64string)
+ resp = self.opener.open(req)
+ re_request_login = re.compile(r'<title>.*Log in to .*</title>')
+ if not re_request_login.search(resp.read()):
+ self.log('Already logged in.')
+ self.authenticated = True
+ return
+
+ # prompt for username if we were not supplied with it
+ if not self.user:
+ self.log('No username given.')
+ self.user = self.get_input('Username: ')
+
+ # prompt for password if we were not supplied with it
+ if not self.password:
+ self.log('No password given.')
+ self.password = getpass.getpass()
+
+ # perform login
+ qparams = config.params['auth'].copy()
+ qparams['Bugzilla_login'] = self.user
+ qparams['Bugzilla_password'] = self.password
+ if not self.forget:
+ qparams['Bugzilla_remember'] = 'on'
+
+ req_url = urljoin(self.base, config.urls['auth'])
+ req = Request(req_url, urlencode(qparams), config.headers)
+ if self.httpuser and self.httppassword:
+ base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1]
+ req.add_header("Authorization", "Basic %s" % base64string)
+ resp = self.opener.open(req)
+ if resp.info().has_key('Set-Cookie'):
+ self.authenticated = True
+ if not self.forget:
+ self.cookiejar.save()
+ os.chmod(self.cookiejar.filename, 0600)
+ return True
+ else:
+ raise RuntimeError("Failed to login")
+
+ def extractResults(self, resp):
+ # parse the results into dicts.
+ results = []
+ columns = []
+ rows = []
+
+ for r in csv.reader(resp): rows.append(r)
+ for field in rows[0]:
+ if config.choices['column_alias'].has_key(field):
+ columns.append(config.choices['column_alias'][field])
+ else:
+ self.log('Unknown field: ' + field)
+ columns.append(field)
+ for row in rows[1:]:
+ if "Missing Search" in row[0]:
+ self.log('Bugzilla error (Missing search found)')
+ return None
+ fields = {}
+ for i in range(min(len(row), len(columns))):
+ fields[columns[i]] = row[i]
+ results.append(fields)
+ return results
+
+ def search(self, query, comments = False, order = 'number',
+ assigned_to = None, reporter = None, cc = None,
+ commenter = None, whiteboard = None, keywords = None,
+ status = [], severity = [], priority = [], product = [],
+ component = []):
+ """Search bugzilla for a bug.
+
+ @param query: query string to search in title or {comments}.
+ @type query: string
+ @param order: what order to returns bugs in.
+ @type order: string
+
+ @keyword assigned_to: email address which the bug is assigned to.
+ @type assigned_to: string
+ @keyword reporter: email address matching the bug reporter.
+ @type reporter: string
+ @keyword cc: email that is contained in the CC list
+ @type cc: string
+ @keyword commenter: email of a commenter.
+ @type commenter: string
+
+ @keyword whiteboard: string to search in status whiteboard (gentoo?)
+ @type whiteboard: string
+ @keyword keywords: keyword to search for
+ @type keywords: string
+
+ @keyword status: bug status to match. default is ['NEW', 'ASSIGNED',
+ 'REOPENED'].
+ @type status: list
+ @keyword severity: severity to match, empty means all.
+ @type severity: list
+ @keyword priority: priority levels to patch, empty means all.
+ @type priority: list
+ @keyword comments: search comments instead of just bug title.
+ @type comments: bool
+ @keyword product: search within products. empty means all.
+ @type product: list
+ @keyword component: search within components. empty means all.
+ @type component: list
+
+ @return: list of bugs, each bug represented as a dict
+ @rtype: list of dicts
+ """
+
+ if not self.authenticated and not self.skip_auth:
+ self.auth()
+
+ qparams = config.params['list'].copy()
+ if comments:
+ qparams['long_desc'] = query
+ else:
+ qparams['short_desc'] = query
+
+ qparams['order'] = config.choices['order'].get(order, 'Bug Number')
+ qparams['bug_severity'] = severity or []
+ qparams['priority'] = priority or []
+ if status is None:
+ # NEW, ASSIGNED and REOPENED is obsolete as of bugzilla 3.x and has
+ # been removed from bugs.gentoo.org on 2011/05/01
+ qparams['bug_status'] = ['NEW', 'ASSIGNED', 'REOPENED', 'UNCONFIRMED', 'CONFIRMED', 'IN_PROGRESS']
+ elif [s.upper() for s in status] == ['ALL']:
+ qparams['bug_status'] = config.choices['status']
+ else:
+ qparams['bug_status'] = [s.upper() for s in status]
+ qparams['product'] = product or ''
+ qparams['component'] = component or ''
+ qparams['status_whiteboard'] = whiteboard or ''
+ qparams['keywords'] = keywords or ''
+
+ # hoops to jump through for emails, since there are
+ # only two fields, we have to figure out what combinations
+ # to use if all three are set.
+ unique = list(set([assigned_to, cc, reporter, commenter]))
+ unique = [u for u in unique if u]
+ if len(unique) < 3:
+ for i in range(len(unique)):
+ e = unique[i]
+ n = i + 1
+ qparams['email%d' % n] = e
+ qparams['emailassigned_to%d' % n] = int(e == assigned_to)
+ qparams['emailreporter%d' % n] = int(e == reporter)
+ qparams['emailcc%d' % n] = int(e == cc)
+ qparams['emaillongdesc%d' % n] = int(e == commenter)
+ else:
+ raise AssertionError('Cannot set assigned_to, cc, and '
+ 'reporter in the same query')
+
+ req_params = urlencode(qparams, True)
+ req_url = urljoin(self.base, config.urls['list'])
+ req_url += '?' + req_params
+ req = Request(req_url, None, config.headers)
+ if self.httpuser and self.httppassword:
+ base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1]
+ req.add_header("Authorization", "Basic %s" % base64string)
+ resp = self.opener.open(req)
+ return self.extractResults(resp)
+
+ def namedcmd(self, cmd):
+ """Run command stored in Bugzilla by name.
+
+ @return: Result from the stored command.
+ @rtype: list of dicts
+ """
+
+ if not self.authenticated and not self.skip_auth:
+ self.auth()
+
+ qparams = config.params['namedcmd'].copy()
+ # Is there a better way of getting a command with a space in its name
+ # to be encoded as foo%20bar instead of foo+bar or foo%2520bar?
+ qparams['namedcmd'] = quote(cmd)
+ req_params = urlencode(qparams, True)
+ req_params = req_params.replace('%25','%')
+
+ req_url = urljoin(self.base, config.urls['list'])
+ req_url += '?' + req_params
+ req = Request(req_url, None, config.headers)
+ if self.user and self.password:
+ base64string = base64.encodestring('%s:%s' % (self.user, self.password))[:-1]
+ req.add_header("Authorization", "Basic %s" % base64string)
+ resp = self.opener.open(req)
+
+ return self.extractResults(resp)
+
+ def get(self, bugid):
+ """Get an ElementTree representation of a bug.
+
+ @param bugid: bug id
+ @type bugid: int
+
+ @rtype: ElementTree
+ """
+ if not self.authenticated and not self.skip_auth:
+ self.auth()
+
+ qparams = config.params['show'].copy()
+ qparams['id'] = bugid
+
+ req_params = urlencode(qparams, True)
+ req_url = urljoin(self.base, config.urls['show'])
+ req_url += '?' + req_params
+ req = Request(req_url, None, config.headers)
+ if self.httpuser and self.httppassword:
+ base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1]
+ req.add_header("Authorization", "Basic %s" % base64string)
+ resp = self.opener.open(req)
+
+ data = resp.read()
+ # Get rid of control characters.
+ data = re.sub('[\x00-\x08\x0e-\x1f\x0b\x0c]', '', data)
+ fd = StringIO(data)
+
+ # workaround for ill-defined XML templates in bugzilla 2.20.2
+ (major_version, minor_version) = \
+ (sys.version_info[0], sys.version_info[1])
+ if major_version > 2 or \
+ (major_version == 2 and minor_version >= 7):
+ # If this is 2.7 or greater, then XMLTreeBuilder
+ # does what we want.
+ parser = ElementTree.XMLParser()
+ else:
+ # Running under Python 2.6, so we need to use our
+ # subclass of XMLTreeBuilder instead.
+ parser = ForcedEncodingXMLTreeBuilder(encoding = 'utf-8')
+
+ etree = ElementTree.parse(fd, parser)
+ bug = etree.find('.//bug')
+ if bug is not None and bug.attrib.has_key('error'):
+ return None
+ else:
+ return etree
+
+ def modify(self, bugid, title = None, comment = None, url = None,
+ status = None, resolution = None,
+ assigned_to = None, duplicate = 0,
+ priority = None, severity = None,
+ add_cc = [], remove_cc = [],
+ add_dependson = [], remove_dependson = [],
+ add_blocked = [], remove_blocked = [],
+ whiteboard = None, keywords = None,
+ component = None):
+ """Modify an existing bug
+
+ @param bugid: bug id
+ @type bugid: int
+ @keyword title: new title for bug
+ @type title: string
+ @keyword comment: comment to add
+ @type comment: string
+ @keyword url: new url
+ @type url: string
+ @keyword status: new status (note, if you are changing it to RESOLVED, you need to set {resolution} as well.
+ @type status: string
+ @keyword resolution: new resolution (if status=RESOLVED)
+ @type resolution: string
+ @keyword assigned_to: email (needs to exist in bugzilla)
+ @type assigned_to: string
+ @keyword duplicate: bug id to duplicate against (if resolution = DUPLICATE)
+ @type duplicate: int
+ @keyword priority: new priority for bug
+ @type priority: string
+ @keyword severity: new severity for bug
+ @type severity: string
+ @keyword add_cc: list of emails to add to the cc list
+ @type add_cc: list of strings
+ @keyword remove_cc: list of emails to remove from cc list
+ @type remove_cc: list of string.
+ @keyword add_dependson: list of bug ids to add to the depend list
+ @type add_dependson: list of strings
+ @keyword remove_dependson: list of bug ids to remove from depend list
+ @type remove_dependson: list of strings
+ @keyword add_blocked: list of bug ids to add to the blocked list
+ @type add_blocked: list of strings
+ @keyword remove_blocked: list of bug ids to remove from blocked list
+ @type remove_blocked: list of strings
+
+ @keyword whiteboard: set status whiteboard
+ @type whiteboard: string
+ @keyword keywords: set keywords
+ @type keywords: string
+ @keyword component: set component
+ @type component: string
+
+ @return: list of fields modified.
+ @rtype: list of strings
+ """
+ if not self.authenticated and not self.skip_auth:
+ self.auth()
+
+
+ buginfo = Bugz.get(self, bugid)
+ if not buginfo:
+ return False
+
+ modified = []
+ qparams = config.params['modify'].copy()
+ qparams['id'] = bugid
+ # NOTE: knob has been removed in bugzilla 4 and 3?
+ qparams['knob'] = 'none'
+
+ # copy existing fields
+ FIELDS = ('bug_file_loc', 'bug_severity', 'short_desc', 'bug_status',
+ 'status_whiteboard', 'keywords', 'resolution',
+ 'op_sys', 'priority', 'version', 'target_milestone',
+ 'assigned_to', 'rep_platform', 'product', 'component', 'token')
+
+ FIELDS_MULTI = ('blocked', 'dependson')
+
+ for field in FIELDS:
+ try:
+ qparams[field] = buginfo.find('.//%s' % field).text
+ if qparams[field] is None:
+ del qparams[field]
+ except:
+ pass
+
+ for field in FIELDS_MULTI:
+ qparams[field] = [d.text for d in buginfo.findall('.//%s' % field)
+ if d is not None and d.text is not None]
+
+ # set 'knob' if we are change the status/resolution
+ # or trying to reassign bug.
+ if status:
+ status = status.upper()
+ if resolution:
+ resolution = resolution.upper()
+
+ if status and status != qparams['bug_status']:
+ # Bugzilla >= 3.x
+ qparams['bug_status'] = status
+
+ if status == 'RESOLVED':
+ qparams['knob'] = 'resolve'
+ if resolution:
+ qparams['resolution'] = resolution
+ else:
+ qparams['resolution'] = 'FIXED'
+
+ modified.append(('status', status))
+ modified.append(('resolution', qparams['resolution']))
+ elif status == 'ASSIGNED' or status == 'IN_PROGRESS':
+ qparams['knob'] = 'accept'
+ modified.append(('status', status))
+ elif status == 'REOPENED':
+ qparams['knob'] = 'reopen'
+ modified.append(('status', status))
+ elif status == 'VERIFIED':
+ qparams['knob'] = 'verified'
+ modified.append(('status', status))
+ elif status == 'CLOSED':
+ qparams['knob'] = 'closed'
+ modified.append(('status', status))
+ elif duplicate:
+ # Bugzilla >= 3.x
+ qparams['bug_status'] = "RESOLVED"
+ qparams['resolution'] = "DUPLICATE"
+
+ qparams['knob'] = 'duplicate'
+ qparams['dup_id'] = duplicate
+ modified.append(('status', 'RESOLVED'))
+ modified.append(('resolution', 'DUPLICATE'))
+ elif assigned_to:
+ qparams['knob'] = 'reassign'
+ qparams['assigned_to'] = assigned_to
+ modified.append(('assigned_to', assigned_to))
+
+ # setup modification of other bits
+ if comment:
+ qparams['comment'] = comment
+ modified.append(('comment', ellipsis(comment, 60)))
+ if title:
+ qparams['short_desc'] = title or ''
+ modified.append(('title', title))
+ if url is not None:
+ qparams['bug_file_loc'] = url
+ modified.append(('url', url))
+ if severity is not None:
+ qparams['bug_severity'] = severity
+ modified.append(('severity', severity))
+ if priority is not None:
+ qparams['priority'] = priority
+ modified.append(('priority', priority))
+
+ # cc manipulation
+ if add_cc is not None:
+ qparams['newcc'] = ', '.join(add_cc)
+ modified.append(('newcc', qparams['newcc']))
+ if remove_cc is not None:
+ qparams['cc'] = remove_cc
+ qparams['removecc'] = 'on'
+ modified.append(('cc', remove_cc))
+
+ # bug depend/blocked manipulation
+ changed_dependson = False
+ changed_blocked = False
+ if remove_dependson:
+ for bug_id in remove_dependson:
+ qparams['dependson'].remove(str(bug_id))
+ changed_dependson = True
+ if remove_blocked:
+ for bug_id in remove_blocked:
+ qparams['blocked'].remove(str(bug_id))
+ changed_blocked = True
+ if add_dependson:
+ for bug_id in add_dependson:
+ qparams['dependson'].append(str(bug_id))
+ changed_dependson = True
+ if add_blocked:
+ for bug_id in add_blocked:
+ qparams['blocked'].append(str(bug_id))
+ changed_blocked = True
+
+ qparams['dependson'] = ','.join(qparams['dependson'])
+ qparams['blocked'] = ','.join(qparams['blocked'])
+ if changed_dependson:
+ modified.append(('dependson', qparams['dependson']))
+ if changed_blocked:
+ modified.append(('blocked', qparams['blocked']))
+
+ if whiteboard is not None:
+ qparams['status_whiteboard'] = whiteboard
+ modified.append(('status_whiteboard', whiteboard))
+ if keywords is not None:
+ qparams['keywords'] = keywords
+ modified.append(('keywords', keywords))
+ if component is not None:
+ qparams['component'] = component
+ modified.append(('component', component))
+
+ req_params = urlencode(qparams, True)
+ req_url = urljoin(self.base, config.urls['modify'])
+ req = Request(req_url, req_params, config.headers)
+ if self.httpuser and self.httppassword:
+ base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1]
+ req.add_header("Authorization", "Basic %s" % base64string)
+
+ try:
+ resp = self.opener.open(req)
+ re_error = re.compile(r'id="error_msg".*>([^<]+)<')
+ error = re_error.search(resp.read())
+ if error:
+ print error.group(1)
+ return []
+ return modified
+ except:
+ return []
+
+ def attachment(self, attachid):
+ """Get an attachment by attachment_id
+
+ @param attachid: attachment id
+ @type attachid: int
+
+ @return: dict with three keys, 'filename', 'size', 'fd'
+ @rtype: dict
+ """
+ if not self.authenticated and not self.skip_auth:
+ self.auth()
+
+ qparams = config.params['attach'].copy()
+ qparams['id'] = attachid
+
+ req_params = urlencode(qparams, True)
+ req_url = urljoin(self.base, config.urls['attach'])
+ req_url += '?' + req_params
+ req = Request(req_url, None, config.headers)
+ if self.httpuser and self.httppassword:
+ base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1]
+ req.add_header("Authorization", "Basic %s" % base64string)
+ resp = self.opener.open(req)
+
+ try:
+ content_type = resp.info()['Content-type']
+ namefield = content_type.split(';')[1]
+ filename = re.search(r'name=\"(.*)\"', namefield).group(1)
+ content_length = int(resp.info()['Content-length'], 0)
+ return {'filename': filename, 'size': content_length, 'fd': resp}
+ except:
+ return {}
+
+ def post(self, product, component, title, description, url = '', assigned_to = '', cc = '', keywords = '', version = '', dependson = '', blocked = '', priority = '', severity = ''):
+ """Post a bug
+
+ @param product: product where the bug should be placed
+ @type product: string
+ @param component: component where the bug should be placed
+ @type component: string
+ @param title: title of the bug.
+ @type title: string
+ @param description: description of the bug
+ @type description: string
+ @keyword url: optional url to submit with bug
+ @type url: string
+ @keyword assigned_to: optional email to assign bug to
+ @type assigned_to: string.
+ @keyword cc: option list of CC'd emails
+ @type: string
+ @keyword keywords: option list of bugzilla keywords
+ @type: string
+ @keyword version: version of the component
+ @type: string
+ @keyword dependson: bugs this one depends on
+ @type: string
+ @keyword blocked: bugs this one blocks
+ @type: string
+ @keyword priority: priority of this bug
+ @type: string
+ @keyword severity: severity of this bug
+ @type: string
+
+ @rtype: int
+ @return: the bug number, or 0 if submission failed.
+ """
+ if not self.authenticated and not self.skip_auth:
+ self.auth()
+
+ qparams = config.params['post'].copy()
+ qparams['product'] = product
+ qparams['component'] = component
+ qparams['short_desc'] = title
+ qparams['comment'] = description
+ qparams['assigned_to'] = assigned_to
+ qparams['cc'] = cc
+ qparams['bug_file_loc'] = url
+ qparams['dependson'] = dependson
+ qparams['blocked'] = blocked
+ qparams['keywords'] = keywords
+
+ #XXX: default version is 'unspecified'
+ if version != '':
+ qparams['version'] = version
+
+ #XXX: default priority is 'Normal'
+ if priority != '':
+ qparams['priority'] = priority
+
+ #XXX: default severity is 'normal'
+ if severity != '':
+ qparams['bug_severity'] = severity
+
+ req_params = urlencode(qparams, True)
+ req_url = urljoin(self.base, config.urls['post'])
+ req = Request(req_url, req_params, config.headers)
+ if self.httpuser and self.httppassword:
+ base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1]
+ req.add_header("Authorization", "Basic %s" % base64string)
+ resp = self.opener.open(req)
+
+ try:
+ re_bug = re.compile(r'(?:\s+)?<title>.*Bug ([0-9]+) Submitted.*</title>')
+ bug_match = re_bug.search(resp.read())
+ if bug_match:
+ return int(bug_match.group(1))
+ except:
+ pass
+
+ return 0
+
+ def attach(self, bugid, title, description, filename,
+ content_type = 'text/plain', ispatch = False):
+ """Attach a file to a bug.
+
+ @param bugid: bug id
+ @type bugid: int
+ @param title: short description of attachment
+ @type title: string
+ @param description: long description of the attachment
+ @type description: string
+ @param filename: filename of the attachment
+ @type filename: string
+ @keywords content_type: mime-type of the attachment
+ @type content_type: string
+
+ @rtype: bool
+ @return: True if successful, False if not successful.
+ """
+ if not self.authenticated and not self.skip_auth:
+ self.auth()
+
+ qparams = config.params['attach_post'].copy()
+ qparams['bugid'] = bugid
+ qparams['description'] = title
+ qparams['comment'] = description
+ if ispatch:
+ qparams['ispatch'] = '1'
+ qparams['contenttypeentry'] = 'text/plain'
+ else:
+ qparams['contenttypeentry'] = content_type
+
+ filedata = [('data', filename, open(filename).read())]
+ content_type, body = encode_multipart_formdata(qparams.items(),
+ filedata)
+
+ req_headers = config.headers.copy()
+ req_headers['Content-type'] = content_type
+ req_headers['Content-length'] = len(body)
+ req_url = urljoin(self.base, config.urls['attach_post'])
+ req = Request(req_url, body, req_headers)
+ if self.httpuser and self.httppassword:
+ base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1]
+ req.add_header("Authorization", "Basic %s" % base64string)
+ resp = self.opener.open(req)
+
+ # TODO: return attachment id and success?
+ try:
+ re_attach = re.compile(r'<title>(.+)</title>')
+ # Bugzilla 3/4
+ re_attach34 = re.compile(r'Attachment \d+ added to Bug \d+')
+ response = resp.read()
+ attach_match = re_attach.search(response)
+ if attach_match:
+ if attach_match.group(1) == "Changes Submitted" or re_attach34.match(attach_match.group(1)):
+ return True
+ else:
+ return attach_match.group(1)
+ else:
+ return False
+ except:
+ pass
+
+ return False
diff --git a/third_party/pybugz-0.9.3/bugz/cli.py b/third_party/pybugz-0.9.3/bugz/cli.py
new file mode 100644
index 0000000..35bf98e
--- /dev/null
+++ b/third_party/pybugz-0.9.3/bugz/cli.py
@@ -0,0 +1,607 @@
+#!/usr/bin/env python
+
+import commands
+import locale
+import os
+import re
+import sys
+import tempfile
+import textwrap
+
+from urlparse import urljoin
+
+try:
+ import readline
+except ImportError:
+ readline = None
+
+from bugzilla import Bugz
+from config import config
+
+BUGZ_COMMENT_TEMPLATE = \
+"""
+BUGZ: ---------------------------------------------------
+%s
+BUGZ: Any line beginning with 'BUGZ:' will be ignored.
+BUGZ: ---------------------------------------------------
+"""
+
+DEFAULT_NUM_COLS = 80
+
+#
+# Auxiliary functions
+#
+
+def raw_input_block():
+ """ Allows multiple line input until a Ctrl+D is detected.
+
+ @rtype: string
+ """
+ target = ''
+ while True:
+ try:
+ line = raw_input()
+ target += line + '\n'
+ except EOFError:
+ return target
+
+#
+# This function was lifted from Bazaar 1.9.
+#
+def terminal_width():
+ """Return estimated terminal width."""
+ if sys.platform == 'win32':
+ return win32utils.get_console_size()[0]
+ width = DEFAULT_NUM_COLS
+ try:
+ import struct, fcntl, termios
+ s = struct.pack('HHHH', 0, 0, 0, 0)
+ x = fcntl.ioctl(1, termios.TIOCGWINSZ, s)
+ width = struct.unpack('HHHH', x)[1]
+ except IOError:
+ pass
+ if width <= 0:
+ try:
+ width = int(os.environ['COLUMNS'])
+ except:
+ pass
+ if width <= 0:
+ width = DEFAULT_NUM_COLS
+
+ return width
+
+def launch_editor(initial_text, comment_from = '',comment_prefix = 'BUGZ:'):
+ """Launch an editor with some default text.
+
+ Lifted from Mercurial 0.9.
+ @rtype: string
+ """
+ (fd, name) = tempfile.mkstemp("bugz")
+ f = os.fdopen(fd, "w")
+ f.write(comment_from)
+ f.write(initial_text)
+ f.close()
+
+ editor = (os.environ.get("BUGZ_EDITOR") or
+ os.environ.get("EDITOR"))
+ if editor:
+ result = os.system("%s \"%s\"" % (editor, name))
+ if result != 0:
+ raise RuntimeError('Unable to launch editor: %s' % editor)
+
+ new_text = open(name).read()
+ new_text = re.sub('(?m)^%s.*\n' % comment_prefix, '', new_text)
+ os.unlink(name)
+ return new_text
+
+ return ''
+
+def block_edit(comment, comment_from = ''):
+ editor = (os.environ.get('BUGZ_EDITOR') or
+ os.environ.get('EDITOR'))
+
+ if not editor:
+ print comment + ': (Press Ctrl+D to end)'
+ new_text = raw_input_block()
+ return new_text
+
+ initial_text = '\n'.join(['BUGZ: %s'%line for line in comment.split('\n')])
+ new_text = launch_editor(BUGZ_COMMENT_TEMPLATE % initial_text, comment_from)
+
+ if new_text.strip():
+ return new_text
+ else:
+ return ''
+
+#
+# Bugz specific exceptions
+#
+
+class BugzError(Exception):
+ pass
+
+class PrettyBugz(Bugz):
+ def __init__(self, base, user = None, password =None, forget = False,
+ columns = 0, encoding = '', skip_auth = False,
+ quiet = False, httpuser = None, httppassword = None ):
+
+ self.quiet = quiet
+ self.columns = columns or terminal_width()
+
+ Bugz.__init__(self, base, user, password, forget, skip_auth, httpuser, httppassword)
+
+ self.log("Using %s " % self.base)
+
+ if not encoding:
+ try:
+ self.enc = locale.getdefaultlocale()[1]
+ except:
+ self.enc = 'utf-8'
+
+ if not self.enc:
+ self.enc = 'utf-8'
+ else:
+ self.enc = encoding
+
+ def log(self, status_msg, newline = True):
+ if not self.quiet:
+ if newline:
+ print ' * %s' % status_msg
+ else:
+ print ' * %s' % status_msg,
+
+ def warn(self, warn_msg):
+ if not self.quiet:
+ print ' ! Warning: %s' % warn_msg
+
+ def get_input(self, prompt):
+ return raw_input(prompt)
+
+ def search(self, **kwds):
+ """Performs a search on the bugzilla database with the keywords given on the title (or the body if specified).
+ """
+ search_term = ' '.join(kwds['terms']).strip()
+ del kwds['terms']
+ show_status = kwds['show_status']
+ del kwds['show_status']
+ show_url = kwds['show_url']
+ del kwds['show_url']
+ search_opts = sorted([(opt, val) for opt, val in kwds.items()
+ if val is not None and opt != 'order'])
+
+ if not (search_term or search_opts):
+ raise BugzError('Please give search terms or options.')
+
+ if search_term:
+ log_msg = 'Searching for \'%s\' ' % search_term
+ else:
+ log_msg = 'Searching for bugs '
+
+ if search_opts:
+ self.log(log_msg + 'with the following options:')
+ for opt, val in search_opts:
+ self.log(' %-20s = %s' % (opt, val))
+ else:
+ self.log(log_msg)
+
+ result = Bugz.search(self, search_term, **kwds)
+
+ if result is None:
+ raise RuntimeError('Failed to perform search')
+
+ if len(result) == 0:
+ self.log('No bugs found.')
+ return
+
+ self.listbugs(result, show_url, show_status)
+
+ def namedcmd(self, command, show_status=False, show_url=False):
+ """Run a command stored in Bugzilla by name."""
+ log_msg = 'Running namedcmd \'%s\''%command
+ result = Bugz.namedcmd(self, command)
+ if result is None:
+ raise RuntimeError('Failed to run command\nWrong namedcmd perhaps?')
+
+ if len(result) == 0:
+ self.log('No result from command')
+ return
+
+ self.listbugs(result, show_url, show_status)
+
+ def get(self, bugid, comments = True, attachments = True):
+ """ Fetch bug details given the bug id """
+ self.log('Getting bug %s ..' % bugid)
+
+ result = Bugz.get(self, bugid)
+
+ if result is None:
+ raise RuntimeError('Bug %s not found' % bugid)
+
+ # Print out all the fields below by extract the text
+ # directly from the tag, and just ignore if we don't
+ # see the tag.
+ FIELDS = (
+ ('short_desc', 'Title'),
+ ('assigned_to', 'Assignee'),
+ ('creation_ts', 'Reported'),
+ ('delta_ts', 'Updated'),
+ ('bug_status', 'Status'),
+ ('resolution', 'Resolution'),
+ ('bug_file_loc', 'URL'),
+ ('bug_severity', 'Severity'),
+ ('priority', 'Priority'),
+ ('reporter', 'Reporter'),
+ )
+
+ MORE_FIELDS = (
+ ('product', 'Product'),
+ ('component', 'Component'),
+ ('status_whiteboard', 'Whiteboard'),
+ ('keywords', 'Keywords'),
+ )
+
+ for field, name in FIELDS + MORE_FIELDS:
+ try:
+ value = result.find('.//%s' % field).text
+ if value is None:
+ continue
+ except AttributeError:
+ continue
+ print '%-12s: %s' % (name, value.encode(self.enc))
+
+ # Print out the cc'ed people
+ cced = result.findall('.//cc')
+ for cc in cced:
+ print '%-12s: %s' % ('CC', cc.text)
+
+ # print out depends
+ dependson = ', '.join([d.text for d in result.findall('.//dependson')])
+ blocked = ', '.join([d.text for d in result.findall('.//blocked')])
+ if dependson:
+ print '%-12s: %s' % ('DependsOn', dependson)
+ if blocked:
+ print '%-12s: %s' % ('Blocked', blocked)
+
+ bug_comments = result.findall('.//long_desc')
+ bug_attachments = result.findall('.//attachment')
+
+ print '%-12s: %d' % ('Comments', len(bug_comments))
+ print '%-12s: %d' % ('Attachments', len(bug_attachments))
+ print
+
+ if attachments:
+ for attachment in bug_attachments:
+ aid = attachment.find('.//attachid').text
+ desc = attachment.find('.//desc').text
+ when = attachment.find('.//date').text
+ print '[Attachment] [%s] [%s]' % (aid, desc.encode(self.enc))
+
+ if comments:
+ i = 0
+ wrapper = textwrap.TextWrapper(width = self.columns)
+ for comment in bug_comments:
+ try:
+ who = comment.find('.//who').text.encode(self.enc)
+ except AttributeError:
+ # Novell doesn't use 'who' on xml
+ who = ""
+ when = comment.find('.//bug_when').text.encode(self.enc)
+ what = comment.find('.//thetext').text
+ print '\n[Comment #%d] %s : %s' % (i, who, when)
+ print '-' * (self.columns - 1)
+
+ if what is None:
+ what = ''
+
+ # print wrapped version
+ for line in what.split('\n'):
+ if len(line) < self.columns:
+ print line.encode(self.enc)
+ else:
+ for shortline in wrapper.wrap(line):
+ print shortline.encode(self.enc)
+ i += 1
+ print
+
+ def post(self, product = None, component = None,
+ title = None, description = None, assigned_to = None,
+ cc = None, url = None, keywords = None,
+ description_from = None, prodversion = None, append_command = None,
+ dependson = None, blocked = None, batch = False,
+ default_confirm = 'y', priority = None, severity = None):
+ """Post a new bug"""
+
+ # load description from file if possible
+ if description_from:
+ try:
+ description = open(description_from, 'r').read()
+ except IOError, e:
+ raise BugzError('Unable to read from file: %s: %s' % \
+ (description_from, e))
+
+ if not batch:
+ self.log('Press Ctrl+C at any time to abort.')
+
+ #
+ # Check all bug fields.
+ # XXX: We use "if not <field>" for mandatory fields
+ # and "if <field> is None" for optional ones.
+ #
+
+ # check for product
+ if not product:
+ while not product or len(product) < 1:
+ product = self.get_input('Enter product: ')
+ else:
+ self.log('Enter product: %s' % product)
+
+ # check for component
+ if not component:
+ while not component or len(component) < 1:
+ component = self.get_input('Enter component: ')
+ else:
+ self.log('Enter component: %s' % component)
+
+ # check for version
+ # FIXME: This default behaviour is not too nice.
+ if prodversion is None:
+ prodversion = self.get_input('Enter version (default: unspecified): ')
+ else:
+ self.log('Enter version: %s' % prodversion)
+
+ # check for default severity
+ if severity is None:
+ severity_msg ='Enter severity (eg. normal) (optional): '
+ severity = self.get_input(severity_msg)
+ else:
+ self.log('Enter severity (optional): %s' % severity)
+
+ # fixme: hw platform
+ # fixme: os
+ # fixme: milestone
+
+ # check for default priority
+ if priority is None:
+ priority_msg ='Enter priority (eg. Normal) (optional): '
+ priority = self.get_input(priority_msg)
+ else:
+ self.log('Enter priority (optional): %s' % priority)
+
+ # fixme: status
+
+ # check for default assignee
+ if assigned_to is None:
+ assigned_msg ='Enter assignee (eg. liquidx@gentoo.org) (optional): '
+ assigned_to = self.get_input(assigned_msg)
+ else:
+ self.log('Enter assignee (optional): %s' % assigned_to)
+
+ # check for CC list
+ if cc is None:
+ cc_msg = 'Enter a CC list (comma separated) (optional): '
+ cc = self.get_input(cc_msg)
+ else:
+ self.log('Enter a CC list (optional): %s' % cc)
+
+ # check for optional URL
+ if url is None:
+ url = self.get_input('Enter URL (optional): ')
+ else:
+ self.log('Enter URL (optional): %s' % url)
+
+ # check for title
+ if not title:
+ while not title or len(title) < 1:
+ title = self.get_input('Enter title: ')
+ else:
+ self.log('Enter title: %s' % title)
+
+ # check for description
+ if not description:
+ description = block_edit('Enter bug description: ')
+ else:
+ self.log('Enter bug description: %s' % description)
+
+ if append_command is None:
+ append_command = self.get_input('Append the output of the following command (leave blank for none): ')
+ else:
+ self.log('Append command (optional): %s' % append_command)
+
+ # check for Keywords list
+ if keywords is None:
+ kwd_msg = 'Enter a Keywords list (comma separated) (optional): '
+ keywords = self.get_input(kwd_msg)
+ else:
+ self.log('Enter a Keywords list (optional): %s' % keywords)
+
+ # check for bug dependencies
+ if dependson is None:
+ dependson_msg = 'Enter a list of bug dependencies (comma separated) (optional): '
+ dependson = self.get_input(dependson_msg)
+ else:
+ self.log('Enter a list of bug dependencies (optional): %s' % dependson)
+
+ # check for blocker bugs
+ if blocked is None:
+ blocked_msg = 'Enter a list of blocker bugs (comma separated) (optional): '
+ blocked = self.get_input(blocked_msg)
+ else:
+ self.log('Enter a list of blocker bugs (optional): %s' % blocked)
+
+ # fixme: groups
+ # append the output from append_command to the description
+ if append_command is not None and append_command != '':
+ append_command_output = commands.getoutput(append_command)
+ description = description + '\n\n' + '$ ' + append_command + '\n' + append_command_output
+
+ # raise an exception if mandatory fields are not specified.
+ if product is None:
+ raise RuntimeError('Product not specified')
+ if component is None:
+ raise RuntimeError('Component not specified')
+ if title is None:
+ raise RuntimeError('Title not specified')
+ if description is None:
+ raise RuntimeError('Description not specified')
+
+ # set optional fields to their defaults if they are not set.
+ if prodversion is None:
+ prodversion = ''
+ if priority is None:
+ priority = ''
+ if severity is None:
+ severity = ''
+ if assigned_to is None:
+ assigned_to = ''
+ if cc is None:
+ cc = ''
+ if url is None:
+ url = ''
+ if keywords is None:
+ keywords = ''
+ if dependson is None:
+ dependson = ''
+ if blocked is None:
+ blocked = ''
+
+ # print submission confirmation
+ print '-' * (self.columns - 1)
+ print 'Product : ' + product
+ print 'Component : ' + component
+ print 'Version : ' + prodversion
+ print 'severity : ' + severity
+ # fixme: hardware
+ # fixme: OS
+ # fixme: Milestone
+ print 'priority : ' + priority
+ # fixme: status
+ print 'Assigned to : ' + assigned_to
+ print 'CC : ' + cc
+ print 'URL : ' + url
+ print 'Title : ' + title
+ print 'Description : ' + description
+ print 'Keywords : ' + keywords
+ print 'Depends on : ' + dependson
+ print 'Blocks : ' + blocked
+ # fixme: groups
+ print '-' * (self.columns - 1)
+
+ if not batch:
+ if default_confirm in ['Y','y']:
+ confirm = raw_input('Confirm bug submission (Y/n)? ')
+ else:
+ confirm = raw_input('Confirm bug submission (y/N)? ')
+ if len(confirm) < 1:
+ confirm = default_confirm
+ if confirm[0] not in ('y', 'Y'):
+ self.log('Submission aborted')
+ return
+
+ result = Bugz.post(self, product, component, title, description, url, assigned_to, cc, keywords, prodversion, dependson, blocked, priority, severity)
+ if result is not None and result != 0:
+ self.log('Bug %d submitted' % result)
+ else:
+ raise RuntimeError('Failed to submit bug')
+
+ def modify(self, bugid, **kwds):
+ """Modify an existing bug (eg. adding a comment or changing resolution.)"""
+ if 'comment_from' in kwds:
+ if kwds['comment_from']:
+ try:
+ kwds['comment'] = open(kwds['comment_from'], 'r').read()
+ except IOError, e:
+ raise BugzError('Failed to get read from file: %s: %s' % \
+ (comment_from, e))
+
+ if 'comment_editor' in kwds:
+ if kwds['comment_editor']:
+ kwds['comment'] = block_edit('Enter comment:', kwds['comment'])
+ del kwds['comment_editor']
+
+ del kwds['comment_from']
+
+ if 'comment_editor' in kwds:
+ if kwds['comment_editor']:
+ kwds['comment'] = block_edit('Enter comment:')
+ del kwds['comment_editor']
+
+ if kwds['fixed']:
+ kwds['status'] = 'RESOLVED'
+ kwds['resolution'] = 'FIXED'
+ del kwds['fixed']
+
+ if kwds['invalid']:
+ kwds['status'] = 'RESOLVED'
+ kwds['resolution'] = 'INVALID'
+ del kwds['invalid']
+ result = Bugz.modify(self, bugid, **kwds)
+ if not result:
+ raise RuntimeError('Failed to modify bug')
+ else:
+ self.log('Modified bug %s with the following fields:' % bugid)
+ for field, value in result:
+ self.log(' %-12s: %s' % (field, value))
+
+ def attachment(self, attachid, view = False):
+ """ Download or view an attachment given the id."""
+ self.log('Getting attachment %s' % attachid)
+
+ result = Bugz.attachment(self, attachid)
+ if not result:
+ raise RuntimeError('Unable to get attachment')
+
+ action = {True:'Viewing', False:'Saving'}
+ self.log('%s attachment: "%s"' % (action[view], result['filename']))
+ safe_filename = os.path.basename(re.sub(r'\.\.', '',
+ result['filename']))
+
+ if view:
+ print result['fd'].read()
+ else:
+ if os.path.exists(result['filename']):
+ raise RuntimeError('Filename already exists')
+
+ open(safe_filename, 'wb').write(result['fd'].read())
+
+ def attach(self, bugid, filename, content_type = 'text/plain', patch = False, description = None):
+ """ Attach a file to a bug given a filename. """
+ if not os.path.exists(filename):
+ raise BugzError('File not found: %s' % filename)
+ if not description:
+ description = block_edit('Enter description (optional)')
+ result = Bugz.attach(self, bugid, filename, description, filename,
+ content_type, patch)
+ if result == True:
+ self.log("'%s' has been attached to bug %s" % (filename, bugid))
+ else:
+ reason = ""
+ if result and result != False:
+ reason = "\nreason: %s" % result
+ raise RuntimeError("Failed to attach '%s' to bug %s%s" % (filename,
+ bugid, reason))
+
+ def listbugs(self, buglist, show_url=False, show_status=False):
+ x = ''
+ if re.search("/$", self.base) is None:
+ x = '/'
+ for row in buglist:
+ bugid = row['bugid']
+ if show_url:
+ bugid = '%s%s%s?id=%s'%(self.base, x, config.urls['show'], bugid)
+ status = row['status']
+ desc = row['desc']
+ line = '%s' % (bugid)
+ if show_status:
+ line = '%s %s' % (line, status)
+ if row.has_key('assignee'): # Novell does not have 'assignee' field
+ assignee = row['assignee'].split('@')[0]
+ line = '%s %-20s' % (line, assignee)
+
+ line = '%s %s' % (line, desc)
+
+ try:
+ print line.encode(self.enc)[:self.columns]
+ except UnicodeDecodeError:
+ print line[:self.columns]
+
+ self.log("%i bug(s) found." % len(buglist))
diff --git a/third_party/pybugz-0.9.3/bugz/config.py b/third_party/pybugz-0.9.3/bugz/config.py
new file mode 100644
index 0000000..5ca48c3
--- /dev/null
+++ b/third_party/pybugz-0.9.3/bugz/config.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python
+
+from bugz import __version__
+import csv
+import locale
+
+BUGZ_USER_AGENT = 'PyBugz/%s +http://www.github.com/williamh/pybugz/' % __version__
+
+class BugzConfig:
+ urls = {
+ 'auth': 'index.cgi',
+ 'list': 'buglist.cgi',
+ 'show': 'show_bug.cgi',
+ 'attach': 'attachment.cgi',
+ 'post': 'post_bug.cgi',
+ 'modify': 'process_bug.cgi',
+ 'attach_post': 'attachment.cgi',
+ }
+
+ headers = {
+ 'Accept': '*/*',
+ 'User-agent': BUGZ_USER_AGENT,
+ }
+
+ params = {
+ 'auth': {
+ "Bugzilla_login": "",
+ "Bugzilla_password": "",
+ "GoAheadAndLogIn": "1",
+ },
+
+ 'post': {
+ 'product': '',
+ 'version': 'unspecified',
+ 'component': '',
+ 'short_desc': '',
+ 'comment': '',
+# 'rep_platform': 'All',
+# 'op_sys': 'Linux',
+ },
+
+ 'attach': {
+ 'id':''
+ },
+
+ 'attach_post': {
+ 'action': 'insert',
+ 'ispatch': '',
+ 'contenttypemethod': 'manual',
+ 'bugid': '',
+ 'description': '',
+ 'contenttypeentry': 'text/plain',
+ 'comment': '',
+ },
+
+ 'show': {
+ 'id': '',
+ 'ctype': 'xml'
+ },
+
+ 'list': {
+ 'query_format': 'advanced',
+ 'short_desc_type': 'allwordssubstr',
+ 'short_desc': '',
+ 'long_desc_type': 'substring',
+ 'long_desc' : '',
+ 'bug_file_loc_type': 'allwordssubstr',
+ 'bug_file_loc': '',
+ 'status_whiteboard_type': 'allwordssubstr',
+ 'status_whiteboard': '',
+ # NEW, ASSIGNED and REOPENED is obsolete as of bugzilla 3.x and has
+ # been removed from bugs.gentoo.org on 2011/05/01
+ 'bug_status': ['NEW', 'ASSIGNED', 'REOPENED', 'UNCONFIRMED', 'CONFIRMED', 'IN_PROGRESS'],
+ 'bug_severity': [],
+ 'priority': [],
+ 'emaillongdesc1': '1',
+ 'emailassigned_to1':'1',
+ 'emailtype1': 'substring',
+ 'email1': '',
+ 'emaillongdesc2': '1',
+ 'emailassigned_to2':'1',
+ 'emailreporter2':'1',
+ 'emailcc2':'1',
+ 'emailtype2':'substring',
+ 'email2':'',
+ 'bugidtype':'include',
+ 'bug_id':'',
+ 'chfieldfrom':'',
+ 'chfieldto':'Now',
+ 'chfieldvalue':'',
+ 'cmdtype':'doit',
+ 'order': 'Bug Number',
+ 'field0-0-0':'noop',
+ 'type0-0-0':'noop',
+ 'value0-0-0':'',
+ 'ctype':'csv',
+ },
+
+ 'modify': {
+ # 'delta_ts': '%Y-%m-%d %H:%M:%S',
+ 'longdesclength': '1',
+ 'id': '',
+ 'newcc': '',
+ 'removecc': '', # remove selected cc's if set
+ 'cc': '', # only if there are already cc's
+ 'bug_file_loc': '',
+ 'bug_severity': '',
+ 'bug_status': '',
+ 'op_sys': '',
+ 'priority': '',
+ 'version': '',
+ 'target_milestone': '',
+ 'rep_platform': '',
+ 'product':'',
+ 'component': '',
+ 'short_desc': '',
+ 'status_whiteboard': '',
+ 'keywords': '',
+ 'dependson': '',
+ 'blocked': '',
+ 'knob': ('none', 'assigned', 'resolve', 'duplicate', 'reassign'),
+ 'resolution': '', # only valid for knob=resolve
+ 'dup_id': '', # only valid for knob=duplicate
+ 'assigned_to': '',# only valid for knob=reassign
+ 'form_name': 'process_bug',
+ 'comment':''
+ },
+
+ 'namedcmd': {
+ 'cmdtype' : 'runnamed',
+ 'namedcmd' : '',
+ 'ctype':'csv'
+ }
+ }
+
+ choices = {
+ 'status': {
+ 'unconfirmed': 'UNCONFIRMED',
+ 'confirmed': 'CONFIRMED',
+ 'new': 'NEW',
+ 'assigned': 'ASSIGNED',
+ 'in_progress': 'IN_PROGRESS',
+ 'reopened': 'REOPENED',
+ 'resolved': 'RESOLVED',
+ 'verified': 'VERIFIED',
+ 'closed': 'CLOSED'
+ },
+
+ 'order': {
+ 'number' : 'Bug Number',
+ 'assignee': 'Assignee',
+ 'importance': 'Importance',
+ 'date': 'Last Changed'
+ },
+
+ 'columns': [
+ 'bugid',
+ 'alias',
+ 'severity',
+ 'priority',
+ 'arch',
+ 'assignee',
+ 'status',
+ 'resolution',
+ 'desc'
+ ],
+
+ 'column_alias': {
+ 'bug_id': 'bugid',
+ 'alias': 'alias',
+ 'bug_severity': 'severity',
+ 'priority': 'priority',
+ 'op_sys': 'arch', #XXX: Gentoo specific?
+ 'assigned_to': 'assignee',
+ 'assigned_to_realname': 'assignee', #XXX: Distinguish from assignee?
+ 'bug_status': 'status',
+ 'resolution': 'resolution',
+ 'short_desc': 'desc',
+ 'short_short_desc': 'desc',
+ },
+ # Novell: bug_id,"bug_severity","priority","op_sys","bug_status","resolution","short_desc"
+ # Gentoo: bug_id,"bug_severity","priority","op_sys","assigned_to","bug_status","resolution","short_short_desc"
+ # Redhat: bug_id,"alias","bug_severity","priority","rep_platform","assigned_to","bug_status","resolution","short_short_desc"
+ # Mandriva: 'bug_id', 'bug_severity', 'priority', 'assigned_to_realname', 'bug_status', 'resolution', 'keywords', 'short_desc'
+
+ 'resolution': {
+ 'fixed': 'FIXED',
+ 'invalid': 'INVALID',
+ 'wontfix': 'WONTFIX',
+ 'lated': 'LATER',
+ 'remind': 'REMIND',
+ 'worksforme': 'WORKSFORME',
+ 'cantfix': 'CANTFIX',
+ 'needinfo': 'NEEDINFO',
+ 'test-request': 'TEST-REQUEST',
+ 'upstream': 'UPSTREAM',
+ 'duplicate': 'DUPLICATE',
+ },
+
+ 'severity': [
+ 'blocker',
+ 'critical',
+ 'major',
+ 'normal',
+ 'minor',
+ 'trivial',
+ 'enhancement',
+ 'QA',
+ ],
+
+ 'priority': {
+ 1:'Highest',
+ 2:'High',
+ 3:'Normal',
+ 4:'Low',
+ 5:'Lowest',
+ }
+
+ }
+
+#
+# Global configuration
+#
+
+try:
+ config
+except NameError:
+ config = BugzConfig()
+
diff --git a/third_party/pybugz-0.9.3/bugzrc.example b/third_party/pybugz-0.9.3/bugzrc.example
new file mode 100644
index 0000000..3be9006
--- /dev/null
+++ b/third_party/pybugz-0.9.3/bugzrc.example
@@ -0,0 +1,25 @@
+#
+# bugzrc.example - an example configuration file for pybugz
+#
+# This file consists of sections which define parameters for each
+# bugzilla you plan to use.
+#
+# Each section begins with a name in square brackets. This is also the
+# name that should be used with the --connection parameter to the bugz
+# command.
+#
+# Each section of this file consists of lines in the form:
+# key: value
+# as listed below.
+#
+# [sectionname]
+# base: http://my.project.com/bugzilla/
+# user: xyz@zyx.org
+# password: secret2
+# httpuser: xyz
+# httppassword: secret2
+# forget: True
+# columns: 80
+# encoding: utf-8
+# quiet: True
+
diff --git a/third_party/pybugz-0.9.3/contrib/bash-completion b/third_party/pybugz-0.9.3/contrib/bash-completion
new file mode 100644
index 0000000..4edaf63
--- /dev/null
+++ b/third_party/pybugz-0.9.3/contrib/bash-completion
@@ -0,0 +1,66 @@
+#
+# Bash completion support for bugz
+#
+_bugz() {
+ local cur prev commands opts
+ commands="attach attachment get help modify namedcmd post search"
+ opts="--version -h --help --skip-auth -f --forget --encoding -q --quiet
+ -b --base -u --user -H --httpuser -p --password --columns
+ -P --httppassword"
+ COMPREPLY=()
+ cur="${COMP_WORDS[COMP_CWORD]}"
+ if [[ $COMP_CWORD -eq 1 ]]; then
+ if [[ "$cur" == -* ]]; then
+ COMPREPLY=( $( compgen -W '--help -h --version' -- $cur ) )
+ else
+ COMPREPLY=( $( compgen -W "$commands" -- $cur ) )
+ fi
+ else
+ prev="${COMP_WORDS[COMP_CWORD-1]}"
+ command="${COMP_WORDS[1]}"
+ case ${command} in
+ attach)
+ opts="${opts} -d --description -c --content_type"
+ ;;
+ attachment)
+ opts="${opts} -v --view"
+ ;;
+ get)
+ opts="${opts} -n --no-comments"
+ ;;
+ modify)
+ opts="${opts}
+ -c --comment -s --status -F --comment-from
+ --fixed -S --severity -t --title -U --url
+ -w --whiteboard --add-dependson --invalid
+ --add-blocked --priority --remove-cc -d --duplicate
+ --remove-dependson -a --assigned-to -k --keywords
+ --add-cc -C --comment-editor -r --resolution
+ --remove-blocked"
+ ;;
+ namedcmd)
+ opts="${opts} --show-url --show-status"
+ ;;
+ post)
+ opts="${opts}
+ --product -d --description -t --title
+ --append-command -S --severity --depends-on --component
+ --batch --prodversion --default-confirm --priority
+ -F --description-from -U --url -a --assigned-to
+ -k --keywords --cc --blocked"
+ ;;
+ search)
+ opts="${opts}
+ -s --status --show-url --product -w --whiteboard
+ --severity -r --reporter --cc --commenter
+ -C --component -c --comments --priority
+ -a --assigned-to -k --keywords -o --order --show-status"
+ ;;
+ *)
+ ;;
+ esac
+ COMPREPLY=( $( compgen -W "$opts" -- $cur ) )
+ fi
+ return 0
+}
+complete -F _bugz bugz
diff --git a/third_party/pybugz-0.9.3/contrib/zsh-completion b/third_party/pybugz-0.9.3/contrib/zsh-completion
new file mode 100644
index 0000000..c88ebff
--- /dev/null
+++ b/third_party/pybugz-0.9.3/contrib/zsh-completion
@@ -0,0 +1,158 @@
+#compdef bugz
+# Copyright 2009 Ingmar Vanhassel <ingmar@exherbo.org>
+# vim: set et sw=2 sts=2 ts=2 ft=zsh :
+
+_bugz() {
+ local -a _bugz_options _bugz_commands
+ local cmd
+
+ _bugz_options=(
+ '(-b --base)'{-b,--base}'[bugzilla base URL]:bugzilla url: '
+ '(-u --user)'{-u,--user}'[user name (if required)]:user name:_users'
+ '(-p --password)'{-p,--password}'[password (if required)]:password: '
+ '(-H --httpuser)'{-H,--httpuser}'[basic http auth user name (if required)]:user name:_users'
+ '(-P --httppassword)'{-P,--httppassword}'[basic http auth password (if required)]:password: '
+ '(-f --forget)'{-f,--forget}'[do not remember authentication]'
+ '--columns[number of columns to use when displaying output]:number: '
+ '--skip-auth[do not authenticate]'
+ '(-q --quiet)'{-q,--quiet}'[do not display status messages]'
+ )
+ _bugz_commands=(
+ 'attach:attach file to a bug'
+ 'attachment:get an attachment from bugzilla'
+ 'get:get a bug from bugzilla'
+ 'help:display subcommands'
+ 'modify:modify a bug (eg. post a comment)'
+ 'namedcmd:run a stored search'
+ 'post:post a new bug into bugzilla'
+ 'search:search for bugs in bugzilla'
+ )
+
+ for (( i=1; i <= ${CURRENT}; i++ )); do
+ cmd=${_bugz_commands[(r)${words[${i}]}:*]%%:*}
+ (( ${#cmd} )) && break
+ done
+
+ if (( ${#cmd} )); then
+ local curcontext="${curcontext%:*:*}:bugz-${cmd}:"
+
+ while [[ ${words[1]} != ${cmd} ]]; do
+ (( CURRENT-- ))
+ shift words
+ done
+
+ _call_function ret _bugz_cmd_${cmd}
+ return ret
+ else
+ _arguments -s : $_bugz_options
+ _describe -t commands 'commands' _bugz_commands
+ fi
+}
+
+(( ${+functions[_bugz_cmd_attach]} )) ||
+_bugz_cmd_attach()
+{
+ _arguments -s : \
+ '(--content_type= -c)'{--content_type=,-c}'[mimetype of the file]:MIME-Type:_mime_types' \
+ '(--description= -d)'{--description=,-d}'[a description of the attachment]:description: ' \
+ '--help[show help message and exit]'
+}
+
+(( ${+functions[_bugz_cmd_attachment]} )) ||
+_bugz_cmd_attachment()
+{
+ _arguments -s : \
+ '--help[show help message and exit]' \
+ '(--view -v)'{--view,-v}'[print attachment rather than save]'
+}
+
+
+(( ${+functions[_bugz_cmd_get]} )) ||
+_bugz_cmd_get()
+{
+ _arguments -s : \
+ '--help[show help message and exit]' \
+ '(--no-comments -n)'{--no-comments,-n}'[do not show comments]'
+}
+
+(( ${+functions[_bugz_cmd_modify]} )) ||
+_bugz_cmd_modify()
+{
+ _arguments -s : \
+ '--add-blocked=[add a bug to the blocked list]:bug: ' \
+ '--add-dependson=[add a bug to the depends list]:bug: ' \
+ '--add-cc=[add an email to CC list]:email: ' \
+ '(--assigned-to= -a)'{--assigned-to=,-a}'[assign bug to someone other than the default assignee]:assignee: ' \
+ '(--comment= -c)'{--comment=,-c}'[add comment to bug]:Comment: ' \
+ '(--comment-editor -C)'{--comment-editor,-C}'[add comment via default EDITOR]' \
+ '(--comment-from= -F)'{--comment-from=,-F}'[add comment from file]:file:_files' \
+ '(--duplicate= -d)'{--duplicate=,-d}'[mark bug as a duplicate of bug number]:bug: ' \
+ '--fixed[mark bug as RESOLVED, FIXED]' \
+ '--help[show help message and exit]' \
+ '--invalid[mark bug as RESOLVED, INVALID]' \
+ '(--keywords= -k)'{--keywords=,-k}'[list of bugzilla keywords]:keywords: ' \
+ '--priority=[set the priority field of the bug]:priority: ' \
+ '(--resolution= -r)'{--resolution=,-r}'[set new resolution (only if status = RESOLVED)]' \
+ '--remove-cc=[remove an email from the CC list]:email: ' \
+ '--remove-dependson=[remove a bug from the depends list]:bug: ' \
+ '--remove-blocked=[remove a bug from the blocked list]:bug: ' \
+ '(--severity= -S)'{--severity=,-S}'[set severity of the bug]:severity: ' \
+ '(--status -s=)'{--status=,-s}'[set new status of bug (eg. RESOLVED)]:status: ' \
+ '(--title= -t)'{--title=,-t}'[set title of the bug]:title: ' \
+ '(--url= -U)'{--url=,-u}'[set URL field of the bug]:URL: ' \
+ '(--whiteboard= -w)'{--whiteboard=,-w}'[set status whiteboard]:status whiteboard: '
+}
+
+(( ${+functions[_bugz_cmd_namedcmd]} )) ||
+_bugz_cmd_namedcmd()
+{
+ _arguments -s : \
+ '--show-status[show bug status]'
+ '--show-url[show bug ID as url]'
+}
+
+(( ${+functions[_bugz_cmd_post]} )) ||
+_bugz_cmd_post()
+{
+ _arguments -s : \
+ '(--assigned-to= -a)'{--assigned-to=,-a}'[assign bug to someone other than the default assignee]:assignee: ' \
+ '--batch[work in batch mode, non-interactively]' \
+ '--blocked[add a list of blocker bugs]:blockers: ' \
+ '--cc=[add a list of emails to cc list]:email(s): ' \
+ '--commenter[email of a commenter]:email: ' \
+ '--depends-on[add a list of bug dependencies]:dependencies: ' \
+ '(--description= -d)'{--description=,-d}'[description of the bug]:description: ' \
+ '(--description-from= -F)'{--description-from=,-f}'[description from contents of a file]:file:_files' \
+ '--help[show help message and exit]' \
+ '(--keywords= -k)'{--keywords=,-k}'[list of bugzilla keywords]:keywords: ' \
+ '(--append-command)--no-append-command[do not append command output]' \
+ '(--title= -t)'{--title=,-t}'[title of your bug]:title: ' \
+ '(--url= -U)'{--url=,-U}'[URL associated with the bug]:url: ' \
+ '--priority[priority of this bug]:priority: ' \
+ '--severity[severity of this bug]:severity: '
+}
+
+(( ${+functions[_bugz_cmd_search]} )) ||
+_bugz_cmd_search()
+{
+ # TODO --component,--status,--product,--priority can be specified multiple times
+ _arguments -s : \
+ '(--assigned-to= -a)'{--assigned-to=,-a}'[the email adress the bug is assigned to]:email: ' \
+ '--cc=[restrict by CC email address]:email: ' \
+ '(--comments -c)'{--comments,-c}'[search comments instead of title]:comment: ' \
+ '(--component= -C)'{--component=,-C}'[restrict by component]:component: ' \
+ '--help[show help message and exit]' \
+ '(--keywords= -k)'{--keywords=,-k}'[bug keywords]:keywords: ' \
+ '--severity=[restrict by severity]:severity: ' \
+ '--show-status[show bug status]' \
+ '--show-url[show bug ID as url]' \
+ '(--status= -s)'{--status=,-s}'[bug status]:status: ' \
+ '(--order= -o)'{--order=,-o}'[sort by]:order:((number\:"bug number" assignee\:"assignee field" importance\:"importance field" date\:"last changed"))' \
+ '--priority=[restrict by priority]:priority: ' \
+ '--product=[restrict by product]:product: ' \
+ '(--reporter= -r)'{--reporter=,-r}'[email of the reporter]:email: ' \
+ '(--whiteboard= -w)'{--whiteboard=,-w}'[status whiteboard]:status whiteboard: '
+}
+
+_bugz
+
diff --git a/third_party/pybugz-0.9.3/man/bugz.1 b/third_party/pybugz-0.9.3/man/bugz.1
new file mode 100644
index 0000000..628eae9
--- /dev/null
+++ b/third_party/pybugz-0.9.3/man/bugz.1
@@ -0,0 +1,41 @@
+.\" Hey, Emacs! This is an -*- nroff -*- source file.
+.\" Copyright (c) 2011 William Hubbs
+.\" This is free software; see the GNU General Public Licence version 2
+.\" or later for copying conditions. There is NO warranty.
+.TH bugz 1 "17 Feb 2011" "0.9.0"
+.nh
+.SH NAME
+bugz \(em command line interface to bugzilla
+.SH SYNOPSIS
+.B bugz
+[
+.B global options
+]
+.B subcommand
+[
+.B subcommand options
+]
+.\" .SH OPTIONS
+.\" .TP
+.\" .B \-o value, \-\^\-long=value
+.\" Describe the option.
+.SH DESCRIPTION
+Bugz is a cprogram which gives you access to the features of the
+bugzilla bug tracking system from the command line.
+.PP
+This man page is a stub; the bugs program has extensive built in help.
+.B bugz -h
+will show the help for the global options and
+.B bugz [subcommand] -h
+will show the help for a specific subcommand.
+.SH BUGS
+.PP
+The home page of this project is http://www.github.com/williamh/pybugz.
+Bugs should be reported to the bug tracker there.
+.\" .SH SEE ALSO
+.\" .PP
+.SH AUTHOR
+.PP
+The original author is Alastair Tse <alastair@liquidx.net>.
+The current maintainer is William Hubbs <w.d.hubbs@gmail.com>. William
+also wrote this man page.
diff --git a/third_party/pybugz-0.9.3/setup.py b/third_party/pybugz-0.9.3/setup.py
new file mode 100644
index 0000000..9a51e44
--- /dev/null
+++ b/third_party/pybugz-0.9.3/setup.py
@@ -0,0 +1,15 @@
+from bugz import __version__
+from distutils.core import setup
+
+setup(
+ name = 'pybugz',
+ version = __version__,
+ description = 'python interface to bugzilla',
+ author = 'Alastair Tse',
+ author_email = 'alastair@liquidx.net',
+ url = 'http://www.liquidx.net/pybuggz',
+ license = "GPL-2",
+ platforms = ['any'],
+ packages = ['bugz'],
+ scripts = ['bin/bugz'],
+)