public inbox for gentoo-commits@lists.gentoo.org
 help / color / mirror / Atom feed
* [gentoo-commits] proj/arch-tools:new-pybugz commit in: third_party/pybugz-0.9.3/man/, /, third_party/pybugz-0.9.3/contrib/, ...
  2012-10-08 16:03 [gentoo-commits] proj/arch-tools:master commit in: third_party/pybugz-0.9.3/man/, /, third_party/pybugz-0.9.3/contrib/, Paweł Hajdan
@ 2012-05-30 14:35 ` Paweł Hajdan
  0 siblings, 0 replies; 2+ messages in thread
From: Paweł Hajdan @ 2012-05-30 14:35 UTC (permalink / raw
  To: gentoo-commits

commit:     520c9782541d2e3fa509b1a2d470889a6d26bef7
Author:     Pawel Hajdan, Jr <phajdan.jr <AT> gentoo <DOT> org>
AuthorDate: Wed May 30 14:34:40 2012 +0000
Commit:     Paweł Hajdan <phajdan.jr <AT> gentoo <DOT> org>
CommitDate: Wed May 30 14:34:40 2012 +0000
URL:        http://git.overlays.gentoo.org/gitweb/?p=proj/arch-tools.git;a=commit;h=520c9782

Make bugzilla-viewer and maintainer-timeout work

by bundling old pybugz.

---
 bugzilla-viewer.py                               |    2 +
 maintainer-timeout.py                            |    4 +
 stabilization-candidates.py                      |   35 +-
 third_party/pybugz-0.9.3/LICENSE                 |  340 +++++++++
 third_party/pybugz-0.9.3/README                  |  107 +++
 third_party/pybugz-0.9.3/bin/bugz                |  393 ++++++++++
 third_party/pybugz-0.9.3/bugz/__init__.py        |   31 +
 third_party/pybugz-0.9.3/bugz/bugzilla.py        |  862 ++++++++++++++++++++++
 third_party/pybugz-0.9.3/bugz/cli.py             |  607 +++++++++++++++
 third_party/pybugz-0.9.3/bugz/config.py          |  229 ++++++
 third_party/pybugz-0.9.3/bugzrc.example          |   25 +
 third_party/pybugz-0.9.3/contrib/bash-completion |   66 ++
 third_party/pybugz-0.9.3/contrib/zsh-completion  |  158 ++++
 third_party/pybugz-0.9.3/man/bugz.1              |   41 +
 third_party/pybugz-0.9.3/setup.py                |   15 +
 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.
+\f
+		    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.)
+\f
+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.
+\f
+  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.
+\f
+  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
+\f
+	    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'],
+)



^ permalink raw reply related	[flat|nested] 2+ messages in thread

* [gentoo-commits] proj/arch-tools:master commit in: third_party/pybugz-0.9.3/man/, /, third_party/pybugz-0.9.3/contrib/, ...
@ 2012-10-08 16:03 Paweł Hajdan
  2012-05-30 14:35 ` [gentoo-commits] proj/arch-tools:new-pybugz " Paweł Hajdan
  0 siblings, 1 reply; 2+ messages in thread
From: Paweł Hajdan @ 2012-10-08 16:03 UTC (permalink / raw
  To: gentoo-commits

commit:     520c9782541d2e3fa509b1a2d470889a6d26bef7
Author:     Pawel Hajdan, Jr <phajdan.jr <AT> gentoo <DOT> org>
AuthorDate: Wed May 30 14:34:40 2012 +0000
Commit:     Paweł Hajdan <phajdan.jr <AT> gentoo <DOT> org>
CommitDate: Wed May 30 14:34:40 2012 +0000
URL:        http://git.overlays.gentoo.org/gitweb/?p=proj/arch-tools.git;a=commit;h=520c9782

Make bugzilla-viewer and maintainer-timeout work

by bundling old pybugz.

---
 bugzilla-viewer.py                               |    2 +
 maintainer-timeout.py                            |    4 +
 stabilization-candidates.py                      |   35 +-
 third_party/pybugz-0.9.3/LICENSE                 |  340 +++++++++
 third_party/pybugz-0.9.3/README                  |  107 +++
 third_party/pybugz-0.9.3/bin/bugz                |  393 ++++++++++
 third_party/pybugz-0.9.3/bugz/__init__.py        |   31 +
 third_party/pybugz-0.9.3/bugz/bugzilla.py        |  862 ++++++++++++++++++++++
 third_party/pybugz-0.9.3/bugz/cli.py             |  607 +++++++++++++++
 third_party/pybugz-0.9.3/bugz/config.py          |  229 ++++++
 third_party/pybugz-0.9.3/bugzrc.example          |   25 +
 third_party/pybugz-0.9.3/contrib/bash-completion |   66 ++
 third_party/pybugz-0.9.3/contrib/zsh-completion  |  158 ++++
 third_party/pybugz-0.9.3/man/bugz.1              |   41 +
 third_party/pybugz-0.9.3/setup.py                |   15 +
 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.
+\f
+		    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.)
+\f
+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.
+\f
+  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.
+\f
+  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
+\f
+	    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'],
+)


^ permalink raw reply related	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2012-10-08 16:04 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2012-10-08 16:03 [gentoo-commits] proj/arch-tools:master commit in: third_party/pybugz-0.9.3/man/, /, third_party/pybugz-0.9.3/contrib/, Paweł Hajdan
2012-05-30 14:35 ` [gentoo-commits] proj/arch-tools:new-pybugz " Paweł Hajdan

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox