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