pax_global_header00006660000000000000000000000064125264106070014515gustar00rootroot0000000000000052 comment=6e5db79191f3f030409f06dc35357b9af8b0e66d git-hub-0.9.0/000077500000000000000000000000001252641060700130625ustar00rootroot00000000000000git-hub-0.9.0/.gitignore000066400000000000000000000000611252641060700150470ustar00rootroot00000000000000git-hub.1 bash-completion deb/*.deb deb/install/ git-hub-0.9.0/LICENSE000066400000000000000000001045131252641060700140730ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. 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 them 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 prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. 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. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey 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; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If 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 convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU 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 that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. 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. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 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. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. 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 state 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 3 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, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program 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, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU 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 Lesser General Public License instead of this License. But first, please read . git-hub-0.9.0/Makefile000066400000000000000000000032231252641060700145220ustar00rootroot00000000000000 prefix ?= /usr/local export PYTHON := python version ?= $(shell git describe --dirty 2> /dev/null | cut -b2-) version := $(if $(version),$(version),devel) .PHONY: default default: all .PHONY: all all: man bash-completion .PHONY: deb deb: $(MAKE) prefix=/usr DESTDIR=deb/install install deb/build .PHONY: man man: git-hub.1 git-hub.1: man.rst git-hub sed 's/^:Version: devel$$/:Version: $(version)/' $< | \ rst2man --exit-status=1 > $@ || ($(RM) $@ && false) bash-completion: generate-bash-completion git-hub ./$^ > $@ || ($(RM) $@ && false) .PHONY: install install: git-hub git-hub.1 ftdetect.vim bash-completion README.rst install -m 755 -D git-hub $(DESTDIR)$(prefix)/bin/git-hub sed -i 's/^VERSION = "git-hub devel"$$/VERSION = "git-hub $(version)"/' \ $(DESTDIR)$(prefix)/bin/git-hub sed -i 's|^#!/usr/bin/env python$$|#!/usr/bin/env $(PYTHON)|' \ $(DESTDIR)$(prefix)/bin/git-hub install -m 644 -D git-hub.1 $(DESTDIR)$(prefix)/share/man/man1/git-hub.1 install -m 644 -D ftdetect.vim \ $(DESTDIR)$(prefix)/share/vim/addons/ftdetect/githubmsg.vim -install -m 644 -D bash-completion $(DESTDIR)/etc/bash_completion.d/git-hub install -m 644 -D README.rst $(DESTDIR)$(prefix)/share/doc/git-hub/README.rst .PHONY: release release: @read -p "Enter version (previous: $$(git describe --abbrev=0)): " version; \ test -z $$version && exit 1; \ msg=`echo $$version | sed 's/v/Version /;s/-rc/ Release Candidate /'`; \ echo ; \ echo Changelog: ; \ git log --format='* %s (%h)' `git describe --abbrev=0 HEAD^`..HEAD; \ echo ; \ set -x; \ git tag -a -m "$$msg" $$version .PHONY: clean clean: $(RM) -r git-hub.1 bash-completion deb/*.deb deb/install git-hub-0.9.0/README.rst000066400000000000000000000212671252641060700145610ustar00rootroot00000000000000====================================== A Git command line interface to GitHub ====================================== .. contents:: :depth: 1 :local: Description =========== **git hub** is a simple command line interface to GitHub_, enabling most useful GitHub_ tasks (like creating and listing pull request or issues) to be accessed directly through the Git_ command line. Although probably the most outstanding feature (and the one that motivated the creation of this tool) is the ``pull rebase`` command, which is the *rebasing* version of the `GitHub Merge (TM) button`__. This enables an easy workflow that doesn't involve thousands of merges which makes the repository history unreadable. __ https://github.com/blog/843-the-merge-button Another *unique* feature is the ability to transform an issue into a pull request by attaching commits to it (this is something offered by the `GitHub API`__ but not by the web interface). __ http://developer.github.com/ Usage ===== Here are some usage examples, for more information about all the supported command an options, please refer to the man page using ``git hub --help`` or looking at the `online version`__ (this is for the latest development snapshot though). __ https://github.com/sociomantic/git-hub/blob/master/man.rst One time global setup to get the credentials -------------------------------------------- :: $ git hub setup --global --user octocat GitHub password (will not be stored): You can revoke this credentials at any time in the `GitHub Applications Settings page`__. __ https://github.com/settings/applications Clone (and fork) a project -------------------------- :: $ git hub clone -t sociomantic/git-hub Forking sociomantic/git-hub to octocat/git-hub Cloning git@github.com:sociomantic/git-hub.git to git-hub Fetching from fork (git@github.com:octocat/git-hub.git) The fork will happen only if you haven't fork the project before, of course. And we are using the *triangular workflow* option (``-t`` / ``--triangular``), so we can pull from the *parent* repo but push to our fork by default. Using a pre-existing cloned repository -------------------------------------- :: $ git config hub.upstream sociomantic/git-hub This sets the *master* GitHub_ project. It's where we query for issues and pull requests and where we create new pull requests, etc. This is only necessary if you didn't clone your repository using ``git hub clone`` and is a one time only setup step. List issues ----------- :: $ git hub issue list [3] pull: Use the tracking branch as default base branch (leandro-lucarella-sociomantic) https://github.com/sociomantic/git-hub/issues/3 [1] bash-completion: Complete with IDs only when is appropriate according to command line arguments (leandro-lucarella-sociomantic) https://github.com/sociomantic/git-hub/issues/1 Update an issue --------------- :: $ git hub issue update --label important --label question \ -m 'New Title' --assign octocat --open --milestone v0.5 1 [1] New Title (leandro-lucarella-sociomantic) https://github.com/sociomantic/git-hub/issues/1 Create a new pull request ------------------------- :: $ git hub pull new -b experimental -c mypull Pushing master to mypull in myfork [4] Some pull request (octocat) https://github.com/sociomantic/git-hub/pull/4 This creates a pull request against the upstream branch ``experimental`` using the current ``HEAD``, but creating a new topic branch called ``mypull`` to store the actual pull request (assuming our ``HEAD`` is in the branch ``master``). Attach code to an existing issue -------------------------------- :: $ git hub pull attach -b experimental -c mypull 1 Pushing master to mypull in myfork [1] Some issue (octocat) https://github.com/sociomantic/git-hub/pull/1 Same as before, but this time attach the commits to issue 2 (effectively converting the issue into a pull request). Rebase a pull request --------------------- :: $ git hub pull rebase 4 Fetching mypull from git@github.com:octocat/git-hub.git Rebasing to master in git@github.com:sociomantic/git-hub.git Pushing results to master in git@github.com:sociomantic/git-hub.git [4] Some pull request (octocat) https://github.com/sociomantic/git-hub/pull/4 If the rebase fails, you can use ``git hub pull rebase --continue`` as you would do with a normal rebase. Download ======== You can get this tool from the `GitHub project`__. If you want to grab a release, please remember to visit the Release__ section. __ https://github.com/sociomantic/git-hub __ https://github.com/sociomantic/git-hub/releases Installation ============ Dependencies ------------ * Python_ 2.7 (3.x can be used too but you have to run the ``2to3`` tool to the script first) * Git_ >= 1.7.7 (if you use Ubuntu_ you can easily get the latest Git version using the `Git stable PPA`__) * Docutils_ (>= 0.8, although it might work with older versions too, only needed to build the man page) * FPM_ (>= 1.0.1, although it might work with older versions too, only needed to build the Debian package) __ https://launchpad.net/~git-core/+archive/ppa Building -------- Only the man page and *bash completion* script need to be built. Type ``make`` to build them. Alternatively, you can build a Debian_/Ubuntu_ package. Use ``make deb`` for that. Installing ---------- If you built the Debian_/Ubuntu_ package, you can just install the package (``dpkg -i ../git-hub_VER_all.deb``). Otherwise you can type ``make install`` to install the tool, man page, *bash completion* and VIM_ *ftdetect* plugin (by default in ``/usr/local``, but you can pick a different location by passing the ``prefix`` variable to ``make`` (for example ``make install prefix=/usr``). The installation locations might be too specific for Debian_/Ubuntu_ though. Please report any failed installation attempts. To enjoy the *bash completion* you need to re-login (or re-load the ``/etc/bash_completion`` script). To have syntax highlight in VIM_ when editing **git-hub** messages, you need to activate the *ftdetect* plugin by copying or symbolic-linking it to ``~/.vim/ftdetect/githubmsg.vim``:: mkdir -p ~/.vim/ftdetect ln -s $(prefix)/share/vim/addons/githubmsg.vim ~/.vim/ftdetect/ # or if you are copying from the sources: # ln -s ftdetect.vim ~/.vim/ftdetect/githubmsg.vim Similar Projects ================ We explored other other alternatives before starting this project, but none of these tools do (or are targeted) at what we needed. But here are the ones we found, in case they are a better fit for you: * `hub `_: Is the *official* tool, but it completely replaces the Git command, adding special syntax for official git commands. This is definitely something we didn't want. We don't want to mess with Git. * `ghi `_: This only handle issues. Not what we needed. * `git-spindle `_: This tool was discovered after we started and published this project. It covers similar ground, but doesn't offer rebase capabilities (this, of course, could have been implemented as an extension). Sadly, it also extends the Git command-line adding the ``hub`` command, which can introduce a lot of confusion to users. We might try to merge our code into that project eventually, if there is interest. Contact ======= If you want to contact us, either because you are an user and have questions, or because you want to contribute to the project, you can subscribe to the mailing list. Subscription happens automatically (after confirmation) the first time you write to: git.hub@librelist.com (this first e-mail will be dropped). You can always visit the `mailing list archives`__ to check if your questions were already answered in the past :) __ http://librelist.com/browser/git.hub/ You can also use GMANE__ to get a `better list archive`__ (both threaded__ and `blog-like`__ interfaces available) or to `read the list using NNTP`__. __ http://www.gmane.org/ __ http://dir.gmane.org/gmane.comp.version-control.git.git-hub __ http://news.gmane.org/gmane.comp.version-control.git.git-hub __ http://blog.gmane.org/gmane.comp.version-control.git.git-hub __ nntp://news.gmane.org/gmane.comp.version-control.git.git-hub If you want to report a bug, just `create an issue`__ please (if you use this tool I'm sure you already have a GitHub_ account ;). __ https://github.com/sociomantic/git-hub/issues/new .. _Python: http://www.python.org/ .. _Docutils: http://docutils.sourceforge.net/ .. _Git: http://www.git-scm.com/ .. _GitHub: http://www.github.com/ .. _Ubuntu: http://www.ubuntu.com/ .. _Debian: http://www.debian.org/ .. _VIM: http://www.vim.org/ .. _FPM: https://github.com/jordansissel/fpm .. vim: set et sw=2 tw=80 : git-hub-0.9.0/deb/000077500000000000000000000000001252641060700136145ustar00rootroot00000000000000git-hub-0.9.0/deb/build000077500000000000000000000022501252641060700146400ustar00rootroot00000000000000#!/bin/sh set -e cd `dirname $0` genchangelog() { echo "$1 ($2) `lsb_release -sc`; urgency=low" echo prevtag=$(git describe --abbrev=0 HEAD^) git log --date=short --format=" * %s (%h, %cd)" "$prevtag"..HEAD | fold --spaces --width 76 | sed 's/^\([^ ]\+\)/ \1/' echo echo " -- $3 `LANG=C date -R`" } pkgversion=$(git describe --dirty | cut -c2- | sed 's/-\([0-9]\+\)-\(g[0-9a-f]\+\)/+\1~\2/' | sed 's/\(~g[0-9a-f]\+\)-dirty$/-dirty\1/' | sed 's/-dirty/~dirty.'`date +%Y%m%d%H%M%S`'/' )-$(lsb_release -cs) pkgmaint=$(echo "`git config user.name` <`git config user.email`>") changelog=`mktemp` trap "rm -f '$changelog'; exit 1" INT TERM QUIT pkgname=git-hub genchangelog "$pkgname" "$pkgversion" "$pkgmaint" > "$changelog" fpm -s dir -t deb -n "$pkgname" -v "$pkgversion" \ --architecture all \ --maintainer "$pkgmaint" \ --description "Git command line interface to GitHub" \ --url 'https://github.com/sociomantic/git-hub' \ --vendor 'Sociomantic Labs GmbH' \ --license GPLv3 \ --category vcs \ --depends python2.7 \ --depends "git (>=1.7.7)" \ --deb-changelog "$changelog" \ install/usr=/ \ install/etc=/ git-hub-0.9.0/ftdetect.vim000066400000000000000000000000671252641060700154040ustar00rootroot00000000000000au BufNewFile,BufRead *.git/HUB_EDITMSG setf gitcommit git-hub-0.9.0/generate-bash-completion000077500000000000000000000040231252641060700176630ustar00rootroot00000000000000#!/bin/bash set -e # Generates a bash-completion script as a git subcommand to stdout # # Based on the _git_svn completion function in git distribution's bash # completion. PYTHON=${PYTHON:-python} issue_arg_cmds="issue-show issue-update issue-comment issue-close pull-attach" pull_arg_cmds="pull-show pull-update pull-comment pull-close" get_opts() { $PYTHON git-hub "$@" -h > /dev/null || exit 1 $PYTHON git-hub "$@" -h | sed -n 's/^ \(-., \)\?\(--[^ ]\+\) .*$/\2/p' | tr '\n' ' ' $PYTHON git-hub "$@" -h | sed -n 's/^ \(-. \([A-Z]\+\), \)\?\(--[^ ]\+\) \2\( .*\)\?$/\3=/p' | tr '\n' ' ' } get_cmds() { $PYTHON git-hub "$@" -h > /dev/null || exit 1 $PYTHON git-hub "$@" -h | sed -n 's/,/ /g;s/^ {\(.*\)}$/\1/p' } cmds=$(get_cmds) cat < /dev/null | sed -n 's/^\[\([0-9]\+\)\] .*$/\1/p' | tr '\n' ' ' } __git_hub_get_pulls() { git hub pull list 2> /dev/null | sed -n 's/^\[\([0-9]\+\)\] .*$/\1/p' | tr '\n' ' ' } _git_hub () { local subcommand="\$(__git_find_on_cmdline "$cmds")" if [ -z "\$subcommand" ]; then case "\$cur" in --*) __gitcomp "$(get_opts)" ;; *) __gitcomp "$cmds" ;; esac else case "\$subcommand" in EOT for cmd in $cmds do subcmds=$(get_cmds $cmd) cat < # # Copyright (c) 2013 by Sociomantic Labs GmbH # # This program is written as a single file for practical reasons, this way # users can download just one file and use it, while if it's spread across # multiple modules, a setup procedure must be provided. This might change in # the future though, if the program keeps growing. # # 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 3 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, see . """\ Git command line interface to GitHub. This program integrates Git and GitHub as a Git extension command (hub). It enabling most useful GitHub tasks (like creating and listing pull request or issues) to be accessed directly through the git command line. """ VERSION = "git-hub devel" import re import sys import json import string import base64 import urllib import pprint import urllib2 import getpass import os.path import urlparse import argparse import subprocess # Output levels according to user selected verbosity DEBUG = 3 INFO = 2 WARN = 1 ERR = 0 # Output functions, all use the str.format() function for formatting ######################################################################## def localeprintf(stream, fmt='', *args, **kwargs): encoding = sys.getfilesystemencoding() msg = fmt.decode(encoding, 'replace').format(*args, **kwargs) + '\n' stream.write(msg.encode(encoding, 'replace')) def pformat(obj): return pprint.pformat(obj, indent=4) def debugf(fmt='', *args, **kwargs): if verbose < DEBUG: return localeprintf(sys.stdout, fmt, *args, **kwargs) def infof(fmt='', *args, **kwargs): if verbose < INFO: return localeprintf(sys.stdout, fmt, *args, **kwargs) def warnf(fmt='', *args, **kwargs): if verbose < WARN: return msg = '' if sys.stderr.isatty(): msg += '\033[33m' msg += 'Warning: ' + fmt.format(*args, **kwargs) if sys.stderr.isatty(): msg += '\033[0m' localeprintf(sys.stderr, '{}', msg) sys.stderr.flush() def errf(fmt='', *args, **kwargs): if verbose < ERR: return msg = '' if sys.stderr.isatty(): msg += '\033[31m' msg += 'Error: ' + fmt.format(*args, **kwargs) if sys.stderr.isatty(): msg += '\033[0m' localeprintf(sys.stderr, '{}', msg) sys.stderr.flush() def die(fmt='', *args, **kwargs): errf(fmt, *args, **kwargs) sys.exit(1) # This is very similar to die() but used to exit the program normally having # full stack unwinding. The message is printed with infof() and the exit status # is 0 (normal termination). The exception is handled by the regular exception # handling code after calling main() def interrupt(fmt='', *args, **kwargs): raise InterruptException(fmt.format(*args, **kwargs)) class InterruptException (Exception): def __init__(self, msg): super(InterruptException, self).__init__(msg) # Ask the user a question # # `question` is the text to display (information with the available options # will be automatically appended). `options` must be an array of strings, and # `default` a string inside that list, or None. If default is None, if the user # press ENTER without giving an actual answer, it will be asked again, # otherwise this function returns the `default` option. # # Returns the item selected by the user (one of the strings in `options`) # unless stdin is not a tty, in which case no question is asked and this # function returns None. def ask(question, default=None, options=["yes", "no"]): if not sys.stdin.isatty(): return None if default not in options and default is not None: raise ValueError("invalid default answer: '%s'" % default) valid_answers = dict() opts_strings = list() for opt in options: if opt is None: raise ValueError("options can't be None") valid_answers[opt.lower()] = opt valid_answers[opt[0].lower()] = opt opt_str = opt.capitalize() if default == opt: # default in bold opt_str = '\033[1m' + opt.upper() + '\033[21m' opts_strings.append(opt_str) # options in dark question += ' \033[2m[' + '/'.join(opts_strings) + '] ' while True: # question in yellow sys.stderr.write('\033[33m' + question + '\033[0m') choice = raw_input().lower().strip() if default is not None and choice == '': return default elif choice in valid_answers: return valid_answers[choice] else: errf("Invalid answer, valid options: {}", ', '.join(options)) # Git manipulation functions and constants ######################################################################## GIT_CONFIG_PREFIX = 'hub.' # This error is thrown when a git command fails (if git prints something in # stderr, this is considered an error even if the return code is 0). The # `output` argument is really the stderr output only, the `output` name is # preseved because that's how is it called in the parent class. class GitError (subprocess.CalledProcessError): def __init__(self, returncode, cmd, output): super(GitError, self).__init__(returncode, cmd, output) def __str__(self): return '%s failed (return code: %s)\n%s' % (' '.join(self.cmd), self.returncode, self.output) # Convert a variable arguments tuple to an argument list # # If args as only one element, the element is expected to be a string and the # new argument list is populated by splitting the string using spaces as # separator. If it has multiple arguments, is just converted to a list. def args_to_list(args): if len(args) == 1: return args[0].split() return list(args) # Run a git command returning its output (throws GitError on error) # # `args` should be strings. If only one string is found in the list, is split # by spaces and the resulting list is used as `args`. kwargs are extra # arguments you might want to pass to subprocess.Popen(). # # Returns the text printed to stdout by the git command without the EOL. def git(*args, **kwargs): args = args_to_list(args) args.insert(0, 'git') kwargs['stdout'] = subprocess.PIPE kwargs['stderr'] = subprocess.PIPE debugf('git command: {} {}', args, kwargs) proc = subprocess.Popen(args, **kwargs) (stdout, stderr) = proc.communicate() if proc.returncode != 0: raise GitError(proc.returncode, args, stderr.rstrip('\n')) return stdout.rstrip('\n') # Check if the git version is at least min_version # Returns a tuple with the current version and True/False depending on how the # check went (True if it passed, False otherwise) def git_check_version(min_version): cur_ver_str = git('--version').split()[2] cur_ver = cur_ver_str.split('.') min_ver = min_version.split('.') for i in range(len(min_ver)): if cur_ver[i] < min_ver[i]: return (cur_ver_str, False) if cur_ver[i] > min_ver[i]: return (cur_ver_str, True) return (cur_ver_str, True) # Same as git() but inserts --quiet at quiet_index if verbose is less than DEBUG def git_quiet(quiet_index, *args, **kwargs): args = args_to_list(args) if verbose < DEBUG: args.insert(quiet_index, '--quiet') return git(*args, **kwargs) # Specialized version of git_quiet() for `push`ing that accepts an extra # keyword argument named 'force'. If present, '--force' is passed to git push. def git_push(*args, **kwargs): cmd = ['push'] if 'force' in kwargs: cmd.append('--force') del kwargs['force'] cmd += args_to_list(args) git_quiet(1, *cmd, **kwargs) # Dummy class to indicate a value is required by git_config class NO_DEFAULT: pass # Specialized version of git() to get/set git config variables. # # `name` is the name of the git variable to get/set. `default` is the value # that should be returned if variable is not defined (None will return None in # that case, use NO_DEFAULT to make this function exit with an error if the # variable is undefined). `prefix` is a text to prefix to the variable `name`. # If `value` is present, then the variable is set instead. `opts` is an # optional list of extra arguments to pass to git config. # # Returns the variable value (or the default if not present) if `value` is # None, otherwise it just sets the variable. def git_config(name, default=None, prefix=GIT_CONFIG_PREFIX, value=None, opts=()): name = prefix + name cmd = ['config'] + list(opts) + [name] try: if value is not None: cmd.append(value) return git(*cmd) except subprocess.CalledProcessError as e: if e.returncode == 1: if default is not NO_DEFAULT: return default die("Can't find '{}' config key in git config. " "Read the man page for details.", name) raise e # Returns the .git directory location def git_dir(): return git('rev-parse', '--git-dir') # Invokes the editor defined by git var GIT_EDITOR. # # The editor is invoked to edit the file .git/HUB_EDITMSG, the default content # of that file will be the `msg` (if any) followed by the `help_msg`, used to # hint the user about the file format. # # The contents of .git/HUB_EDITMSG are returned by the function (after the # editor is closed). The text is not filtered at all at this stage (so if the # user left the `help_msg`, it will be part of the returned string too. def editor(help_msg, msg=None): prog = git('var', 'GIT_EDITOR') fname = os.path.join(git_dir(), 'HUB_EDITMSG') with file(fname, 'w') as f: f.write(msg or '') f.write(help_msg) status = subprocess.call([prog, fname]) if status != 0: die("Editor returned {}, aborting...", status) with file(fname) as f: msg = f.read() return msg # git-hub specific configuration container # # These variables are described in the manual page. class Config: def __init__(self): self.username = git_config('username', getpass.getuser()) self.oauthtoken = git_config('oauthtoken') self.upstream = git_config('upstream') if self.upstream and '/' not in self.upstream: die("Invalid hub.upstream configuration, '/' not found") self.forkrepo = git_config('forkrepo') if not self.forkrepo and self.upstream: upstream = self.upstream.split('/') self.forkrepo = self.username + '/' + upstream[1] self.forkremote = git_config('forkremote', 'origin') self.pullbase = git_config('pullbase', 'master') self.urltype = git_config('urltype', 'ssh_url') self.baseurl = self.sanitize_url('baseurl', git_config('baseurl', 'https://api.github.com')) self.forcerebase = git_config('forcerebase', "true", opts=['--bool']) == "true" self.triangular = git_config('triangular', "false", opts=['--bool']) == "true" def sanitize_url(self, name, url): u = urlparse.urlsplit(url) name = GIT_CONFIG_PREFIX + name # interpret www.github.com/api/v4 as www.github.com, /api/v4 if not u.hostname or not u.scheme: die("Please provide a full URL for '{}' (e.g. " "https://api.github.com), got {}", name, url) if u.username or u.password: warnf("Username and password in '{}' ({}) will be " "ignored, use the 'setup' command " "for authentication", name, url) netloc = u.hostname if u.port: netloc += ':' + u.port return urlparse.urlunsplit((u.scheme, netloc, u.path.rstrip('/'), u.query, u.fragment)) def check(self, name): if getattr(self, name) is None: die("Can't find '{}{}' config key in git config. " "Read the man page for details.", GIT_CONFIG_PREFIX, name) # Manages GitHub request handling authentication and content headers. # # The real interesting methods are created after the class declaration, for # each type of request: head(), get(), post(), patch(), put() and delete(). # # All these methods take an URL (relative to the config.baseurl) and optionally # an arbitrarily number of positional or keyword arguments (but not both at the # same time). The extra arguments, if present, are serialized as json and sent # as the request body. # All these methods return None if the response is empty, or the deserialized # json data received in the body of the response. # # Example: # # r = req.post('/repos/sociomantic/test/labels/', name=name, color=color) # # The basic auth has priority over oauthtoken for authentication. If you want # to use OAuth just leave `basic_auth` and set `oauthtoken`. To fill the # `basic_auth` member, the `set_basic_auth()` convenient method is provided). # # See http://developer.github.com/ for more details on the GitHub API class RequestManager: basic_auth = None oauthtoken = None links_re = re.compile(r'<([^>]+)>;.*rel=[\'"]?([^"]+)[\'"]?', re.M) def __init__(self, base_url, oauthtoken=None, username=None, password=None): self.base_url = base_url if oauthtoken is not None: self.oauthtoken = oauthtoken elif username is not None: self.set_basic_auth(username, password) # Configure the class to use basic authentication instead of OAuth def set_basic_auth(self, username, password): self.basic_auth = "Basic " + base64.urlsafe_b64encode("%s:%s" % (username, password)) # Open an URL in an authenticated manner using the specified HTTP # method. It also add other convenience headers, like Content-Type, # Accept (both to json) and Content-Length). def auth_urlopen(self, url, method, body): req = urllib2.Request(url, body) if self.basic_auth: req.add_header("Authorization", self.basic_auth) elif self.oauthtoken: req.add_header("Authorization", "bearer " + self.oauthtoken) req.add_header("Content-Type", "application/json") req.add_header("Accept", "application/vnd.github.v3+json") req.add_header("Content-Length", str(len(body) if body else 0)) req.get_method = lambda: method.upper() debugf('{}', req.get_full_url()) # Hide sensitive information from DEBUG output if verbose >= DEBUG: for h in req.header_items(): if h[0].lower() == 'authorization': debugf('{}: {}', h[0], '') else: debugf('{}: {}', *h) debugf('{}', req.get_data()) return urllib2.urlopen(req) # Serialize args OR kwargs (they are mutually exclusive) as json. def dump(self, *args, **kwargs): if args and kwargs: raise ValueError('args and kwargs are mutually ' 'exclusive') if args: return json.dumps(args) if kwargs: return json.dumps(kwargs) return None # Get the next URL from the Link: header, if any def get_next_url(self, response): links = list() for l in response.headers.get("Link", "").split(','): links.extend(self.links_re.findall(l)) links = dict((rel, url) for url, rel in links) return links.get("next", None) # This is the real method used to do the work of the head(), get() and # other high-level methods. `url` should be a relative URL for the # GitHub API, `method` is the HTTP method to be used (must be in # uppercase), and args/kwargs are data to be sent to the client. Only # one can be specified at a time and they are serialized as json (args # as a json list and kwargs as a json dictionary/object). def json_req(self, url, method, *args, **kwargs): url = self.base_url + url if method.upper() in ('POST', 'PATCH', 'PUT'): body = self.dump(*args, **kwargs) else: body = None url += '?' + urllib.urlencode(kwargs) data = None prev_data = None while url: debugf("Request: {} {}\n{}", method, url, body) res = self.auth_urlopen(url, method, body) data = res.read() debugf("Response:\n{}", data) if data: data = json.loads(data) if isinstance(data, list): if prev_data is None: prev_data = list() prev_data.extend(data) data = None url = self.get_next_url(res) assert not (prev_data and data) if prev_data is not None: data = prev_data debugf("Parsed data:\n{}", pformat(data)) return data # Create RequestManager.head(), get(), ... methods # We need the make_method() function to make Python bind the method variable # (from the loop) early (in the loop) instead of when is called. Otherwise all # methods get bind with the last value of method ('delete') in this case, which # is not only what we want, is also very dangerous. def make_method(method): return lambda self, url, *args, **kwargs: \ self.json_req(url, method, *args, **kwargs) for method in ('OPTIONS', 'HEAD', 'GET', 'POST', 'PATCH', 'PUT', 'DELETE'): setattr(RequestManager, method.lower(), make_method(method)) # Message cleaning and parsing functions # (used to clean the text returned by editor()) ######################################################################## def check_empty_message(msg): if not msg.strip(): die("Message is empty, aborting...") return msg message_markdown_help = '''\ # Remember GitHub will parse comments and descriptions as GitHub # Flavored Markdown. # For details see: http://github.github.com/github-flavored-markdown/ # # Lines starting with '# ' (note the space after the hash!) will be # ignored, and an empty message aborts the command. The space after the # hash is required by comments to avoid accidentally commenting out a # line starting with a reference to an issue (#4 for example). If you # want to include Markdown headers in your message, use the Setext-style # headers, which consist on underlining titles with '=' for first-level # or '-' for second-level headers. ''' # For now it only removes comment lines def clean_message(msg): lines = msg.splitlines() # Remove comment lines lines = [l for l in lines if not l.strip().startswith('#')] return '\n'.join(lines) # For messages expecting a title, split the first line as the title and the # rest as the message body (but expects the title and message to be separated # by an empty line). # # Returns the tuple (title, body) where body might be an empty string. def split_titled_message(msg): lines = check_empty_message(clean_message(msg)).splitlines() title = lines[0] body = '' if len(lines) > 1: if lines[1].strip(): die("Wrong message format, leave an " "empty line between the title " "and the body") body = '\n'.join(lines[2:]) return (title, body) # Command-line commands helper classes ######################################################################## # Base class for commands that just group other subcommands # # The subcommands are inspected automatically from the derived class by # matching the names to the `subcommand_suffix`. For each sub-command, the # method will be called to add options to the command-line # parser.. # # Each sub-command is expected to have a certain structure (some methods and/or # attributes): # # `run(parser, args)` # Required method that actually does the command work. # `parser` is the command-line parser instance, and `args` are the parsed # arguments (only the ones specific to that sub-command). # # `setup_parser(parser)` # Will be called to setup the command-line parser. This is where new # subcommands or specific options can be added to the parser. If it returns # True, then parse_known_args() will be used instead, so you can collect # unknown arguments (stored in args.unknown_args). # # `cmd_name` # Command name, if not present the name of the class (with the # `subcommand_suffix` removed and all in lowercase) is used as the name. # # `cmd_title` # A string shown in the help message when listing the group of subcommands # (not required but strongly recommended) for the current class. # # `cmd_help` # A string describing what this command is for (shown in the help text when # listing subcommands). If not present, the class __doc__ will be used. # # `cmd_usage` # A usage string to be passed to the parser. If it's not defined, is # genreated from the options as usual. %(prog)s can be used to name the # current program (and subcommand). # # `cmd_required_config` # An optional list of configuration variables that this command needs to work # (if any of the configuration variables in this list is not defined, the # program exists with an error). # # All methods are expected to be `classmethod`s really. class CmdGroup (object): subcommand_suffix = 'Cmd' @classmethod def setup_parser(cls, parser): partial = False suffix = cls.subcommand_suffix subcommands = [getattr(cls, a) for a in dir(cls) if a.endswith(suffix)] if not subcommands: return title = None if hasattr(cls, 'cmd_title'): title = cls.cmd_title subparsers = parser.add_subparsers(title=title) for cmd in subcommands: name = cmd.__name__.lower()[:-len(suffix)] if hasattr(cmd, 'cmd_name'): name = cmd.cmd_name help = cmd.__doc__ if hasattr(cmd, 'cmd_help'): help = cmd.cmd_help kwargs = dict(help=help) if hasattr(cmd, 'cmd_usage'): kwargs['usage'] = cmd.cmd_usage p = subparsers.add_parser(name, **kwargs) partial = cmd.setup_parser(p) or partial if not hasattr(cmd, 'run'): continue if hasattr(cmd, 'cmd_required_config'): def make_closure(cmd): def check_config_and_run(parser, args): for c in cmd.cmd_required_config: config.check(c) cmd.run(parser, args) return check_config_and_run p.set_defaults(run=make_closure(cmd)) else: p.set_defaults(run=cmd.run) return partial # `git hub setup` command implementation class SetupCmd (object): cmd_help = 'perform an initial setup to connect to GitHub' @classmethod def setup_parser(cls, parser): parser.add_argument('-u', '--username', help="GitHub's username (login name). If an e-mail is " "provided instead, a username matching that e-mail " "will be searched and used instead, if found (for " "this to work the e-mail must be part of the public " "profile)") parser.add_argument('-p', '--password', help="GitHub's password (will not be stored)") parser.add_argument('-b', '--baseurl', metavar='URL', help="GitHub's base URL to use to access the API " "(Enterprise servers usually use https://host/api/v3)") group = parser.add_mutually_exclusive_group() group.add_argument('--global', dest='opts', action='store_const', const=['--global'], help="store settings in the global configuration " "(see git config --global for details)") group.add_argument('--system', dest='opts', action='store_const', const=['--system'], help="store settings in the system configuration " "(see git config --system for details)") parser.set_defaults(opts=[]) @classmethod def run(cls, parser, args): is_global = ('--system' in args.opts or '--global' in args.opts) try: if not is_global: git('rev-parse --git-dir') except GitError as error: errf(error.output) die("Maybe you want to use --global or --system?") username = args.username password = args.password if (username is None or password is None) and \ not sys.stdin.isatty(): die("Can't perform an interactive setup outside a tty") if username is None: username = config.username or getpass.getuser() reply = raw_input('GitHub username [%s]: ' % username) if reply: username = reply if password is None: password = getpass.getpass( 'GitHub password (will not be stored): ') if '@' in username: infof("E-mail used to authenticate, trying to " "retrieve the GitHub username...") username = cls.find_username(username) infof("Found: {}", username) req.set_basic_auth(username, password) note = 'git-hub' if not is_global and config.forkrepo: proj = config.forkrepo.split('/', 1)[1] note += ' (%s)' % proj while True: infof("Looking for GitHub authorization token...") auths = dict([(a['note'], a) for a in req.get('/authorizations')]) if note not in auths: break errf("The OAuth token with name '{}' already exists.", note) infof("If you want to create a new one, enter a " "name for it. Otherwise you can go to " "https://github.com/settings/tokens to " "regenerate or delete the token '{}'", note) note = raw_input("Enter a new token name (an empty " "name cancels the setup): ") if not note: sys.exit(0) infof("Creating auth token '{}'", note) auth = req.post('/authorizations', note=note, scopes=['user', 'repo']) set_config = lambda k, v: git_config(k, value=v, opts=args.opts) set_config('username', username) set_config('oauthtoken', auth['token']) if args.baseurl is not None: set_config('baseurl', args.baseurl) @classmethod def find_username(cls, name): users = req.get('/search/users', q=name)['items'] users = [u['login'] for u in users] if not users: die("No users found when searching for '{}'", name) if len(users) > 1: die("More than one username found ({}), please try " "again using your username instead", ', '.join(users)) return users[0].encode('UTF8') # `git hub clone` command implementation class CloneCmd (object): cmd_required_config = ['username', 'urltype', 'oauthtoken'] cmd_help = 'clone a GitHub repository (and fork as needed)' cmd_usage = '%(prog)s [OPTIONS] [GIT CLONE OPTIONS] REPO [DEST]' @classmethod def setup_parser(cls, parser): parser.add_argument('repository', metavar='REPO', help="name of the repository to fork; in " "/ format is the upstream repository, " "if only is specified, the part is " "taken from hub.username") parser.add_argument('dest', metavar='DEST', nargs='?', help="destination directory where to put the new " "cloned repository") parser.add_argument('-r', '--remote', metavar='NAME', help="use NAME as the upstream remote repository name " "instead of the default 'upstream' (for a conventional " "clone) or 'fork' (for a --triangular clone)") parser.add_argument('-t', '--triangular', action="store_true", help="use Git 'triangular workflow' setup, so you can " "push by default to your fork but pull by default " "from 'upstream'") return True # we need to get unknown arguments @classmethod def run(cls, parser, args): upstream = urlparse.urlsplit(args.repository).path if upstream.endswith('.git'): upstream = upstream[:-4] if '/' in upstream: (owner, proj) = upstream.split('/')[-2:] for repo in req.get('/user/repos'): if repo['name'] == proj and repo['fork']: break else: # Fork not found infof('Forking {} to {}/{}', upstream, config.username, proj) repo = req.post('/repos/' + upstream + '/forks') repo = req.get('/repos/' + repo['full_name']) else: # no '/' in upstream repo = req.get('/repos/%s/%s' % (config.username, upstream)) if not repo['fork']: warnf('Repository {} is not a fork, just ' 'clonning, upstream will not be set', repo['full_name']) upstream = None else: upstream = repo['parent']['full_name'] dest = args.dest or repo['name'] triangular = cls.check_triangular(config.triangular or args.triangular) if triangular and not upstream: parser.error("Can't use triangular workflow without " "an upstream repo") url = repo['parent'][config.urltype] if triangular \ else repo[config.urltype] infof('Cloning {} to {}', url, dest) git_quiet(1, 'clone', *(args.unknown_args + [url, dest])) if not upstream: # Not a forked repository, nothing else to do return # Complete the repository setup os.chdir(dest) remote = args.remote or ('fork' if triangular else 'upstream') remote_url = repo['parent'][config.urltype] if triangular: remote_url = repo[config.urltype] git_config('remote.pushdefault', prefix='', value=remote) git_config('forkremote', value=remote) git_config('upstream', value=upstream) git('remote', 'add', remote, remote_url) infof('Fetching from {} ({})', remote, remote_url) git_quiet(1, 'fetch', remote) @classmethod def check_triangular(cls, triangular): if not triangular: return False min_ver = '1.8.3' (cur_ver, ver_ok) = git_check_version(min_ver) if not ver_ok: warnf("Current git version ({}) is too old to support " "--triangular, at least {} is needed. Ignoring " "the --triangular option...", cur_ver, min_ver) return False min_ver = '1.8.4' (cur_ver, ver_ok) = git_check_version(min_ver) pd = git_config('push.default', prefix='', default=None) if not ver_ok and pd == 'simple': warnf("Current git version ({}) has an issue when " "using the option push.default=simple and " "using the --triangular workflow. Please " "use Git {} or newer, or change push.default " "to 'current' for example. Ignoring the " "--triangular option...", cur_ver, min_ver) return False return True # Utility class that group common functionality used by the multiple `git hub # issue` (and `git hub pull`) subcommands. class IssueUtil (object): cmd_required_config = ['upstream', 'oauthtoken'] # Since this class is reused by the CmdPull subcommands, we use several # variables to customize the out and help message to adjust to both # issues and pull requests. name = 'issue' gh_path = 'issues' id_var = 'ISSUE' help_msg = ''' # Please enter the title and description below. # # The first line is interpreted as the title. An optional description # can follow after and empty line. # # Example: # # Some title # # Some description that can span several # lines. # ''' + message_markdown_help comment_help_msg = ''' # Please enter your comment below. # ''' + message_markdown_help @classmethod def print_issue_summary(cls, issue): infof(u'[{number}] {title} ({user[login]})\n{}{html_url}', u' ' * (len(str(issue['number'])) + 3), **issue) @classmethod def print_issue_header(cls, issue): issue['labels'] = ' '.join(['['+l['name']+']' for l in issue.get('labels', [])]) if issue['labels']: issue['labels'] += '\n' infof(u""" #{number}: {title} ================================================================================ {name} is {state}, was reported by {user[login]} and has {comments} comment(s). {labels}<{html_url}> {body} """, **issue) if issue['comments'] > 0: infof(u'Comments:') @classmethod def print_issue_comment(cls, comment): body = comment['body'] body = '\n'.join([' '+l for l in body.splitlines()]) infof(u'On {created_at}, {user[login]} commented:\n' '{0}\n\n <{html_url}>\n', body, **comment) @classmethod def merge_issue_comments(cls, comments, review_comments): for c in comments: c['sort_key'] = c['created_at'] prev_commit_pos = None prev_created = None for c in review_comments: curr_commit_pos = c['commit_id'], c['position'] if prev_commit_pos is None: prev_commit_pos = curr_commit_pos prev_created = c['created_at'] if prev_commit_pos == curr_commit_pos: c['sort_key'] = '%s %s %s' % (prev_created, prev_commit_pos[0], prev_commit_pos[1]) else: prev_commit_pos = None prev_created = None comments = comments + review_comments comments.sort(key=lambda c: c['sort_key']) return comments @classmethod def print_issue(cls, issue, comments, review_comments=()): review_comments = list(review_comments) issue = dict(issue) issue['name'] = cls.name.capitalize() issue['comments'] += len(comments) + len(review_comments) cls.print_issue_header(issue) prev_commit_pos = None for c in cls.merge_issue_comments(comments, review_comments): if 'diff_hunk' in c: curr_commit_pos = c['commit_id'], ['position'] if prev_commit_pos is None: infof(u'{}\n', u'-' * 80) infof(u'diff --git a/{path} b/{path}\n' u'index {original_commit_id}..{commit_id}\n' u'--- a/{path}\n' u'+++ b/{path}\n' u'{diff_hunk}\n', **c) prev_commit_pos = curr_commit_pos if prev_commit_pos == curr_commit_pos: cls.print_issue_comment(c) else: infof(u'{}\n', u'-' * 80) cls.print_issue_comment(c) @classmethod def print_comment(cls, comment): body = comment['body'].decode('UTF8') infof(u'[{id}] {}{} ({user[login]})', body[:60], u'…' if len(body) > 60 else u'', u' ' * (len(str(comment['id'])) + 3), **comment) @classmethod def url(cls, number=None): s = '/repos/%s/%s' % (config.upstream, cls.gh_path) if number: s += '/' + number return s @classmethod def editor(cls, msg=None): return editor(cls.help_msg, msg) @classmethod def comment_editor(cls, msg=None): return editor(cls.comment_help_msg, msg) @classmethod def clean_and_post_comment(cls, issue_num, body): # URL fixed to issues, pull requests comments are made through # issues url = '/repos/%s/issues/%s/comments' % (config.upstream, issue_num) body = check_empty_message(clean_message(body)) comment = req.post(url, body=body) cls.print_comment(comment) # `git hub issue` command implementation class IssueCmd (CmdGroup): cmd_title = 'subcommands to manage issues' cmd_help = 'manage issues' class ListCmd (IssueUtil): cmd_help = "show a list of open issues" @classmethod def setup_parser(cls, parser): parser.add_argument('-c', '--closed', action='store_true', default=False, help="show only closed pull requests") parser.add_argument('-C', '--created-by-me', action='store_true', help=("show only %ss created by me" % cls.name)) parser.add_argument('-A', '--assigned-to-me', action='store_true', help=("show only %ss assigned to me" % cls.name)) @classmethod def run(cls, parser, args): def filter(issue, name): a = issue[name] if a and a['login'] == config.username: return True state = 'closed' if args.closed else 'open' issues = req.get(cls.url(), state=state) if not issues: return if args.created_by_me and args.assigned_to_me: issues = [i for i in issues if filter(i, 'assignee') or filter(i, 'user')] elif args.created_by_me: issues = [i for i in issues if filter(i, 'user')] elif args.assigned_to_me: issues = [i for i in issues if filter(i, 'assignee')] for issue in issues: cls.print_issue_summary(issue) class ShowCmd (IssueUtil): cmd_help = "show details for existing issues" @classmethod def setup_parser(cls, parser): parser.add_argument('issues', nargs='+', metavar=cls.id_var, help="number identifying the %s to show" % cls.name) parser.add_argument('--summary', default=False, action='store_true', help="print just a summary of the issue, not " "the full details with comments") @classmethod def run(cls, parser, args): for n in args.issues: issue = req.get(cls.url(n)) if args.summary: cls.print_issue_summary(issue) continue c = [] if issue['comments'] > 0: c = req.get(cls.url(n) + "/comments") cls.print_issue(issue, c) class NewCmd (IssueUtil): cmd_help = "create a new issue" @classmethod def setup_parser(cls, parser): parser.add_argument('-m', '--message', metavar='MSG', help="%s's title (and description); the " "first line is used as the title and " "any text after an empty line is used as " "the optional body" % cls.name) parser.add_argument('-l', '--label', dest='labels', metavar='LABEL', action='append', help="attach LABEL to the %s (can be " "specified multiple times to set multiple " "labels)" % cls.name) parser.add_argument('-a', '--assign', dest='assignee', metavar='USER', help="assign an user to the %s; must be a " "valid GitHub login name" % cls.name) parser.add_argument('-M', '--milestone', metavar='ID', help="assign the milestone identified by the " "number ID to the %s" % cls.name) @classmethod def run(cls, parser, args): msg = args.message or cls.editor() (title, body) = split_titled_message(msg) issue = req.post(cls.url(), title=title, body=body, assignee=args.assignee, labels=args.labels, milestone=args.milestone) cls.print_issue_summary(issue) class UpdateCmd (IssueUtil): cmd_help = "update an existing issue" @classmethod def setup_parser(cls, parser): parser.add_argument('issue', metavar=cls.id_var, help="number identifying the %s to update" % cls.name) parser.add_argument('-m', '--message', metavar='MSG', help="new %s title (and description); the " "first line is used as the title and " "any text after an empty line is used as " "the optional body" % cls.name) parser.add_argument('-e', '--edit-message', action='store_true', default=False, help="open the default $GIT_EDITOR to edit the " "current title (and description) of the %s" % cls.name) group = parser.add_mutually_exclusive_group() group.add_argument('-o', '--open', dest='state', action='store_const', const='open', help="reopen the %s" % cls.name) group.add_argument('-c', '--close', dest='state', action='store_const', const='closed', help="close the %s" % cls.name) parser.add_argument('-l', '--label', dest='labels', metavar='LABEL', action='append', help="if one or more labels are specified, " "they will replace the current %s labels; " "otherwise the labels are unchanged. If one of " "the labels is empty, the labels will be " "cleared (so you can use -l'' to clear the " "labels)" % cls.name) parser.add_argument('-a', '--assign', dest='assignee', metavar='USER', help="assign an user to the %s; must be a " "valid GitHub login name" % cls.name) parser.add_argument('-M', '--milestone', metavar='ID', help="assign the milestone identified by the " "number ID to the %s" % cls.name) @classmethod def run(cls, parser, args): # URL fixed to issues, pull requests updates are made # through issues to allow changing labels, assignee and # milestone (even when GitHub itself doesn't support it # :D) url = '/repos/%s/issues/%s' % (config.upstream, args.issue) params = dict() # Should labels be cleared? if (args.labels and len(args.labels) == 1 and not args.labels[0]): params['labels'] = [] elif args.labels: params['labels'] = args.labels if args.state: params['state'] = args.state if args.assignee is not None: params['assignee'] = args.assignee if args.milestone is not None: params['milestone'] = args.milestone msg = args.message if args.edit_message: if not msg: issue = req.get(url) msg = issue['title'] if issue['body']: msg += '\n\n' + issue['body'] msg = cls.editor(msg) if msg: (title, body) = split_titled_message(msg) params['title'] = title params['body'] = body issue = req.patch(url, **params) cls.print_issue_summary(issue) class CommentCmd (IssueUtil): cmd_help = "add a comment to an existing issue" @classmethod def setup_parser(cls, parser): parser.add_argument('issue', metavar=cls.id_var, help="number identifying the %s to comment on" % cls.name) parser.add_argument('-m', '--message', metavar='MSG', help="comment to be added to the %s; if " "this option is not used, the default " "$GIT_EDITOR is opened to write the comment" % cls.name) @classmethod def run(cls, parser, args): body = args.message or cls.comment_editor() cls.clean_and_post_comment(args.issue, body) class CloseCmd (IssueUtil): cmd_help = "close an opened issue" @classmethod def setup_parser(cls, parser): parser.add_argument('issue', metavar=cls.id_var, help="number identifying the %s to close" % cls.name) parser.add_argument('-m', '--message', metavar='MSG', help="add a comment to the %s before " "closing it" % cls.name) parser.add_argument('-e', '--edit-message', action='store_true', default=False, help="open the default $GIT_EDITOR to write " "a comment to be added to the %s before " "closing it" % cls.name) @classmethod def run(cls, parser, args): msg = args.message if args.edit_message: msg = cls.comment_editor(msg) if msg: cls.clean_and_post_comment(args.issue, msg) issue = req.patch(cls.url(args.issue), state='closed') cls.print_issue_summary(issue) # Utility class that group common functionality used by the multiple `git hub # pull`) subcommands specifically. class PullUtil (IssueUtil): name = 'pull request' gh_path = 'pulls' id_var = 'PULL' rebase_msg = 'This pull request has been rebased via ' \ '`git hub pull rebase`. Original pull request HEAD ' \ 'was {}, new (rebased) HEAD is {}' @classmethod def get_ref(cls, ref='HEAD'): ref_hash = git('rev-parse ' + ref) ref_name = git('rev-parse --abbrev-ref ' + ref) if not ref_name or ref_name == 'HEAD' or ref_hash == ref_name: ref_name = None return ref_hash, ref_name @classmethod def tracking_branch(cls, head): if head is None: return None ref = git_config('branch.%s.merge' % head, prefix='') if ref is None: return None # the format is usually a full reference specification, like # "refs/heads/", we just assume the user is always # using a branch return ref.split('/')[-1] # push head to remote_head only if is necessary @classmethod def push(cls, head, remote_head, force): local_hash = git('rev-parse', head) remote_hash = 'x' # dummy variable that doesn't match any git hash remote_branch = '%s/%s' % (config.forkremote, head) if cls.branch_exists(remote_branch): remote_hash = git('rev-parse', remote_branch) if local_hash != remote_hash: infof('Pushing {} to {} in {}', head, remote_head, config.forkremote) git_push(config.forkremote, head+':refs/heads/'+remote_head, force=force) @classmethod def branch_exists(cls, branch): status = subprocess.call('git rev-parse --verify --quiet ' + 'refs/heads/' + branch + ' > /dev/null', shell=True) return status == 0 @classmethod def get_default_branch_msg(cls, branch_ref, branch_name): if branch_name is not None: msg = git_config('branch.%s.description' % branch_name, '', '') if msg: return msg return git('log -1 --pretty=format:%s%n%n%b ' + branch_ref) @classmethod def get_local_remote_heads(cls, parser, args): head_ref, head_name = cls.get_ref(args.head or 'HEAD') remote_head = args.create_branch or head_name if not remote_head: die("Can't guess remote branch name, please " "use --create-branch to specify one") base = args.base or cls.tracking_branch(head_name) or \ config.pullbase gh_head = config.username + ':' + remote_head return head_ref, head_name, remote_head, base, gh_head # `git hub pull` command implementation class PullCmd (IssueCmd): cmd_title = 'subcommands to manage pull requests' cmd_help = 'manage pull requests' # Most of the commands are just aliases to the git hub issue commands. # We derive from the PullUtil first to get the pull specific variables # (name, gh_path, id_var) with higher priority than the ones in the # IssueCmd subcommands. class ListCmd (PullUtil, IssueCmd.ListCmd): cmd_help = "show a list of open pull requests" pass class ShowCmd (PullUtil, IssueCmd.ShowCmd): cmd_help = "show details for existing pull requests" @classmethod def run(cls, parser, args): for n in args.issues: pull = req.get(cls.url(n)) if args.summary: cls.print_issue_summary(pull) continue # Damn GitHub doesn't provide labels for # pull request objects issue_url = '/repos/%s/issues/%s' % ( config.upstream, n) issue = req.get(issue_url) pull['labels'] = issue.get('labels', []) c = [] if pull['comments'] > 0: c = req.get(issue_url + '/comments') ic = [] if pull['review_comments'] > 0: ic = req.get(cls.url(n) + "/comments") cls.print_issue(pull, c, ic) class UpdateCmd (PullUtil, IssueCmd.UpdateCmd): cmd_help = "update an existing pull request" pass class CommentCmd (PullUtil, IssueCmd.CommentCmd): cmd_help = "add a comment to an existing pull request" pass class CloseCmd (PullUtil, IssueCmd.CloseCmd): cmd_help = "close an opened pull request" pass class NewCmd (PullUtil): cmd_help = "create a new pull request" @classmethod def setup_parser(cls, parser): parser.add_argument('head', metavar='HEAD', nargs='?', help="branch (or git ref) where your changes " "are implemented") parser.add_argument('-m', '--message', metavar='MSG', help="pull request title (and description); " "the first line is used as the pull request " "title and any text after an empty line is " "used as the optional body") parser.add_argument('-b', '--base', metavar='BASE', help="branch (or git ref) you want your " "changes pulled into (uses the tracking " "branch by default, or hub.pullbase if " "there is none, or 'master' as a fallback)") parser.add_argument('-c', '--create-branch', metavar='NAME', help="create a new remote branch with NAME " "as the real head for the pull request instead " "of using the HEAD name passed as 'head'") parser.add_argument('-f', '--force-push', action='store_true', default=False, help="force the push git operation (use with " "care!)") @classmethod def run(cls, parser, args): head_ref, head_name, remote_head, base, gh_head = \ cls.get_local_remote_heads(parser, args) msg = args.message if not msg: msg = cls.editor(cls.get_default_branch_msg( head_ref, head_name)) (title, body) = split_titled_message(msg) cls.push(head_name or head_ref, remote_head, force=args.force_push) infof("Creating pull request from branch {} to {}:{}", remote_head, config.upstream, base) pull = req.post(cls.url(), head=gh_head, base=base, title=title, body=body) cls.print_issue_summary(pull) class AttachCmd (PullUtil): cmd_help = "attach code to an existing issue (convert it " \ "to a pull request)" @classmethod def setup_parser(cls, parser): parser.add_argument('issue', metavar='ISSUE', help="pull request ID to attach code to") parser.add_argument('head', metavar='HEAD', nargs='?', help="branch (or git ref) where your changes " "are implemented") parser.add_argument('-m', '--message', metavar='MSG', help="add a comment to the new pull request") parser.add_argument('-e', '--edit-message', action='store_true', default=False, help="open the default $GIT_EDITOR to write " "a comment to be added to the pull request " "after attaching the code to it") parser.add_argument('-b', '--base', metavar='BASE', help="branch (or git ref) you want your " "changes pulled into (uses the tracking " "branch by default, or hub.pullbase if " "there is none, or 'master' as a fallback)") parser.add_argument('-c', '--create-branch', metavar='NAME', help="create a new remote branch with NAME " "as the real head for the pull request instead " "of using the HEAD name passed as 'head'") parser.add_argument('-f', '--force-push', action='store_true', default=False, help="force the push git operation (use with " "care!)") @classmethod def run(cls, parser, args): head_ref, head_name, remote_head, base, gh_head = \ cls.get_local_remote_heads(parser, args) msg = args.message if args.edit_message: if not msg: msg = cls.get_default_branch_msg( head_ref, head_name) msg = cls.comment_editor(msg) cls.push(head_name or head_ref, remote_head, force=args.force_push) infof("Attaching commits in branch {} to issue #{} " "(to be merged to {}:{})", remote_head, args.issue, config.upstream, base) pull = req.post(cls.url(), issue=args.issue, base=base, head=gh_head) cls.print_issue_summary(pull) if msg: cls.clean_and_post_comment(args.issue, msg) class CheckoutCmd (PullUtil): cmd_help = "checkout the remote branch (head) of the pull request" @classmethod def setup_parser(cls, parser): parser.add_argument('pull', help="number identifying the pull request to checkout") parser.add_argument("args", nargs=argparse.REMAINDER, help="any extra arguments to pass to `git checkout`") @classmethod def run(cls, parser, args): pull = req.get(cls.url(args.pull)) if pull['state'] == 'closed': warnf('Checking out a closed pull request ' '(closed at {closed_at})!', **pull) remote_url = pull['head']['repo'][config.urltype] remote_branch = pull['head']['ref'] infof('Fetching {} from {}', remote_branch, remote_url) git_quiet(1, 'fetch', remote_url, remote_branch) git_quiet(1, 'checkout', 'FETCH_HEAD', *args.args) # This class is top-level just for convenience, because is too big. Is added to # PullCmd after is completely defined! # # This command is by far the most complex part of this program. Since a rebase # can fail (because of conflicts) and the user gets a prompt back, there are # millions of possible situations when we try to resume the rebase. For this # reason this command is divided in several small methods that are reused as # much as possible (usually by using the command-line option `action` to figure # out what to do next). # # Error (exception) handling in these methods is EXTREMELY important too. If # anything fails badly, we need to restore the user repo to its original state, # but if the failure is because of conflicts, we only have to do partial # cleanup (or no cleanup at all). For this reason is the class variable # `in_conflict` defined. When we are recovering from a conflict, this flag is # set to true so the normal cleanup is not done (or done partially). class RebaseCmd (PullUtil): cmd_help = "close a pull request by rebasing its base branch" stash_msg_base = "stashed by git hub pull rebase" in_conflict = False # These variables are stored in the .git/HUB_PULL_REBASING file if the # rebasing was interrupted due to conflicts. When the rebasing is # resumed (via --continue or --skip) these variables are loaded from # that file. saved_old_ref = None saved_message = None saved_edit_msg = None saved_pause = None saved_delete_branch = None # this variable is a bit different, as is read/write by # read_rebasing_file()/create_rebasing_file() directly. This is not # ideal and should be addressed when #35 is fixed. in_pause = False @classmethod def setup_parser(cls, parser): group = parser.add_mutually_exclusive_group(required=True) group.add_argument('pull', metavar=cls.id_var, nargs='?', help="number identifying the pull request to rebase") group.add_argument('--continue', dest='action', action='store_const', const='--continue', help="continue an ongoing rebase") group.add_argument('--abort', dest='action', action='store_const', const='--abort', help="abort an ongoing rebase") group.add_argument('--skip', dest='action', action='store_const', const='--skip', help="skip current patch and continue") parser.add_argument('-m', '--message', metavar='MSG', help="add a comment to the pull request before closing " "it; if not specified a default comment is added (to " "avoid adding a comment at all use -m'')") parser.add_argument('-e', '--edit-message', action='store_true', default=False, help="open the default $GIT_EDITOR to edit the comment " "to be added to the pull request before closing it") parser.add_argument('--force-push', action='store_true', default=False, help="force the push git operation (use with care!)") parser.add_argument('-p', '--pause', action='store_true', default=False, help="pause the rebase just before the results are " "pushed (useful for testing)") parser.add_argument('-u', '--stash-include-untracked', action='store_true', default=False, help="uses git stash save --include-untracked when " "stashing local changes") parser.add_argument('-a', '--stash-all', action='store_true', default=False, help="uses git stash save --all when stashing local " "changes") parser.add_argument('-D', '--delete-branch', action='store_true', default=False, help="removes the PR branch, like the Delete " "Branch button (TM)") @classmethod def run(cls, parser, args): ongoing_rebase_pull_id = cls.read_rebasing_file() if args.pull is not None and ongoing_rebase_pull_id is not None: die("Another pull rebase is in progress, can't start " "a new one") if (args.pull is None and ongoing_rebase_pull_id is None and args.action != '--abort'): die("Can't {}, no pull rebase is in progress", args.action) if args.pull is not None: if cls.rebasing(): die("Can't start a pull rebase while a " "regular reabase is in progress") cls.start_rebase(args) else: args.pull = ongoing_rebase_pull_id if args.message is None: args.message = cls.saved_message if not args.edit_message: args.edit_message = cls.saved_edit_msg if not args.pause: args.pause = cls.saved_pause if not args.delete_branch: args.delete_branch = cls.saved_delete_branch if args.action == '--abort': cls.abort_rebase(args) else: cls.check_continue_rebasing(args) cls.start_rebase(args) # Check if we are able to continue an ongoing rebase (if we can't for # any reason, we quit, this function only returns on success). On some # conditions the user is asked about what to do. @classmethod def check_continue_rebasing(cls, args): if cls.rebasing(): return head_ref, head_name = cls.get_ref() if args.action == '--continue' and \ cls.get_tmp_ref(args.pull) == head_name: return answer = ask("No rebase in progress found for this pull " "rebase, do you want to continue as if the rebase was " "successfully finished, abort the rebasing cancelling " "the whole rebase or just quit?", default="quit", options=['continue', 'abort', 'quit']) if answer is None: die("No rebase in progress found for this " "pull rebase, don't know how to proceed") if answer == 'abort': cls.abort_rebase(args) sys.exit(0) # Abort an ongoing rebase by trying to return to the state the repo was # before the rebasing started (reset to the previous head, remove # temporary branches, restore the stashed changes, etc.). @classmethod def abort_rebase(cls, args): aborted = cls.force_rebase_abort() if args.pull is not None and cls.saved_old_ref is not None: cls.clean_ongoing_rebase(args.pull, cls.saved_old_ref, warnf) aborted = True aborted = cls.remove_rebasing_file() or aborted aborted = cls.pop_stashed() or aborted if not aborted: die("Nothing done, maybe there isn't an ongoing rebase " "of a pull request?") # Tries to (restart a rebase (or continue it, depending on the # args.action). If is starting a new rebase, it stash any local changes # and creates a branch, rebase, etc. @classmethod def start_rebase(cls, args): starting = args.action is None pull = cls.get_pull(args.pull, starting) if starting: cls.stash(pull, args) try: pushed_sha = cls.fetch_rebase_push(args, pull) finally: if not cls.in_conflict and not cls.in_pause: cls.pop_stashed() try: pull = cls.update_github(args, pull, pushed_sha) except (OSError, IOError) as e: errf("GitHub information couldn't be updated " "correctly, but the pull request was " "successfully rebased ({}).", e) raise e finally: cls.print_issue_summary(pull) # Get the pull request object from GitHub performing some sanity checks # (if it's already merged, or closed or in a mergeable state). If # the state is not the ideal, it asks the user how to proceed. # # If `check_all` is False, the only check performed is if it is already # merged). # # If the pull request can't be merged (or the user decided to cancel), # this function terminates the program (it returns only on success). @classmethod def get_pull(cls, pull_id, check_all): pull = req.get(cls.url(pull_id)) if pull['merged']: infof("Nothing to do, already merged (--abort to get " "back to normal)") sys.exit(0) if not check_all: return pull if pull['state'] == 'closed': answer = ask("The pull request is closed, are you sure " "you want to rebase it?", default="no") if answer is None: die("Can't rebase/merge, pull request is closed") elif answer == 'no': sys.exit(0) if not pull['mergeable']: answer = ask("The pull request is not in a mergeable " "state (there are probably conflicts to " "resolve), do you want to continue anyway?", default="no") if answer is None: die("Can't continue, the pull request isn't " "in a mergeable state") elif answer == 'no': sys.exit(0) # Check status status = req.get('/repos/%s/commits/%s/status' % (config.upstream, pull['head']['sha'])) state = status['state'] statuses = status['statuses'] if len(statuses) > 0 and state != 'success': url = statuses[0]['target_url'] answer = ask("The current pull request status is '%s' " "(take a look at %s for more information), " "do you want to continue anyway?" % (state, url), default="no") if answer is None: die("Can't continue, the pull request status " "is '{}' (take a look at {} for more " "information)", state, url) elif answer == 'no': sys.exit(0) return pull # Returns the name of the temporary branch to work on while doing the # rebase @classmethod def get_tmp_ref(cls, pull_id): return 'git-hub-pull-rebase-%s' % pull_id # Pop stashed changes (if any). Warns (but doesn't pop the stashed # changes) if they are present in the stash, but not in the top. # # Returns True if it was successfully popped, False otherwise. @classmethod def pop_stashed(cls): stashed_index = cls.get_stashed_state() if stashed_index == 0: git_quiet(2, 'stash', 'pop') return True elif stashed_index > 0: warnf("Stash produced by this command found " "(stash@{{}}) but not as the last stashed " "changes, leaving the stashed as it is", stashed_index) return False # Returns the index of the stash created by this program in the stash # in the stack, or None if not present at all. 0 means is the latest # stash in the stash stack. @classmethod def get_stashed_state(cls): stash_msg_re = re.compile(r'.*' + cls.stash_msg_base + r' \d+') stashs = git('stash', 'list').splitlines() for i, stash in enumerate(stashs): if stash_msg_re.match(stash): return i return None # Returns a string with the message to use when stashing local changes. @classmethod def stash_msg(cls, pull): return '%s %s' % (cls.stash_msg_base, pull['number']) # Do a git stash using the required options @classmethod def stash(cls, pull, args): git_args = [2, 'stash', 'save'] if args.stash_include_untracked: git_args.append('--include-untracked') if args.stash_all: git_args.append('--all') git_args.append(cls.stash_msg(pull)) try: git_quiet(*git_args) except GitError as e: errf("Couldn't stash current changes, try with " "--stash-include-untracked or --stash-all " "if you had conflicts") raise e # Performs the whole rebasing procedure, including fetching the branch # to be rebased, creating a temporary branch for it, fetching the base # branch, rebasing to the base branch and pushing the results. # # The number of operations performed can vary depending on the # `args.action` (i.e. if we are --continue'ing or --skip'ping # a rebase). If the rebase is being continued, only the steps starting # with the rebasing itself are performed. # # Returns the new HEAD hash after the rebase is completed. @classmethod def fetch_rebase_push(cls, args, pull): if pull['head']['repo'] is None: die("It seems like the repository referenced by " "this pull request has been deleted") starting = args.action is None head_url = pull['head']['repo'][config.urltype] head_ref = pull['head']['ref'] base_url = pull['base']['repo'][config.urltype] base_ref = pull['base']['ref'] tmp_ref = cls.get_tmp_ref(pull['number']) old_ref = cls.saved_old_ref if old_ref is None: old_ref_ref, old_ref_name = cls.get_ref() old_ref = old_ref_name or old_ref_ref if starting: infof('Fetching {} from {}', head_ref, head_url) git_quiet(1, 'fetch', head_url, head_ref) git_quiet(1, 'checkout', '-b', tmp_ref, 'FETCH_HEAD') try: if starting: infof('Rebasing to {} in {}', base_ref, base_url) git_quiet(1, 'fetch', base_url, base_ref) cls.create_rebasing_file(pull, args, old_ref) # Only run the rebase if we are not continuing with # a pull rebase that the user finished rebasing using # a plain git rebase --continue if starting or cls.rebasing(): cls.rebase(args, pull) if args.pause and not cls.in_pause: cls.in_pause = True # Re-create the rebasing file as we are going # to pause cls.remove_rebasing_file() cls.create_rebasing_file(pull, args, old_ref) interrupt("Rebase done, now --pause'ing. " 'Use --continue {}when done.', '' if starting else 'once more ') # If we were paused, remove the rebasing file that we # just re-created if cls.in_pause: cls.in_pause = False cls.remove_rebasing_file() infof('Pushing results to {} in {}', base_ref, base_url) git_push(base_url, 'HEAD:' + base_ref, force=args.force_push) if args.delete_branch: infof('Removing pull request branch {} in {}', head_ref, head_url) git_push(head_url, ':' + head_ref, force=args.force_push) return git('rev-parse HEAD') finally: if not cls.in_conflict and not cls.in_pause: cls.clean_ongoing_rebase(pull['number'], old_ref) # Reverts the operations done by fetch_rebase_push(). @classmethod def clean_ongoing_rebase(cls, pull_id, old_ref, errfunc=die): tmp_ref = cls.get_tmp_ref(pull_id) git_quiet(1, 'reset', '--hard') try: git_quiet(1, 'checkout', old_ref) except subprocess.CalledProcessError as e: errfunc("Can't checkout '{}', maybe it was removed " "during the rebase? {}", old_ref, e) if cls.branch_exists(tmp_ref): git('branch', '-D', tmp_ref) # Performs the rebasing itself. Sets the `in_conflict` flag if # a conflict is detected. @classmethod def rebase(cls, args, pull): starting = args.action is None try: if starting: a = [] if config.forcerebase: a.append('--force') a.append('FETCH_HEAD') git_quiet(1, 'rebase', *a) else: git('rebase', args.action) except subprocess.CalledProcessError as e: if e.returncode == 1 and cls.rebasing(): cls.in_conflict = True die("Conflict detected, resolve " "conflicts and run git hub " "pull rebase --continue to " "proceed") raise e finally: if not cls.in_conflict: # Always try to abort the rebasing, in case # there was an error cls.remove_rebasing_file() cls.force_rebase_abort() # Run git rebase --abort without complaining if it fails. @classmethod def force_rebase_abort(cls): try: git('rebase', '--abort', stderr=subprocess.STDOUT) except subprocess.CalledProcessError: return False return True # Do all the GitHub part of the rebasing (closing the rebase, adding # a comment including opening the editor if needed to get the message). @classmethod def update_github(cls, args, pull, pushed_sha): pull_sha = pull['head']['sha'] msg = args.message if msg is None and pull_sha != pushed_sha: msg = cls.rebase_msg.format(pull_sha, pushed_sha) if args.edit_message: msg = cls.comment_editor(msg) if msg: cls.clean_and_post_comment(args.pull, msg) pull = req.get(cls.url(args.pull)) if pull['state'] == 'open' and pull_sha != pushed_sha: pull = req.patch(cls.url(args.pull), state='closed') return pull # Returns True if there is a (pure) `git rebase` going on (not # necessarily a `git hub pull rebase`). @classmethod def rebasing(cls): return os.path.exists(git_dir()+'/rebase-apply/rebasing') # Returns the file name used to store `git hub pull rebase` metadata # (the sole presence of this file indicates there is a `git hub pull # rebase` going on). @classmethod def rebasing_file_name(cls): return os.path.join(git_dir(), 'HUB_PULL_REBASING') # Reads and parses the contents of the `rebasing_file`, returning them # as variables, if the file exists. # # If the file exists, the class variables `saved_old_ref`, # `saved_edit_msg` and `saved_message` are filled with the file # contents and the pull request ID that's being rebased is returned. # Otherwise it just returns None and leaves the class variables alone. @classmethod def read_rebasing_file(cls): fname = cls.rebasing_file_name() if os.path.exists(fname): try: with file(fname) as f: # id read as string pull_id = f.readline()[:-1] # strip \n cls.saved_old_ref = f.readline()[:-1] assert cls.saved_old_ref pause = f.readline()[:-1] cls.saved_pause = (pause == "True") delete_branch = f.readline()[:-1] cls.saved_delete_branch = (delete_branch == "True") in_pause = f.readline()[:-1] cls.in_pause = (in_pause == "True") edit_msg = f.readline()[:-1] cls.saved_edit_msg = (edit_msg == "True") msg = f.read() if msg == '\n': msg = '' elif not msg: msg = None cls.saved_message = msg return pull_id except EnvironmentError as e: die("Error reading pull rebase information " "file '{}': {}", fname, e) return None # Creates the `rebasing_file` storing: the `pull` ID, the `old_ref` # (the hash of the HEAD commit before the rebase was started), # the `args.edit_message` flag and the `args.message` text. It fails # (and exits) if the file was already present. @classmethod def create_rebasing_file(cls, pull, args, old_ref): fname = cls.rebasing_file_name() try: fd = os.open(cls.rebasing_file_name(), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o777) with os.fdopen(fd, 'w') as f: # id written as string f.write(str(pull['number']) + '\n') f.write(old_ref + '\n') f.write(repr(args.pause) + '\n') f.write(repr(args.delete_branch) + '\n') f.write(repr(cls.in_pause) + '\n') f.write(repr(args.edit_message) + '\n') if (args.message is not None): f.write(args.message + '\n') except EnvironmentError as e: die("Error writing pull rebase information " "file '{}': {}", fname, e) # Removes the `rebasing_file` from the filesystem. # # Returns True if it was successfully removed, False if it didn't exist # and terminates the program if there is another I/O error. @classmethod def remove_rebasing_file(cls): fname = cls.rebasing_file_name() if not os.path.exists(fname): return False try: os.unlink(fname) except EnvironmentError as e: die("Error removing pull rebase information " "file '{}': {}", fname, e) return True # and we finally add it to the PullCmd class as a member PullCmd.RebaseCmd = RebaseCmd # `git hub` command implementation class HubCmd (CmdGroup): cmd_title = "subcommands" cmd_help = "git command line interface to GitHub" SetupCmd = SetupCmd CloneCmd = CloneCmd IssueCmd = IssueCmd PullCmd = PullCmd def main(): global args, config, req, verbose parser = argparse.ArgumentParser( description='Git command line interface to GitHub') parser.add_argument('--version', action='version', version=VERSION) parser.add_argument('-v', '--verbose', action='count', default=INFO, help="be more verbose (can be specified multiple times to get " "extra verbosity)") parser.add_argument('-s', '--silent', action='count', default=0, help="be less verbose (can be specified multiple times to get " "less verbosity)") partial = HubCmd.setup_parser(parser) if partial: args, unknown_args = parser.parse_known_args() args.unknown_args = unknown_args else: args = parser.parse_args() verbose = args.verbose - args.silent config = Config() req = RequestManager(config.baseurl, config.oauthtoken) # Temporary warning to note the configuration variable changes if git_config('password') is not None: warnf('It looks like your {0}password configuration ' 'variable is set.\nThis variable is not used ' 'anymore, you might want to delete it.\nFor example: ' 'git config --global --unset {0}password', GIT_CONFIG_PREFIX) args.run(parser, args) # Entry point of the program, just calls main() and handle errors. if __name__ == '__main__': try: main() except urllib2.HTTPError as error: try: body = error.read() err = json.loads(body) prefix = 'GitHub error: ' error_printed = False if 'message' in err: errf('{}{message}', prefix, **err) error_printed = True if 'errors' in err: for e in err['errors']: if 'message' in err: errf('{}{message}', ' ' * len(prefix), **e) error_printed = True if not error_printed: errf('{} for {}: {}', error, error.geturl(), body) else: debugf('{}', error) debugf('{}', error.geturl()) debugf('{}', pformat(error.headers)) debugf('{}', error.read()) except: errf('{}', error) errf('{}', error.geturl()) errf('{}', pformat(error.headers)) errf('{}', body) sys.exit(3) sys.exit(4) except urllib2.URLError as error: errf('Network error: {}', error) sys.exit(5) except KeyboardInterrupt: sys.exit(6) except GitError as error: errf('{} failed (return code: {})', ' '.join(error.cmd), error.returncode) if verbose >= ERR: sys.stderr.write(error.output + '\n') sys.exit(7) except InterruptException as error: infof('{}', error) sys.exit(0) git-hub-0.9.0/man.rst000066400000000000000000000444411252641060700143760ustar00rootroot00000000000000======= git-hub ======= ------------------------------------ Git command line interface to GitHub ------------------------------------ :Author: Leandro Lucarella :Copyright: 2013 Sociomantic Labs GmbH :Version: devel :Date: |date| :Manual section: 1 :Manual group: Git Manual .. |date| date:: SYNOPSYS ======== git hub [global options] [options] [arguments] DESCRIPTION =========== `git hub` is a simple command line interface to github, enabling most useful GitHub tasks (like creating and listing pull request or issues) to be accessed directly through the git command line. To use this command you'll probably need to make an initial configuration to get authorization from GitHub. To do this you can use the `setup` command. See the CONFIGURATION_ section for more configuration options. GLOBAL OPTIONS ============== \-h, --help Show this help and exit. \--version Show program's version number and exit. \-v, --verbose Be more verbose (can be specified multiple times to get extra verbosity) \-s, --silent Be less verbose (can be specified multiple times to get less verbosity) COMMANDS ======== `setup` This command performs an initial setup to connect to GitHub. It basically asks GitHub for an authorization token and stores it in the configuration variable `hub.oauthtoken` for future use so you don't need to type your password each time (or store it in the config). The username is also stored for future use in the `hub.username` variable. If the base URL is specified, it is stored in `hub.baseurl` too. \-u USERNAME, --username=USERNAME GitHub's username (login name), will be stored in the configuration variable `hub.username`. If an e-mail is provided, then a username matching that e-mail will be searched and used instead, if found (for this to work the e-mail must be part of the public profile). \-p PASSWORD, --password=PASSWORD GitHub's password (will not be stored). \-b URL, --baseurl=URL GitHub's base URL to use to access the API. Set this when you GitHub API is in another location other than the default (Enterprise servers usually use https://host/api/v3). \--global Store settings in the global configuration (see --global option in `git config(1)` for details). \--system Store settings in the system configuration (see --system option in `git config(1)` for details). `clone` REPO [DEST] This command is used to clone **REPO**, a GitHub repository, to a **DEST** directory (defaults to the name of the project being cloned). If the repository is specified in */* form, the **REPO** will be used as upstream and a personal fork will be looked up. If none is found, a new fork will be created. In both cases, the fork will be cloned instead of the upstream repository. If only ** is specified as **REPO**, then the configuration `hub.username` is used as **, and the parent repository is looked up at GitHub to determine the real upstream repository. The upstream repository is also added as a remote by the name `upstream` (unless **--triangular** is used, in which case the remote is called `fork` by default) and the `hub.upstream` configuration variable is set (see CONFIGURATION_), unless only ** was used and the resulting repository is not really a fork, in which case is impossible to automatically determine the upstream repository. \-r NAME, --remote=NAME Use `NAME` as the upstream remote repository name instead of the default ('fork' if **--triangular** is used, 'upstream' otherwise). \-t, --triangular Use Git's *triangular workflow* configuration. This option clones from the parent/upstream repository instead of cloning the fork, and adds the fork as a remote repository. Then sets the `remote.pushdefault` Git option and `hub.forkremote` git-hub option to the fork. The effect of this having the upstream repository used by default when you pull but using your fork when you push, which is typically what you want when using GitHub's pull requests. Git version 1.8.3 or newer is needed to use this option (and 1.8.4 or newer is recommended due to some issues in 1.8.3 related to this). This option might become the default in the future. To make it the default you can set the option `hub.triangular`. See CONFIGURATION_ for details. GIT CLONE OPTIONS Any standard **git clone** option can be passed. Not all of them might make sense when cloning a GitHub repo to be used with this tool though. `issue` This command is used to manage GitHub issues through a set of subcommands. Is no subcommand is specified, `list` is used. `list` Show a list of open issues. \-c, --closed Show closed issues instead. \-C, --created-by-me Show only issues created by me \-A, --assigned-to-me Show only issues assigned to me `show` ISSUE [ISSUE ...] Show issues identified by **ISSUE**. `new` Create a new issue. \-m MSG, --message=MSG Issue title (and description). The first line is used as the issue title and any text after an empty line is used as the optional body. If this option is not used, the default `GIT_EDITOR` is opened to write one. \-l LABEL, --label=LABEL Attach `LABEL` to the issue (can be specified multiple times to set multiple labels). \-a USER, --assign=USER` Assign an user to the issue. `USER` must be a valid GitHub login name. \-M ID, --milestone=ID Assign the milestone identified by the number ID to the issue. `update` ISSUE Similar to `new` but update an existing issue identified by **ISSUE**. A convenient shortcut to close an issue is provided by the `close` subcommand. \-m MSG, --message=MSG New issue title (and description). The first line is used as the issue title and any text after an empty line is used as the optional body. \-e, --edit-message Open the default `GIT_EDITOR` to edit the current title (and description) of the issue. \-o, --open Reopen the issue. \-c, --close Close the issue. \-l LABEL, --label=LABEL If one or more labels are specified, they will replace the current issue labels. Otherwise the labels are unchanged. If one of the labels is empty, the labels will be cleared (so you can use **-l''** to clear the labels of an issue. \-a USER, --assign=USER Assign an user to the issue. `USER` must be a valid GitHub login name. \-M ID, --milestone=ID Assign the milestone identified by the number ID to the issue. `comment` ISSUE Add a new comment to an existing issue identified by **ISSUE**. \-m MSG, --message=MSG Comment to be added to the issue. If this option is not used, the default `GIT_EDITOR` is opened to write the comment. `close` ISSUE Alias for `update --close`. (+ `comment` if **--message** or **--edit-message** is specified). Closes issue identified by **ISSUE**. \-m MSG, --message=MSG Add a comment to the issue before closing it. \-e, --edit-message Open the default `GIT_EDITOR` to write a comment to be added to the issue before closing it. `pull` This command is used to manage GitHub pull requests. Since pull requests in GitHub are also issues, most of the subcommands are repeated from the `issue` command for convenience. Only the `list` and `new` commands are really different, and `attach` and `rebase` are added. `list` Show a list of open pull requests. \--closed Show closed pull requests instead. `show` PULL [PULL ...] Alias for `issue show`. `checkout` PULL ... Checkout the remote branch (head) of the pull request. This command first fetches the *head* reference from the pull request and then calls the standard `git checkout` command and any extra argument will be passed to `git checkout` as-is, after the reference that was just fetched. Remember this creates a detached checkout by default, use `-b` if you want to create a new branch based on the pull request. Please take a look at `git checkout` help for more details. `new` [HEAD] Create a new pull request. If **HEAD** is specified, it will be used as the branch (or git ref) where your changes are implemented. Otherwise the current branch is used. If the branch used as head is not pushed to your fork remote, a push will be automatically done before creating the pull request. The repository to issue the pull request from is taken from the `hub.forkrepo` configuration, which defaults to *hub.username/*. \-m MSG, --message=MSG Pull request title (and description). The first line is used as the pull request title and any text after an empty line is used as the optional body. If this option is not used, the default `GIT_EDITOR` is opened. If the HEAD branch have a proper description (see `git branch --edit-description`), that description will be used as the default message in the editor and if not, the message of the last commit will be used instead. \-b BASE, --base=BASE Branch (or git ref) you want your changes pulled into. By default the tracking branch (`branch..merge` configuration variable) is used or the configuration `hub.pullbase` if not tracking a remote branch. If none is present, it defaults to **master**. The repository to use as the base is taken from the `hub.upstream` configuration. \-c NAME, --create-branch=NAME Create a new remote branch with (with name **NAME**) as the real head for the pull request instead of using the HEAD name passed as **HEAD**. This is useful to create a pull request for a hot-fix you committed to your regular HEAD without creating a branch first. \-f, --force-push Force the push operations. Use with care! `attach` ISSUE [HEAD] Convert the issue identified by **ISSUE** to a pull request by attaching commits to it. The branch (or git ref) where your changes are implementedhead can be optionally specified with **HEAD** (otherwise the current branch is used). This subcommand is very similar to the `new` subcommand, please refer to it for more details. Please note you can only attach commits to issues if you have commit access to the repository or if you are assigned to the issue. \-m MSG, --message=MSG Add a comment to the issue/new pull request. \-e, --edit-message Open the default `GIT_EDITOR` to write a comment to be added to the issue/new pull request. The default message is taken from the **--message** option if present, otherwise the branch description or the first commit message is used as with the `new` subcommand. \-b BASE, --base=BASE Base branch to which issue the pull request. If this option is not present, then the base branch is taken from the configuration `hub.pullbase` (or just **master** if that configuration is not present either). The repository to use as the base is taken from the `hub.upstream` configuration. \-c NAME, --create-branch=NAME Create a new remote branch with (with name **NAME**) as the real head for the pull request instead of using the HEAD name passed as **HEAD**. This is useful to create a pull request for a hot-fix you committed to your regular HEAD without creating a branch first. \-f, --force-push Force the push operations. Use with care! `rebase` PULL Close a pull request identified by **PULL** by rebasing its base branch (specified in the pull request) instead of merging as GitHub's *Merge Button™* would do. If the operation is successful, a comment will be posted informing the new HEAD commit of the branch that has been rebased and the pull request will be closed. The type of URL used to fetch and push can be specified through the `hub.pullurltype` configuration variable (see CONFIGURATION_ for more details). Your working copy should stay the same ideally, if everything went OK. The operations performed by this subcommand are roughly these: 1. git stash 2. git fetch `pullhead` 3. git checkout -b `tmp` FETCH_HEAD 4. git pull --rebase `pullbase` 5. git push `pullbase` 6. git checkout `oldhead` 7. git branch -D `tmp` 8. git stash pop If `hub.forcerebase` is set to "true" (the default), ``--force`` will be passed to rebase (not to be confused with this command option ``--force-push`` which will force the push), otherwise (if is "false") a regular rebase is performed. When the rebase is forced, all the commits in the pull request are re-committed, so the Committer and CommitterDate metadata is updated in the commits, showing the person that performed the rebase and the time of the rebase instead of the original values, so providing more useful information. As a side effect, the hashes of the commits will change. If conflicts are found, the command is interrupted, similarly to how `git rebase` would do. The user should either **--abort** the rebasing, **--skip** the conflicting commit or resolve the conflict and **--continue**. When using one of these actions, you have to omit the **PULL** argument. \-m MSG, --message=MSG Use this message for the comment instead of the default. Specify an empty message (**-m''**) to completely omit the comment. \-e, --edit-message Open the default `GIT_EDITOR` to write the comment. \--force-push Force the push operations. Use with care! \-p, --pause Pause the rebase just before the results are pushed and the issue is merged. To resume the pull request rebasing (push the changes upstream and close the issue), just use the **--continue** action. This is particularly useful for testing. \-u, --stash-include-untracked Passes the **--include-untracked** option to stash. If used all untracked files are also stashed and then cleaned up with git clean, leaving the working directory in a very clean state, which avoid conflicts when checking out the pull request to rebase. \-a, --stash-all Passes the **--all** option to stash. Is like **--stash-include-untracked** but the ignored files are stashed and cleaned in addition to the untracked files, which completely removes the possibility of conflicts when checking out the pull request to reabase. \-D, --delete-branch Delete the pull request branch if the rebase was successful. This is similar to press the "Delete Branch" Button (TM) in the web interface after merging. Actions: \--continue Continue an ongoing rebase. \--abort Abort an ongoing rebase. \--skip Skip current patch in an ongoing rebase and continue. `update` Alias for `issue update`. `comment` Alias for `issue comment`. `close` Alias for `issue close`. CONFIGURATION ============= This program use the git configuration facilities to get its configuration from. These are the git config keys used: `hub.username` Your GitHub username. [default: *current OS username*] `hub.oauthtoken` required This is the authorization token obtained via the `setup` command. Even when required, you shouldn't need to set this variable manually. Use the `setup` command instead. `hub.upstream` required Blessed repository used to get the issues from and make the pull requests to. The format is */*. This option can be automatically set by the `clone` command and is not really required by it or the `setup` command. `hub.forkrepo` Your blessed repository fork. The format is */*. Used to set the head for your pull requests. [defaul: */(upstream part)*] `hub.forkremote` Remote name for accessing your fork. Used to push branches before creating a pull request. [default: *origin*] `hub.pullbase` Default remote branch (or git reference) you want your changes pulled into when creating a pull request. [default: *master*] `hub.urltype` Type of URL to use when an URL from a GitHub API is needed (for example, when 'pull rebase' is used). At the time of writing it could be *ssh_url* or *clone_url* for HTTP). See GitHub's API documentation[1] for more details or options. [default: *ssh_url*] `hub.baseurl` GitHub's base URL to use to access the API. Set this when you GitHub API is in another location other than the default (Enterprise servers usually use https://host/api/v3). This will be prepended to all GitHub API calls and it has to be a full URL, not just something like "www.example.com/api/v3/". `hub.forcerebase` If is set to "true", ``--force`` will be passed to rebase. If is set to "false" a regular rebase is performed. See the `pull` `rebase` command for detils. [default: *true*] `hub.triangular` Makes **--triangular** for `clone` if set to "true" (boolean value). See `clone` documentation for details. [1] http://developer.github.com/v3/pulls/#get-a-single-pull-request FILES ===== This program creates some temporary files in the '.git' directory during its operation. The contents of these files can be used for debugging/recovery purposes if necessary. `HUB_EDITMSG` This file is used to take input from the user, e.g. issue comments, pull request title & description etc. If, after accepting user input, the command given by the user fails for some reason, then the entered text can still be retrieved from this file. `HUB_PULL_REBASING` This file is used to store various metadata information related to a rebase operation (with the primary aim of being able to rollback the repository to its original state if the rebase fails or is interrupted due to conflicts). The sole presence of this file indicates that a rebase is in progress. VIM SYNTAX HIGHLIGHT ==================== A VIM ftdetect plugin is provided, to enable it you have to follow some steps though. All you need to do is copy (or preferably make a symbolic link) the script to `~/.vim/ftdetect/githubmsg.vim`:: mkdir -p ~/.vim/ftdetect ln -s /usr/share/vim/addons/githubmsg.vim ~/.vim/ftdetect/ # or if you are copying from the sources: # ln -s ftdetect.vim ~/.vim/ftdetect/githubmsg.vim .. vim: set et sw=2 :