pax_global_header00006660000000000000000000000064117515604170014521gustar00rootroot0000000000000052 comment=d540d47e43b6fce3c58005847e5fad44214903b5 williamh-pybugz-74a57cb/000077500000000000000000000000001175156041700152635ustar00rootroot00000000000000williamh-pybugz-74a57cb/LICENSE000066400000000000000000000431331175156041700162740ustar00rootroot00000000000000 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. Copyright (C) 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. , 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. williamh-pybugz-74a57cb/README000066400000000000000000000062301175156041700161440ustar00rootroot00000000000000PyBugz - 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 . 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. 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-to liquidx@gentoo.org * Using https://bugs.gentoo.org/xmlrpc.cgi * Searching for "version bump" 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 https://bugs.gentoo.org/xmlrpc.cgi * 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 https://bugs.gentoo.org/xmlrpc.cgi * 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 ------------- bugz has an extensive help system for commands and options. bugz --help gives help for the global options and subcommands, and bugz --help gives help for that specific subcommand. 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. williamh-pybugz-74a57cb/bin/000077500000000000000000000000001175156041700160335ustar00rootroot00000000000000williamh-pybugz-74a57cb/bin/bugz000077500000000000000000000021541175156041700167320ustar00rootroot00000000000000#!/usr/bin/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 ------- - BugzillaProxy - Server proxy for communication with Bugzilla - PrettyBugz - Command line interface to Bugzilla """ import locale import os import sys import traceback from bugz.argparsers import make_parser from bugz.cli import BugzError, PrettyBugz from bugz.configfile import get_config def main(): parser = make_parser() # parse options args = parser.parse_args() get_config(args) if getattr(args, 'columns') is None: setattr(args, 'columns', 0) try: bugz = PrettyBugz(args) args.func(bugz, args) 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() williamh-pybugz-74a57cb/bugz/000077500000000000000000000000001175156041700162325ustar00rootroot00000000000000williamh-pybugz-74a57cb/bugz/__init__.py000066400000000000000000000006051175156041700203440ustar00rootroot00000000000000__version__ = '0.10.1' __author__ = 'Alastair Tse ' __contributors__ = ['Santiago M. Mola ', 'William Hubbs ', 'Mike Gilbert '] __revision__ = '$Id: $' __license__ = """Copyright (c) 2006, Alastair Tse, All rights reserved. This following source code is licensed under the GPL v2 License.""" williamh-pybugz-74a57cb/bugz/argparsers.py000066400000000000000000000254541175156041700207670ustar00rootroot00000000000000import argparse from bugz import __version__ from bugz.cli import PrettyBugz 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', help = 'mimetype of the file e.g. text/plain (default: auto-detect)') attach_parser.add_argument('-d', '--description', help = 'a long description of the attachment', dest = 'comment') attach_parser.add_argument('-p', '--patch', action = 'store_true', help = 'attachment is a patch', dest = 'is_patch') attach_parser.add_argument('-t', '--title', help = 'a short description of the attachment (default: filename).', dest = 'summary') 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_login_parser(subparsers): login_parser = subparsers.add_parser('login', help = 'log into bugzilla') login_parser.set_defaults(func = PrettyBugz.login) def make_logout_parser(subparsers): logout_parser = subparsers.add_parser('logout', help = 'log out of bugzilla') logout_parser.set_defaults(func = PrettyBugz.logout) 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('--alias', help = 'change the alias for this bug') modify_parser.add_argument('-a', '--assigned-to', help = 'change assignee for this bug') modify_parser.add_argument('--add-blocked', action = 'append', help = 'add a bug to the blocked list', dest = 'blocks_add') modify_parser.add_argument('--remove-blocked', action = 'append', help = 'remove a bug from the blocked list', dest = 'blocks_remove') modify_parser.add_argument('--add-dependson', action = 'append', help = 'add a bug to the depends list', dest = 'depends_on_add') modify_parser.add_argument('--remove-dependson', action = 'append', help = 'remove a bug from the depends list', dest = 'depends_on_remove') modify_parser.add_argument('--add-cc', action = 'append', help = 'add an email to the CC list', dest = 'cc_add') modify_parser.add_argument('--remove-cc', action = 'append', help = 'remove an email from the CC list', dest = 'cc_remove') modify_parser.add_argument('-c', '--comment', help = 'add comment from command line') 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('--component', help = 'change the component for this bug') modify_parser.add_argument('-d', '--duplicate', type = int, default = 0, help = 'this bug is a duplicate', dest = 'dupe_of') modify_parser.add_argument('--add-group', action = 'append', help = 'add a group to this bug', dest = 'groups_add') modify_parser.add_argument('--remove-group', action = 'append', help = 'remove agroup from this bug', dest = 'groups_remove') modify_parser.add_argument('--set-keywords', action = 'append', help = 'set bug keywords', dest = 'keywords_set') modify_parser.add_argument('--op-sys', help = 'change the operating system for this bug') modify_parser.add_argument('--platform', help = 'change the hardware platform for this bug') modify_parser.add_argument('--priority', help = 'change the priority for this bug') modify_parser.add_argument('--product', help = 'change the product for this bug') modify_parser.add_argument('-r', '--resolution', help = 'set new resolution (only if status = RESOLVED)') modify_parser.add_argument('--add-see-also', action = 'append', help = 'add a "see also" URL to this bug', dest = 'see_also_add') modify_parser.add_argument('--remove-see-also', action = 'append', help = 'remove a"see also" URL from this bug', dest = 'see_also_remove') modify_parser.add_argument('-S', '--severity', help = 'set severity for this bug') modify_parser.add_argument('-s', '--status', help = 'set new status of bug (eg. RESOLVED)') modify_parser.add_argument('-t', '--title', help = 'set title of bug', dest = 'summary') modify_parser.add_argument('-U', '--url', help = 'set URL field of bug') modify_parser.add_argument('-v', '--version', help = 'set the version for this bug'), modify_parser.add_argument('-w', '--whiteboard', help = 'set Status whiteboard'), 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_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('--version', help = 'version of the product') post_parser.add_argument('-t', '--title', help = 'title of bug', dest = 'summary') post_parser.add_argument('-d', '--description', help = 'description of the bug') post_parser.add_argument('--op-sys', help = 'set the operating system') post_parser.add_argument('--platform', help = 'set the hardware platform') post_parser.add_argument('--priority', help = 'set priority for the new bug') post_parser.add_argument('-S', '--severity', help = 'set the severity for the new bug') post_parser.add_argument('--alias', help = 'set the alias for this bug') 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('-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('--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.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 and/or body') search_parser.add_argument('--alias', help='The unique alias for this bug') search_parser.add_argument('-a', '--assigned-to', help = 'email the bug is assigned to') search_parser.add_argument('-C', '--component', action='append', help = 'restrict by component (1 or more)') search_parser.add_argument('-r', '--creator', help = 'email of the person who created the bug') search_parser.add_argument('-l', '--limit', type = int, help='Limit the number of records returned in a search') search_parser.add_argument('--offset', type = int, help='Set the start position for a search') search_parser.add_argument('--op-sys', action='append', help = 'restrict by Operating System (one or more)') search_parser.add_argument('--platform', action='append', help = 'restrict by platform (one or more)') search_parser.add_argument('--priority', action='append', help = 'restrict by priority (one or more)') search_parser.add_argument('--product', action='append', help = 'restrict by product (one or more)') search_parser.add_argument('--resolution', help = 'restrict by resolution') search_parser.add_argument('--severity', action='append', help = 'restrict by severity (one or more)') search_parser.add_argument('-s', '--status', action='append', help = 'restrict by status (one or more, use all for all statuses)') search_parser.add_argument('-v', '--version', action='append', help = 'restrict by version (one or more)') 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.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', default = 'https://bugs.gentoo.org/xmlrpc.cgi', 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('--passwordcmd', help = 'password command to evaluate for commands requiring authentication') 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_login_parser(subparsers) make_logout_parser(subparsers) make_modify_parser(subparsers) make_post_parser(subparsers) make_search_parser(subparsers) return parser williamh-pybugz-74a57cb/bugz/bugzilla.py000066400000000000000000000047111175156041700204200ustar00rootroot00000000000000# Author: Mike Gilbert # This code is released into the public domain. # As of this writing, the Bugzilla web service is documented at the # following URL: # http://www.bugzilla.org/docs/4.2/en/html/api/Bugzilla/WebService.html from cookielib import CookieJar from urllib import splittype, splithost, splituser, splitpasswd from urllib2 import build_opener, HTTPBasicAuthHandler, HTTPCookieProcessor from urllib2 import HTTPPasswordMgrWithDefaultRealm, Request from xmlrpclib import ProtocolError, ServerProxy, Transport class RequestTransport(Transport): def __init__(self, uri, cookiejar=None, use_datetime=0): Transport.__init__(self, use_datetime=use_datetime) self.opener = build_opener() # Parse auth (user:passwd) from the uri urltype, rest = splittype(uri) host, rest = splithost(rest) auth, host = splituser(host) self.uri = urltype + '://' + host + rest # Handle HTTP Basic authentication if auth is not None: user, passwd = splitpasswd(auth) passwdmgr = HTTPPasswordMgrWithDefaultRealm() passwdmgr.add_password(realm=None, uri=self.uri, user=user, passwd=passwd) authhandler = HTTPBasicAuthHandler(passwdmgr) self.opener.add_handler(authhandler) # Handle HTTP Cookies if cookiejar is not None: self.opener.add_handler(HTTPCookieProcessor(cookiejar)) def request(self, host, handler, request_body, verbose=0): req = Request(self.uri) req.add_header('User-Agent', self.user_agent) req.add_header('Content-Type', 'text/xml') if hasattr(self, 'accept_gzip_encoding') and self.accept_gzip_encoding: req.add_header('Accept-Encoding', 'gzip') req.add_data(request_body) resp = self.opener.open(req) # In Python 2, resp is a urllib.addinfourl instance, which does not # have the getheader method that parse_response expects. if not hasattr(resp, 'getheader'): resp.getheader = resp.headers.getheader if resp.code == 200: self.verbose = verbose return self.parse_response(resp) resp.close() raise ProtocolError(self.uri, resp.status, resp.reason, resp.msg) class BugzillaProxy(ServerProxy): def __init__(self, uri, encoding=None, verbose=0, allow_none=0, use_datetime=0, cookiejar=None): if cookiejar is None: cookiejar = CookieJar() transport = RequestTransport(use_datetime=use_datetime, uri=uri, cookiejar=cookiejar) ServerProxy.__init__(self, uri=uri, transport=transport, encoding=encoding, verbose=verbose, allow_none=allow_none, use_datetime=use_datetime) williamh-pybugz-74a57cb/bugz/cli.py000066400000000000000000000523641175156041700173650ustar00rootroot00000000000000#!/usr/bin/env python import commands import getpass from cookielib import CookieJar, LWPCookieJar import locale import mimetypes import os import subprocess import re import sys import tempfile import textwrap import xmlrpclib try: import readline except ImportError: readline = None from bugz.bugzilla import BugzillaProxy BUGZ_COMMENT_TEMPLATE = \ """ BUGZ: --------------------------------------------------- %s BUGZ: Any line beginning with 'BUGZ:' will be ignored. BUGZ: --------------------------------------------------- """ DEFAULT_COOKIE_FILE = '.bugz_cookie' DEFAULT_NUM_COLS = 80 # # Auxiliary functions # def get_content_type(filename): return mimetypes.guess_type(filename)[0] or 'application/octet-stream' 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: def __init__(self, args): self.quiet = args.quiet self.columns = args.columns or terminal_width() self.user = args.user self.password = args.password self.passwordcmd = args.passwordcmd self.skip_auth = args.skip_auth cookie_file = os.path.join(os.environ['HOME'], DEFAULT_COOKIE_FILE) self.cookiejar = LWPCookieJar(cookie_file) try: self.cookiejar.load() except IOError: pass if getattr(args, 'encoding'): self.enc = args.encoding else: try: self.enc = locale.getdefaultlocale()[1] except: self.enc = 'utf-8' if not self.enc: self.enc = 'utf-8' self.log("Using %s " % args.base) self.bz = BugzillaProxy(args.base, cookiejar=self.cookiejar) 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 bzcall(self, method, *args): """Attempt to call method with args. Log in if authentication is required. """ try: return method(*args) except xmlrpclib.Fault, fault: # Fault code 410 means login required if fault.faultCode == 410 and not self.skip_auth: self.login() return method(*args) raise def login(self, args=None): """Authenticate a session. """ # 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: if not self.passwordcmd: self.log('No password given.') self.password = getpass.getpass() else: process = subprocess.Popen(self.passwordcmd.split(), shell=False, stdout=subprocess.PIPE) self.password, _ = process.communicate() # perform login params = {} params['login'] = self.user params['password'] = self.password if args is not None: params['remember'] = True self.log('Logging in') self.bz.User.login(params) if args is not None: self.cookiejar.save() os.chmod(self.cookiejar.filename, 0600) def logout(self, args): self.log('logging out') self.bz.User.logout() def search(self, args): """Performs a search on the bugzilla database with the keywords given on the title (or the body if specified). """ valid_keys = ['alias', 'assigned_to', 'component', 'creator', 'limit', 'offset', 'op_sys', 'platform', 'priority', 'product', 'resolution', 'severity', 'status', 'version', 'whiteboard'] search_opts = sorted([(opt, val) for opt, val in args.__dict__.items() if val is not None and opt in valid_keys]) params = {} for key in args.__dict__.keys(): if key in valid_keys and getattr(args, key) is not None: params[key] = getattr(args, key) if getattr(args, 'terms'): params['summary'] = args.terms search_term = ' '.join(args.terms).strip() if not (params or search_term): 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) if not 'status' in params.keys(): params['status'] = ['CONFIRMED', 'IN_PROGRESS', 'UNCONFIRMED'] elif 'ALL' in params['status']: del params['status'] result = self.bzcall(self.bz.Bug.search, params)['bugs'] if not len(result): self.log('No bugs found.') else: self.listbugs(result, args.show_status) def get(self, args): """ Fetch bug details given the bug id """ self.log('Getting bug %s ..' % args.bugid) result = self.bzcall(self.bz.Bug.get, {'ids':[args.bugid]}) for bug in result['bugs']: self.showbuginfo(bug, args.attachments, args.comments) def post(self, args): """Post a new bug""" # load description from file if possible if args.description_from is not None: try: if args.description_from == '-': args.description = sys.stdin.read() else: args.description = open( args.description_from, 'r').read() except IOError, e: raise BugzError('Unable to read from file: %s: %s' % (args.description_from, e)) if not args.batch: self.log('Press Ctrl+C at any time to abort.') # # Check all bug fields. # XXX: We use "if not " for mandatory fields # and "if is None" for optional ones. # # check for product if not args.product: while not args.product or len(args.product) < 1: args.product = self.get_input('Enter product: ') else: self.log('Enter product: %s' % args.product) # check for component if not args.component: while not args.component or len(args.component) < 1: args.component = self.get_input('Enter component: ') else: self.log('Enter component: %s' % args.component) # check for version # FIXME: This default behaviour is not too nice. if not args.version: line = self.get_input('Enter version (default: unspecified): ') if len(line): args.version = line else: args.version = 'unspecified' else: self.log('Enter version: %s' % args.version) # check for title if not args.summary: while not args.summary or len(args.summary) < 1: args.summary = self.get_input('Enter title: ') else: self.log('Enter title: %s' % args.summary) # check for description if not args.description: line = block_edit('Enter bug description: ') if len(line): args.description = line else: self.log('Enter bug description: %s' % args.description) # check for operating system if not args.op_sys: op_sys_msg = 'Enter operating system where this bug occurs: ' line = self.get_input(op_sys_msg) if len(line): args.op_sys = line else: self.log('Enter operating system: %s' % args.op_sys) # check for platform if not args.platform: platform_msg = 'Enter hardware platform where this bug occurs: ' line = self.get_input(platform_msg) if len(line): args.platform = line else: self.log('Enter hardware platform: %s' % args.platform) # check for default priority if args.priority is None: priority_msg ='Enter priority (eg. Normal) (optional): ' line = self.get_input(priority_msg) if len(line): args.priority = line else: self.log('Enter priority (optional): %s' % args.priority) # check for default severity if args.severity is None: severity_msg ='Enter severity (eg. normal) (optional): ' line = self.get_input(severity_msg) if len(line): args.severity = line else: self.log('Enter severity (optional): %s' % args.severity) # check for default alias if args.alias is None: alias_msg ='Enter an alias for this bug (optional): ' line = self.get_input(alias_msg) if len(line): args.alias = line else: self.log('Enter alias (optional): %s' % args.alias) # check for default assignee if args.assigned_to is None: assign_msg ='Enter assignee (eg. liquidx@gentoo.org) (optional): ' line = self.get_input(assign_msg) if len(line): args.assigned_to = line else: self.log('Enter assignee (optional): %s' % args.assigned_to) # check for CC list if args.cc is None: cc_msg = 'Enter a CC list (comma separated) (optional): ' line = self.get_input(cc_msg) if len(line): args.cc = line.split(', ') else: self.log('Enter a CC list (optional): %s' % args.cc) # fixme: groups # fixme: status # fixme: milestone if args.append_command is None: args.append_command = self.get_input('Append the output of the following command (leave blank for none): ') else: self.log('Append command (optional): %s' % args.append_command) # raise an exception if mandatory fields are not specified. if args.product is None: raise RuntimeError('Product not specified') if args.component is None: raise RuntimeError('Component not specified') if args.summary is None: raise RuntimeError('Title not specified') if args.description is None: raise RuntimeError('Description not specified') if not args.version: args.version = 'unspecified' # append the output from append_command to the description if args.append_command is not None and args.append_command != '': append_command_output = commands.getoutput(args.append_command) args.description = args.description + '\n\n' + '$ ' + args.append_command + '\n' + append_command_output # print submission confirmation print '-' * (self.columns - 1) print '%-12s: %s' % ('Product', args.product) print '%-12s: %s' %('Component', args.component) print '%-12s: %s' % ('Title', args.summary) print '%-12s: %s' % ('Version', args.version) print '%-12s: %s' % ('Description', args.description) print '%-12s: %s' % ('Operating System', args.op_sys) print '%-12s: %s' % ('Platform', args.platform) print '%-12s: %s' % ('priority', args.priority) print '%-12s: %s' % ('severity', args.severity) print '%-12s: %s' % ('alias', args.alias) print '%-12s: %s' % ('Assigned to', args.assigned_to) print '%-12s: %s' % ('CC', args.cc) # fixme: groups # fixme: status # fixme: Milestone print '-' * (self.columns - 1) if not args.batch: if args.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 = args.default_confirm if confirm[0] not in ('y', 'Y'): self.log('Submission aborted') return params={} params['product'] = args.product params['component'] = args.component params['version'] = args.version params['summary'] = args.summary if args.description is not None: params['description'] = args.description if args.op_sys is not None: params['op_sys'] = args.op_sys if args.platform is not None: params['platform'] = args.platform if args.priority is not None: params['priority'] = args.priority if args.severity is not None: params['severity'] = args.severity if args.alias is not None: params['alias'] = args.alias if args.assigned_to is not None: params['assigned_to'] = args.assigned_to if args.cc is not None: params['cc'] = args.cc result = self.bzcall(self.bz.Bug.create, params) self.log('Bug %d submitted' % result['id']) def modify(self, args): """Modify an existing bug (eg. adding a comment or changing resolution.)""" if args.comment_from: try: if args.comment_from == '-': args.comment = sys.stdin.read() else: args.comment = open(args.comment_from, 'r').read() except IOError, e: raise BugzError('unable to read file: %s: %s' % \ (args.comment_from, e)) if args.comment_editor: args.comment = block_edit('Enter comment:') params = {} if args.blocks_add is not None or args.blocks_remove is not None: params['blocks'] = {} if args.depends_on_add is not None \ or args.depends_on_remove is not None: params['depends_on'] = {} if args.cc_add is not None or args.cc_remove is not None: params['cc'] = {} if args.comment is not None: params['comment'] = {} if args.groups_add is not None or args.groups_remove is not None: params['groups'] = {} if args.keywords_set is not None: params['keywords'] = {} if args.see_also_add is not None or args.see_also_remove is not None: params['see_also'] = {} params['ids'] = [args.bugid] if args.alias is not None: params['alias'] = args.alias if args.assigned_to is not None: params['assigned_to'] = args.assigned_to if args.blocks_add is not None: params['blocks']['add'] = args.blocks_add if args.blocks_remove is not None: params['blocks']['remove'] = args.blocks_remove if args.depends_on_add is not None: params['depends_on']['add'] = args.depends_on_add if args.depends_on_remove is not None: params['depends_on']['remove'] = args.depends_on_remove if args.cc_add is not None: params['cc']['add'] = args.cc_add if args.cc_remove is not None: params['cc']['remove'] = args.cc_remove if args.comment is not None: params['comment']['body'] = args.comment if args.component is not None: params['component'] = args.component if args.dupe_of: params['dupe_of'] = args.dupe_of args.status = None args.resolution = None if args.groups_add is not None: params['groups']['add'] = args.groups_add if args.groups_remove is not None: params['groups']['remove'] = args.groups_remove if args.keywords_set is not None: params['keywords']['set'] = args.keywords_set if args.op_sys is not None: params['op_sys'] = args.op_sys if args.platform is not None: params['platform'] = args.platform if args.priority is not None: params['priority'] = args.priority if args.product is not None: params['product'] = args.product if args.resolution is not None: params['resolution'] = args.resolution if args.see_also_add is not None: params['see_also']['add'] = args.see_also_add if args.see_also_remove is not None: params['see_also']['remove'] = args.see_also_remove if args.severity is not None: params['severity'] = args.severity if args.status is not None: params['status'] = args.status if args.summary is not None: params['summary'] = args.summary if args.url is not None: params['url'] = args.url if args.version is not None: params['version'] = args.version if args.whiteboard is not None: params['whiteboard'] = args.whiteboard if args.fixed: params['status'] = 'RESOLVED' params['resolution'] = 'FIXED' if args.invalid: params['status'] = 'RESOLVED' params['resolution'] = 'INVALID' if len(params) < 2: raise BugzError('No changes were specified') result = self.bzcall(self.bz.Bug.update, params) for bug in result['bugs']: changes = bug['changes'] if not len(changes): self.log('Added comment to bug %s' % bug['id']) else: self.log('Modified the following fields in bug %s' % bug['id']) for key in changes.keys(): self.log('%-12s: removed %s' %(key, changes[key]['removed'])) self.log('%-12s: added %s' %(key, changes[key]['added'])) def attachment(self, args): """ Download or view an attachment given the id.""" self.log('Getting attachment %s' % args.attachid) params = {} params['attachment_ids'] = [args.attachid] result = self.bzcall(self.bz.Bug.attachments, params) result = result['attachments'][args.attachid] action = {True:'Viewing', False:'Saving'} self.log('%s attachment: "%s"' % (action[args.view], result['file_name'])) safe_filename = os.path.basename(re.sub(r'\.\.', '', result['file_name'])) if args.view: print result['data'].data else: if os.path.exists(result['file_name']): raise RuntimeError('Filename already exists') fd = open(safe_filename, 'wb') fd.write(result['data'].data) fd.close() def attach(self, args): """ Attach a file to a bug given a filename. """ filename = args.filename content_type = args.content_type bugid = args.bugid summary = args.summary is_patch = args.is_patch comment = args.comment if not os.path.exists(filename): raise BugzError('File not found: %s' % filename) if content_type is None: content_type = get_content_type(filename) if comment is None: comment = block_edit('Enter optional long description of attachment') if summary is None: summary = os.path.basename(filename) params = {} params['ids'] = [bugid] fd = open(filename, 'rb') params['data'] = xmlrpclib.Binary(fd.read()) fd.close() params['file_name'] = os.path.basename(filename) params['summary'] = summary if not is_patch: params['content_type'] = content_type; params['comment'] = comment params['is_patch'] = is_patch result = self.bzcall(self.bz.Bug.add_attachment, params) self.log("'%s' has been attached to bug %s" % (filename, bugid)) def listbugs(self, buglist, show_status=False): for bug in buglist: bugid = bug['id'] status = bug['status'] assignee = bug['assigned_to'].split('@')[0] desc = bug['summary'] line = '%s' % (bugid) if show_status: line = '%s %s' % (line, status) 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)) def showbuginfo(self, bug, show_attachments, show_comments): FIELDS = ( ('summary', 'Title'), ('assigned_to', 'Assignee'), ('creation_time', 'Reported'), ('last_change_time', 'Updated'), ('status', 'Status'), ('resolution', 'Resolution'), ('url', 'URL'), ('severity', 'Severity'), ('priority', 'Priority'), ('creator', 'Reporter'), ) MORE_FIELDS = ( ('product', 'Product'), ('component', 'Component'), ('whiteboard', 'Whiteboard'), ) for field, name in FIELDS + MORE_FIELDS: try: value = bug[field] if value is None or value == '': continue except AttributeError: continue print '%-12s: %s' % (name, value) # print keywords k = ', '.join(bug['keywords']) if k: print '%-12s: %s' % ('Keywords', k) # Print out the cc'ed people cced = bug['cc'] for cc in cced: print '%-12s: %s' % ('CC', cc) # print out depends dependson = ', '.join(["%s" % x for x in bug['depends_on']]) if dependson: print '%-12s: %s' % ('DependsOn', dependson) blocked = ', '.join(["%s" % x for x in bug['blocks']]) if blocked: print '%-12s: %s' % ('Blocked', blocked) bug_comments = self.bzcall(self.bz.Bug.comments, {'ids':[bug['id']]}) bug_comments = bug_comments['bugs']['%s' % bug['id']]['comments'] print '%-12s: %d' % ('Comments', len(bug_comments)) bug_attachments = self.bzcall(self.bz.Bug.attachments, {'ids':[bug['id']]}) bug_attachments = bug_attachments['bugs']['%s' % bug['id']] print '%-12s: %d' % ('Attachments', len(bug_attachments)) print if show_attachments: for attachment in bug_attachments: aid = attachment['id'] desc = attachment['summary'] when = attachment['creation_time'] print '[Attachment] [%s] [%s]' % (aid, desc.encode(self.enc)) if show_comments: i = 0 wrapper = textwrap.TextWrapper(width = self.columns, break_long_words = False, break_on_hyphens = False) for comment in bug_comments: who = comment['creator'] when = comment['time'] what = comment['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 williamh-pybugz-74a57cb/bugz/configfile.py000066400000000000000000000042331175156041700207130ustar00rootroot00000000000000import ConfigParser import os import sys DEFAULT_CONFIG_FILE = '~/.bugzrc' 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, e: print " ! Error: option "+option+" is not in the right format: "+str(e) sys.exit(1) def fill_config_option(args, parser, get, section, option): value = config_option(parser, get, section, option) if value is not None: setattr(args, option, value) def fill_config(args, parser, section): fill_config_option(args, parser, parser.get, section, 'base') fill_config_option(args, parser, parser.get, section, 'user') fill_config_option(args, parser, parser.get, section, 'password') fill_config_option(args, parser, parser.get, section, 'passwordcmd') fill_config_option(args, parser, parser.getint, section, 'columns') fill_config_option(args, parser, parser.get, section, 'encoding') fill_config_option(args, parser, parser.getboolean, section, 'quiet') def get_config(args): config_file = getattr(args, 'config_file') if config_file is None: config_file = DEFAULT_CONFIG_FILE 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 # try to parse config file try: parser.readfp(file) sections = parser.sections() except ConfigParser.ParsingError, e: print " ! Error: Can't parse user configuration file: "+str(e) sys.exit(1) # parse the default section first if "default" in sections: fill_config(args, parser, "default") if section is None: section = config_option(parser, parser.get, "default", "connection") # parse a specific section if section in sections: fill_config(args, parser, section) elif section is not None: print " ! Error: Can't find section ["+section+"] in configuration file" sys.exit(1) williamh-pybugz-74a57cb/bugzrc.example000066400000000000000000000035141175156041700201370ustar00rootroot00000000000000# # 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] # # The base url of the bugzilla you wish to use. # This must point to the xmlrpc.cgi script on the bugzilla installation. # # base: http://my.project.com/bugzilla/xmlrpc.cgi # # It is also possible to encode a username and password into this URL # for basic http authentication as follows: # # base: http://myhttpname:myhttppasswd@my.project.com/bugzilla/xmlrpc.cgi # # Next are your username and password for this bugzilla. If you do not # provide these, you will be prompted for them. # # user: myname@my.project.com # password: secret2 # # As an alternative to keeping your password in this file you can provide a # password command. It is evaluated and pybugz expects this command to output # the password to standard out. E.g.: # # passwordcmd: gpg2 --decrypt /myhome/.my-encrypted-password.gpg # # The number of columns your terminal can display. # Most of the time you should not have to set this. # # columns: 80 # # Set the output encoding for pybugz. # # encoding: utf-8 # # Run in quiet mode. # # quiet: True # # The special section named 'default' may also be used. Other sections will # override any values specified here. The optional special key 'connection' is # used to name the default connection, to use when no --connection parameter is # specified to the bugz command. # # [default] # connection: sectionname # # All parameters listed above can be used in the default section if you # only use one bugzilla installation. williamh-pybugz-74a57cb/contrib/000077500000000000000000000000001175156041700167235ustar00rootroot00000000000000williamh-pybugz-74a57cb/contrib/bash-completion000066400000000000000000000034571175156041700217430ustar00rootroot00000000000000# # Bash completion support for bugz. # # Copyright 2012 Tim Stoakes # # Complete a list of command names by scanning the help output. __bugz_comp_commands_from_help () { local cur="${3#$2}" local opts opts="$($1 --help | \ awk -F, '/^\s+\{/ {for(i=1; i<=NF; ++i) {sub(/\W+/, "", $i); print $i} exit;}')" COMPREPLY=( $( compgen -W "$opts" -- $cur ) ) } # Complete a list of options, both generic, and specific to the given command, # by scanning the help output. __bugz_comp_opts_from_help () { local cur="${3#$2}" local opts if [ "x" == "x$2" ]; then opts="$($1 --help | \ awk '/^\s+-/ {for(i=1; i<=NF; ++i) if ($i~/^-.+$/) {sub(/,$/, "", $i); print $i}}')" COMPREPLY=( $( compgen -W "$opts" -- $cur ) ) else opts="$($1 $2 --help 2>/dev/null | \ awk '/^\s+-/ {for(i=1; i<=NF; ++i) if ($i~/^-.+$/) {sub(/,$/, "", $i); print $i}}')" if [ $? -eq 0 ]; then COMPREPLY=( $( compgen -W "$opts" -- $cur ) ) fi fi } _bugz() { local i c=1 command cur command_valid=0 bugz COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" bugz="${COMP_WORDS[0]}" # Find the command name. while [ $c -le $COMP_CWORD ]; do i="${COMP_WORDS[c]}" case "$i" in -*) ;; *) # This is the best guess so far. command="$i" # Is it a valid command already? $bugz $i --help > /dev/null 2>&1 if [ $? -eq 0 ]; then # Yes! Stop looking. command_valid=1 break 2 fi ;; esac c=$((++c)) done case "$cur" in -*) # Complete an option. __bugz_comp_opts_from_help "$bugz" "$command" "$cur" return 0 ;; *) if [ $command_valid -ne 1 ]; then # Complete the command name if required. __bugz_comp_commands_from_help "$bugz" "" $command fi # If there's anything left, don't complete it. return 0 ;; esac } complete -F _bugz bugz williamh-pybugz-74a57cb/contrib/zsh-completion000066400000000000000000000140261175156041700216240ustar00rootroot00000000000000#compdef bugz # Copyright 2009 Ingmar Vanhassel # 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: ' '--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)' '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' \ '(--title= -t)'{--title=,-t}'[a short description of the attachment]:title: ' \ '(--description= -d)'{--description=,-d}'[a long description of the attachment]:description: ' \ '(--bigfile)'{--bigfile}'[the attachment is a big file]:bigfile: ' \ '--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_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]' \ '(--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 williamh-pybugz-74a57cb/lbugz000077500000000000000000000015251175156041700163370ustar00rootroot00000000000000#!/usr/bin/python """ lbugz: execute bugz from the git repository. This script is primarily used to test bugz. It is a wrapper which adds the package directory to the system path and executes bin/bugz from the git repository passing along any arguments supplied to it. This is based on a patch from Mike Frysinger . """ import os import subprocess import sys args = sys.argv path = os.path.normpath(os.path.dirname(os.path.realpath(__file__))) # path = os.path.normpath(os.path.join(path, '.')) print path pkg = os.path.join(path, 'bugz') script = os.path.join(path, 'bin/bugz') if os.path.exists(pkg) and os.path.exists(script): if 'PYTHONPATH' in os.environ.keys(): os.environ['PYTHONPATH'] = path + ':' + os.environ['PYTHONPATH'] else: os.environ['PYTHONPATH'] = path + ':' args[0] = script subprocess.call(args) williamh-pybugz-74a57cb/man/000077500000000000000000000000001175156041700160365ustar00rootroot00000000000000williamh-pybugz-74a57cb/man/bugz.1000066400000000000000000000021751175156041700170740ustar00rootroot00000000000000.\" 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 . The current maintainer is William Hubbs . William also wrote this man page. williamh-pybugz-74a57cb/setup.py000066400000000000000000000013011175156041700167700ustar00rootroot00000000000000from bugz import __version__ from distutils.core import setup try: from distutils.command.build_py import build_py_2to3 as build_py from distutils.command.build_scripts import build_scripts_2to3 as build_scripts except ImportError: from distutils.command.build_py import build_py from distutils.command.build_scripts import build_scripts 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'], cmdclass = {'build_py': build_py, 'build_scripts': build_scripts}, )