diff options
author | Matteo F. Vescovi <mfv@debian.org> | 2018-06-11 19:23:49 -0300 |
---|---|---|
committer | Matteo F. Vescovi <mfv@debian.org> | 2018-06-11 19:23:49 -0300 |
commit | 7dd1e35e345b4a318efcb14a25a8f5e3fdd0bd9e (patch) | |
tree | a9a20af2b86de431bfb9a1fefb82545c298d23a3 |
Import magithub_0.1.7.orig.tar.xz
[dgit import orig magithub_0.1.7.orig.tar.xz]
46 files changed, 7840 insertions, 0 deletions
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..34e18c0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,7 @@ +# Thanks for helping out! + +Reporting an error? Oh no! I'm happy to take a look at it, but *please* provide the backtrace in your issue: before triggering the error make sure `debug-on-error` is set to `t` (interactively, you can use `M-x toggle-debug-on-error`). Then, just copy the backtrace that's provided :smile: + +# Feature Requests + +Always welcome! Please be specific about what you want, though. A little bit of code can go a long way :wink: (and a pull request goes even further!) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..8f350f6 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ +Thanks for contributing! It's really appreciated :smile: + +Here are some things to remember: + +- In the past, commit messages have mentioned issues directly. I've found this to be more trouble than it's worth -- especially when code goes under review and is edited and subsequently rebased. Try to leave issue/PR references out of commit messages -- opting instead to use the PR body to link the necessary records in the bug tracker. + +- If you're fixing a bug or implementing a new feature, add something to the appropriate `RelNotes/*.org` file. + +- If it's a breaking change (i.e., it could reasonably break someone's configuration), document that in the release notes as well. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..11ee388 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.elc +*~ +/.cask +/magithub.html +/magithub.pdf +/magithub/ +.elpa/ +/build.log +/magithub-autoloads.el diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..76a2976 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,35 @@ +language: emacs-lisp +sudo: required +dist: trusty +cache: +- directories: + - "$HOME/emacs" +matrix: + fast_finish: true + allow_failures: + - env: EMACS_VERSION=snapshot + - env: EMACS_VERSION=26.1 MELPA_STABLE=true +env: + matrix: + - EMACS_VERSION=25.1 + - EMACS_VERSION=25.2 + - EMACS_VERSION=25.3 + - EMACS_VERSION=26.1 + - EMACS_VERSION=26.1 MELPA_STABLE=true + - EMACS_VERSION=snapshot +before_install: +- make setup-CI +install: +- make install +script: +- make --keep-going test +notifications: + email: + on_success: never + on_failure: change + webhooks: + urls: + - https://webhooks.gitter.im/e/b1163bae60c65660fbd2 + on_success: change + on_failure: always + on_start: never diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..be51f79 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,57 @@ +# Contributing + +First off, thanks for contributing to this project. It's people like +you -- your time and effort in reporting bugs, suggesting features, or +submitting pull requests -- that make this project and so many others +like it in the Emacs world a joy to work with. Stay awesome! + +## Roadmap + +Magithub's vision is to become the bridge between the `git` VCS and +GitHub social network. Not only do I want to replicate the standard +functionality you would expect from a GitHub client, but I want to +closely *integrate* Magit's workflows with GitHub's featureset to +develop and optimize the broader experience of using `git` with other +people. + +Magit itself may include such support in the future, though probably +to a less-specialized extent. At present, Magithub is focusing on +GitHub (although the lessons learned here could be applied to a +Magitlab, for instance). + +## Reporting Bugs + +Ugh, nasty bugs! Every software project has them (except +[TeX, vπ][tex-bug]), and many of them are found only by users like +you. As you write your issue, please follow the instructions the +issue template provides. A stack trace helps tremendously! + +Sometimes there are intermittent bugs that cannot be reproduced +easily. Anyone who develops software can tell you that it is very +difficult to debug an issue that you cannot see. For this reason, the +'unconfirmed' label indicates an issue that hasn't been reproduced by +a maintainer and the 'waiting' label indicates an issue is waiting for +some response from the folks who are actually experiencing it. Any +issue that has had the 'waiting' label for more than two weeks can be +closed as 'not reproducible'. If you are still having the issue after +that time, please do re-open the issue! I don't mean to say that bugs +are features, but I don't want to give a false first impression of +bugginess. + +## Suggesting Features + +Feature requests are always welcome! Pull requests even more so. +:wink: Know however that this is a project I do in my free time; +sometimes life gets in the way of doing this development -- or even +reviewing development from a pull request. Don't let that deter you +:smile: It *will* be reviewed. + +## Unit Tests + +Additions of more unit tests are always appreciated -- as well as +improvements to the overall unit test approach. The *only* thing I +would like to continue to avoid is making real API requests (since +this makes pull requests difficult), so please mock the response for +any such test you write. Reach out on Gitter if you need a hand. + +[tex-bug]: http://www.ntg.nl/maps/05/34.pdf @@ -0,0 +1,11 @@ +(source gnu) +(source melpa) + +(package-file "magithub.el") + +(files "magithub*.el") + +(development + (depends-on "cask") + (depends-on "ert") + (depends-on "ert-runner")) @@ -0,0 +1,675 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + 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
100 35147 100 35147 0 0 20460 0 0:00:01 0:00:01 --:--:-- 20470 + 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. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 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 <http://www.gnu.org/licenses/>. + +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: + + <program> Copyright (C) <year> <name of author> + 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 +<http://www.gnu.org/licenses/>. + + 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 +<http://www.gnu.org/philosophy/why-not-lgpl.html>. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..96d0c28 --- /dev/null +++ b/Makefile @@ -0,0 +1,112 @@ +-include config.mk + +PKG = magithub + +EMACS ?= emacs +EMACS_ARGS ?= + +ifndef ORG_LOAD_PATH +ORG_LOAD_PATH = -L ../dash +ORG_LOAD_PATH += -L ../org/lisp +ORG_LOAD_PATH += -L ../org/contrib/lisp +ORG_LOAD_PATH += -L ../ox-texinfo+ +endif + +INSTALL_INFO ?= $(shell command -v ginstall-info || printf install-info) +MAKEINFO ?= makeinfo +MANUAL_HTML_ARGS ?= --css-ref /assets/the.css + +EENVS = PACKAGE_FILE="magithub.el" +EENVS += PACKAGE_TESTS="test/magithub-test.el" +EENVS += PACKAGE_LISP="$(wildcard magithub*.el)" + +ifeq ($(MELPA_STABLE),true) +EENVS += PACKAGE_ARCHIVES="gnu melpa-stable" +else +EENVS += PACKAGE_ARCHIVES="gnu melpa" +endif + +EMAKE := $(EENVS) emacs -batch -l emake.el --eval "(emake (pop argv))" + +doc: info html html-dir pdf + +help: + $(info make doc - generate most manual formats) + $(info make texi - generate texi manual (see comments)) + $(info make info - generate info manual) + $(info make html - generate html manual file) + $(info make html-dir - generate html manual directory) + $(info make pdf - generate pdf manual) + @printf "\n" + +.PHONY: clean install build test + +clean: + rm -f *.elc + rm -rf .elpa/ + +emake.el: + wget 'https://raw.githubusercontent.com/vermiculus/emake.el/master/emake.el' + +.elpa/: emake.el + $(EMAKE) install + +install: .elpa/ + +build: install + $(EMAKE) compile ~error-on-warn + +test: build test-ert + +# run ERT tests +test-ert: emake.el + $(EMAKE) test ert + +setup-CI: + export PATH="$(HOME)/bin:$(PATH)" + wget 'https://raw.githubusercontent.com/flycheck/emacs-travis/master/emacs-travis.mk' + make -f emacs-travis.mk install_emacs + emacs --version + +info: $(PKG).info dir +html: $(PKG).html +pdf: $(PKG).pdf + +ORG_ARGS = --batch -Q $(ORG_LOAD_PATH) -l ox-extra -l ox-texinfo+.el +ORG_EVAL = --eval "(ox-extras-activate '(ignore-headlines))" +ORG_EVAL += --eval "(setq indent-tabs-mode nil)" +ORG_EVAL += --eval "(setq org-src-preserve-indentation nil)" +ORG_EVAL += --funcall org-texinfo-export-to-texinfo + +# This target first bumps version strings in the Org source. The +# necessary tools might be missing so other targets do not depend +# on this target and it has to be run explicitly when appropriate. +# +# AMEND=t make texi Update manual to be amended to HEAD. +# VERSION=N make texi Update manual for release. +# +.PHONY: texi +texi: + @$(EMACS) $(ORG_ARGS) $(PKG).org $(ORG_EVAL) + @printf "\n" >> $(PKG).texi + @rm -f $(PKG).texi~ + +%.info: %.texi + @printf "Generating $@\n" + @$(MAKEINFO) --no-split $< -o $@ + +dir: $(PKG).info + @printf "Generating $@\n" + @printf "%s" $^ | xargs -n 1 $(INSTALL_INFO) --dir=$@ + +%.html: %.texi + @printf "Generating $@\n" + @$(MAKEINFO) --html --no-split $(MANUAL_HTML_ARGS) $< + +html-dir: $(PKG).texi + @printf "Generating $(PKG)/*.html\n" + @$(MAKEINFO) --html $(MANUAL_HTML_ARGS) $< + +%.pdf: %.texi + @printf "Generating $@\n" + @texi2pdf --clean $< > /dev/null diff --git a/README.md b/README.md new file mode 100644 index 0000000..31a31df --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +<a href="screenshots.md"><img align="right" src="https://github.com/vermiculus/magithub/raw/master/images/status.png" width="50%" alt="Overview -- the status buffer"/></a> + +# Magithub + +[![MELPA Status](http://melpa.milkbox.net/packages/magithub-badge.svg)](http://melpa.milkbox.net/#/magithub) +[![Build Status](https://travis-ci.org/vermiculus/magithub.svg?branch=master)](https://travis-ci.org/vermiculus/magithub) +[![Gitter](https://badges.gitter.im/vermiculus/magithub.svg)](https://gitter.im/vermiculus/magithub) +[![MELPA Stable Status](http://melpa-stable.milkbox.net/packages/magithub-badge.svg)](http://melpa-stable.milkbox.net/#/magithub) +[![GitHub Commits](https://img.shields.io/github/commits-since/vermiculus/magithub/0.1.6.svg)](//github.com/vermiculus/magithub/releases) + +Magithub is a collection of interfaces to GitHub integrated into +[Magit][magit] workflows: + +- Push new repositories +- Fork existing ones +- List and create issues and pull requests +- Keep offline notes for your eyes only +- Write comments +- Manage labels and assignees +- Stay up-to-date with status checks (e.g., CI) and notifications +- ... + +as well as support for working offline. + +Happy hacking! + +## Quick Start + +GitHub rate-limits unauthenticated requests heavily, so Magithub does +not support making such requests. Consequently, `ghub` must be +authenticated before using Magithub -- [see its README][ghub] for +those instructions. + +```elisp +(use-package magithub + :after magit + :config + (magithub-feature-autoinject t) + (setq magithub-clone-default-directory "~/github")) +``` + +See [the full documentation][magithub-org] for more details. + +## Getting Help + +See [the FAQ][magithub-org-faq] in the full documentation. If your +question isn't answered there, [drop by the Gitter +room]((https://gitter.im/vermiculus/magithub)). + +## Support + +I'm gainfully and happily employed with a company that frowns on +moonlighting, so unfortunately I can't accept any monetary support. +Instead, [please direct any and all support to Magit +itself][magit-donate]! + +## Note + +There used to be another `magithub`: [nex3/magithub][old-magithub]. +It's long-since unsupported and apparently has many issues +(see [nex3/magithub#11][old-magithub-11] +and [nex3/magithub#13][old-magithub-13]) and +was [removed from MELPA][melpa-1126] some years ago. If you have it +installed or configured, you may wish to remove/archive that +configuration to avoid name-clash issues. Given that the package has +been defunct for over three years and is likely abandoned, the present +package's name will not be changing. + +[magit]: //www.github.com/magit/magit +[magit-donate]: https://magit.vc/donate +[ghub]: //github.com/tarsius/ghub +[hub]: //hub.github.com +[token]: https://github.com/settings/tokens +[gh-use-package]: //github.com/jwiegley/use-package +[old-magithub]: //github.com/nex3/magithub +[old-magithub-11]: //github.com/nex3/magithub/issues/11 +[old-magithub-13]: //github.com/nex3/magithub/issues/13 +[melpa-1126]: //github.com/melpa/melpa/issues/1126 +[magithub-org]: https://github.com/vermiculus/magithub/blob/master/magithub.org +[magithub-org-faq]: https://github.com/vermiculus/magithub/blob/master/magithub.org#faq diff --git a/RelNotes/0.1.6.org b/RelNotes/0.1.6.org new file mode 100644 index 0000000..9673db1 --- /dev/null +++ b/RelNotes/0.1.6.org @@ -0,0 +1,144 @@ +#+Title: Magithub Release 0.1.6 +#+Date: [2018-06-02 Sat] + +#+LINK: PR https://www.github.com/vermiculus/magithub/pull/%s +#+LINK: BUG https://www.github.com/vermiculus/magithub/issues/%s + +* Breaking Changes +- If you were using ~magit-header-line~ to customize the appearance of + the =Issues= and =Pull Requests= section headers, those now use the + ~magit-section-heading~ face. [[PR:196]] +- Many functions related to issue/post creation have been reworked. + Instead of the widget framework, we now use =magithub-edit-mode=. See + more details in 'New Features'. [[PR:204]] +- =magithub-dashboard-show-unread-notifications= is now called + =magithub-dashboard-show-read-notifications= and all functionality + pertaining to that variable has been updated. [[PR:251]] +- Most settings, like the inclusion of sections in ~magit-status~, are + now controlled by various =git config= properties. These settings are + reachable under =H C=. The following functions/variables no longer + exist: + - ~magithub-ci-enabled-p~ (now ~magithub-settings-include-status-p~) + - ~magithub-ci--set-enabled~ + - ~magithub-ci-disable~ + - ~magithub-ci-enable~ + - ~magithub-toggle-ci-status-header~ + - =magithub-cache= (now =magithub-settings-cache-behavior-override=; + ~magithub-settings-cache-behavior~) + - ~magithub-toggle-online~ + - ~magithub-go-online~ + - ~magithub-go-offline~ + - ~magithub-source--remote~ + - ~magithub--deftoggle~ + - ~magithub-toggle-pull-requests~ + - ~magithub-toggle-issues~ + - ~magithub-proxy-set~ + - ~magithub-proxy-set-default~ + - ~magithub-enable~ + - ~magithub-disable~ + - ~magithub-enabled-toggle~ + - =magithub-enabled-by-default= + + The various integration sections are now added to the appropriate + hooks by ~magithub-feature-autoinject~ via =magithub-feature-list=. + + For more details on how to set configure Magithub now, consult the + documentation inside ~magithub-settings-popup~ (=? KEY=) or read + =magithub-settings.el=. [[PR:265]] +- =hub.host= is no longer respected and has been replaced by user option + ~magithub-github-hosts~. This most directly impacts GitHub Enterprise + support. +** Caching [[PR:328]] +Caching has been reworked mostly from the ground-up. 'Offline mode' +is now manifest in a single, Boolean-valued git variable +"magithub.online", which see ~magithub-settings--set-magithub.online~ +for that behavior. + +- ~magithub-cache-invalidate~ was not used, so it is no longer + available. +- ~magithub-issue-refresh~ no longer takes parameters. + +* New Features +- Browse commits by using =w= on a commit section. If the current + section's value cannot be understood as a valid commit, use the + =git-revision= at point. +- ~magithub-feature-autoinject~ can now take a list of features to load. +- Many symbols are now supported by ~thing-at-point~: + - =github-user= + - =github-issue= + - =github-label= + - =github-comment= + - =github-repository= + - =github-pull-request= + - =github-notification= + These symbols should allow other GitHub-sensitive packages to use + the work Magithub has already done without depending on Magithub + directly. [[PR:201]] +- The widget interface for writing issues and pull requests is gone! + Now, everything uses the framework debuted for writing comments. + For issues and pull requests, the first line (i.e., everything up to + the first newline character) is parsed as the title; everything else + as the body. Now issues, pull requests, and comments use a common + interface that supports submitting, canceling, and saving drafts to + finish later. [[PR:204]] +- You can now edit comments using =e= on a comment section. [[PR:206]] +- When submitting pull requests of a single commit, the commit message + is defaulted into the pull request body. Multiple commits? + ~magit-log~ shows you the changes you want to merge. [[PR:239]] +- Headers in issue-view mode are now easier to navigate. [[PR:250]] +- Notifications are marked as read when visited in Emacs. [[PR:252]] +- ~magithub-repo~ can now take a string of the form =user/repo=. This is + helpful when writing other code that uses Magithub functionality. [[PR:253]] +- New command ~magithub-pull-request-new-from-issue~ can create pull + requests from issues. This creates a new pull request by copying + the title/body from the source issue. (To be honest, this API + endpoint is not what I thought it would be.) [[PR:256]] +- Confirmation messages can now be skipped (or the default question + behavior otherwise altered) using =git config= properties. See + ~magithub-confirm-set-default-behavior~ or configure your settings + locally (or globally) interactively when they're asked. [[PR:268]] + [[PR:270]] +- Use default branch of the repository as =BASE= if there's no upstream + for the current branch. [[PR:269]] +- Completion of issue numbers ("#123"), and user names ("@purcell") is + supported in edit and commit message buffers via the standard + ~completion-at-point~ mechanism, and therefore also via ~company~'s ~capf~ + backend. This is enabled by default in certain buffers via the + ~magithub-features~ mechanism. [[PR:263]], [[PR:278]] + +* Bug Fixes +- In ~magithub-repo~, an API request is no longer made when the + repository context cannot be determined. +- The list of labels is now correctly cached per-repository. [[PR:203]] +- The full list of labels is now available for use when modifying + issues and pull requests. [[PR:203]] +- The cache (and other files in =magithub-dir=) are no longer added to + the =recentf= list. [[PR:210]] +- Consistently use ~magithub-request~. [[PR:229]] +- ~magit-magithub-pull-request-section-map~ is now defined in terms of + ~magit-magithub-issue-section-map~. [[PR:238]] +- Fix autoloads to load and install the dispatch with Magit. [[PR:238]] +- Remove awkward blank lines from the end of the dashboard. [[PR:238]] +- Issue/PR drafts are deleted appropriately after successful + submission. [[PR:247]] +- Various performance improvements. [[PR:255]] +- Ghub+ is now required in core. This should help users who utilize + deferred loading. [[PR:260]] +- Submitting pull requests to other repositories in some scenarios + should now be fixed. [[PR:272]] +- ~magithub-clone~ now correctly provides a default destination. [[PR:273]] +- ~magithub-pull-request-new~ now uses a better check to test for pull + requests of a single commit: [[PR:274]] + #+BEGIN_SRC sh + git rev-list --count BASE.. + #+END_SRC +- Authenticate correctly when marking notifications as read. [[PR:277]] +- Don't call ~magit-get~ in a non-existent directory in ~magithub-clone~. + [[PR:282]] +- Pull requests now work in repositories with remotes that point to + non-GitHub locations. [[PR:285]] +- We now only prompt to refresh GitHub data when the command being run + by the user is solely intended to refresh the buffer. [[PR:318]] +- We no longer ever call =/rate_limit= directly, instead relying on an + improved version of ~ghubp-ratelimit~ that handles GitHub Enterprise + sanely. [[BUG:327]] diff --git a/RelNotes/0.2.org b/RelNotes/0.2.org new file mode 100644 index 0000000..18f02a4 --- /dev/null +++ b/RelNotes/0.2.org @@ -0,0 +1,5 @@ +#+Title: Magithub Release 0.2 +#+Date: + +#+LINK: PR https://www.github.com/vermiculus/magithub/pull/%s +#+LINK: BUG https://www.github.com/vermiculus/magithub/issues/%s diff --git a/images/ci-failure.png b/images/ci-failure.png Binary files differnew file mode 100644 index 0000000..c1d556b --- /dev/null +++ b/images/ci-failure.png diff --git a/images/ci-pending.png b/images/ci-pending.png Binary files differnew file mode 100644 index 0000000..f35377f --- /dev/null +++ b/images/ci-pending.png diff --git a/images/ci-success.png b/images/ci-success.png Binary files differnew file mode 100644 index 0000000..1198eb7 --- /dev/null +++ b/images/ci-success.png diff --git a/images/create.gif b/images/create.gif Binary files differnew file mode 100644 index 0000000..bebb010 --- /dev/null +++ b/images/create.gif diff --git a/images/pull-request.gif b/images/pull-request.gif Binary files differnew file mode 100644 index 0000000..2ee912e --- /dev/null +++ b/images/pull-request.gif diff --git a/images/scr1.png b/images/scr1.png Binary files differnew file mode 100644 index 0000000..090dd2a --- /dev/null +++ b/images/scr1.png diff --git a/images/scr2.png b/images/scr2.png Binary files differnew file mode 100644 index 0000000..edaf721 --- /dev/null +++ b/images/scr2.png diff --git a/images/scr3.png b/images/scr3.png Binary files differnew file mode 100644 index 0000000..34b59bc --- /dev/null +++ b/images/scr3.png diff --git a/images/scr4.png b/images/scr4.png Binary files differnew file mode 100644 index 0000000..554caad --- /dev/null +++ b/images/scr4.png diff --git a/images/scr5.png b/images/scr5.png Binary files differnew file mode 100644 index 0000000..794dc23 --- /dev/null +++ b/images/scr5.png diff --git a/images/status.png b/images/status.png Binary files differnew file mode 100644 index 0000000..b876622 --- /dev/null +++ b/images/status.png diff --git a/magithub-ci.el b/magithub-ci.el new file mode 100644 index 0000000..90ae68f --- /dev/null +++ b/magithub-ci.el @@ -0,0 +1,270 @@ +;;; magithub-ci.el --- Show CI status as a magit-status header -*- lexical-binding: t; -*- + +;; Copyright (C) 2016-2018 Sean Allred + +;; Author: Sean Allred <code@seanallred.com> +;; Keywords: tools + +;; 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 <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Provide the CI status of "origin" in the Magit status buffer. + +;;; Code: + +(require 'magit) +(require 'magit-section) +(require 'dash) +(require 's) + +(require 'magithub-core) +(require 'magithub-issue) + +;;;###autoload +(defun magithub-maybe-insert-ci-status-header () + "If this is a GitHub repository, insert the CI status header." + (when (and (magithub-settings-include-status-p) + (magithub-usable-p) + (let ((b (magit-get-current-branch))) + (or (magit-get-upstream-remote b) + (magit-get-push-remote b)))) + (magithub-insert-ci-status-header))) + +(defvar magithub-ci--status-last-refreshed nil + "An alist of alists: repos to refs to times. +For efficiency, repos are represented only by their full names.") + +(defun magithub-ci--status-last-refreshed-time (repo ref) + "The last time the statuses for REPO@REF were retrieved. +This is a generalized variable and can be set with `setf'." + (declare (gv-setter + (lambda (time) + `(let ((repo (magithub-repo-name ,repo))) + (if-let ((statuses (assoc repo magithub-ci--status-last-refreshed))) + (if-let ((status (assoc ,ref (cdr statuses)))) + (setcdr status ,time) + (push (cons ,ref ,time) (cdr statuses))) + (push (cons repo (list (cons ,ref ,time))) + magithub-ci--status-last-refreshed)))))) + '(thread-last magithub-ci--status-last-refreshed + (assoc (magithub-repo-name repo)) (cdr) + (assoc ref) (cdr)) + (cdr (assoc ref (cdr (assoc (magithub-repo-name repo) + magithub-ci--status-last-refreshed))))) + +(defun magithub-pull-request-pr->branch (pull-request) + "Does not handle cases where the local branch has been renamed." + (let-alist pull-request .head.ref)) + +(define-error 'magithub-error-ambiguous-branch "Ambiguous Branch" 'magithub-error) +(defun magithub-pull-request-branch->pr--ghub (branch) + "This is a hueristic; it's not 100% accurate. +It may fail if the fork has multiple branches named BRANCH." + (let ((repo (magithub-repo-from-remote (magit-get-push-remote branch)))) + (when (alist-get 'fork repo) + (let* ((guess-head (format "%s:%s" (magit-get-push-remote branch) branch)) + (prs (magithub-cache :ci-status + `(magithub-request + (ghubp-get-repos-owner-repo-pulls ',(magithub-repo) :head ,guess-head))))) + (pcase (length prs) + (0) ; this branch does not seem to correspond to any PR + (1 (magit-set (number-to-string (alist-get 'number (car prs))) + "branch" branch "magithub" "sourcePR") + (car prs)) + (_ ;; todo: currently unhandled + (signal 'magithub-error-ambiguous-branch + (list :prs prs + :guess-head guess-head + :repo-from-remote (alist-get 'full_name repo) + :source-repo (alist-get 'full_name (magithub-repo)))))))))) + +(defun magithub-pull-request-branch->pr--gitconfig (branch) + "Gets a pull request object from branch.BRANCH.magithub.sourcePR" + (when-let ((source (magit-get "branch" branch "magithub" "sourcePR"))) + (magithub-pull-request (magithub-repo) (string-to-number source)))) + +(defun magithub-ci-status--get-default-ref (&optional branch) + "The ref to use for CI status based on BRANCH. + +Handles cases where the local branch's name is different than its +remote counterpart." + (setq branch (or branch (magit-get-current-branch))) + (if-let ((pull-request + (or (magithub-pull-request-branch->pr--gitconfig branch) + (and (magithub-online-p) + (with-demoted-errors "Error: %S" + (magithub-pull-request-branch->pr--ghub branch)))))) + (let-alist pull-request .head.sha) + (when-let ((push-branch (magit-get-push-branch branch))) + (when (magit-branch-p push-branch) + (cdr (magit-split-branch-name push-branch)))))) + +(defun magithub-ci-status (ref) + (when (stringp ref) + (if (magit-rebase-in-progress-p) + ;; avoid rate-limiting ourselves + (magithub-debug-message "skipping CI status checks while in rebase") + (or (magithub-cache :ci-status + `(magithub-request + (ghubp-get-repos-owner-repo-commits-ref-status + ',(magithub-repo) ,ref)) + :message + (format "Getting CI status for %s..." + (if (magit-branch-p ref) (format "branch `%s'" ref) + (substring ref 0 6))) + :after-update + (lambda (status &rest _) + (setf (magithub-ci--status-last-refreshed-time (magithub-repo) ref) + (current-time)) + (message "(magithub): new statuses retrieved -- overall: %s" + (alist-get 'state status)))) + '((state . "error") + (total_count . 0) + (magithub-message . "ref not found on remote")))))) + +(defvar magithub-ci-status-alist + '((nil . ((display . "None") (face . magithub-ci-no-status))) + ("error" . ((display . "Error") (face . magithub-ci-error))) + ("failure" . ((display . "Failure") (face . magithub-ci-failure))) + ("pending" . ((display . "Pending") (face . magithub-ci-pending))) + ("success" . ((display . "Success") (face . magithub-ci-success))))) +(defconst magithub-ci-status--unknown + '((face . magithub-ci-unknown))) + +(defun magithub-ci-pr-status (pr) + (interactive (list (thing-at-point 'github-pull-request))) + (unless pr + (user-error "no pr at point")) + (message "state of #%d: %s" + (let-alist pr .number) + (let-alist (ghubp-get-repos-owner-repo-commits-ref-status + (magithub-repo) + (let-alist pr .head.sha)) + .state))) + +(defun magithub-ci-visit (ref) + "Jump to CI with `browse-url'." + (interactive (list (magit-rev-parse (magit-commit-at-point)))) + (let (done) + (when (null ref) + (pcase (oref (magit-current-section) value) + (`(magithub-ci-url . ,url) + (browse-url url) + (setq done t)) + (`(magithub-ci-ref . ,secref) + (setq ref secref)))) + (unless done + (let* ((urls (alist-get 'statuses (magithub-ci-status ref))) + (status + (cond + ((= 1 (length urls)) (car urls)) + (urls (magithub--completing-read + "Status service: " urls + #'magithub-ci--format-status))))) + (let-alist status + (when (or (null .target_url) (string= "" .target_url)) + (user-error "No Status URL detected")) + (browse-url .target_url)))))) + +(defun magithub-ci--format-status (status) + (let-alist status + (format "(%s) %s: %s" + (let ((spec (magithub-ci--status-spec .state))) + (alist-get 'display spec .state)) + .context + .description))) + +(defvar magit-magithub-ci-status-section-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map magithub-map) + (define-key map [remap magit-visit-thing] #'magithub-ci-visit) + (define-key map [remap magithub-browse-thing] #'magithub-ci-visit) + (define-key map [remap magit-refresh] #'magithub-ci-refresh) + map) + "Keymap for `magithub-ci-status' header section.") + +(defun magithub-ci-refresh () + "Invalidate the CI cache and refresh the buffer." + (interactive) + (unless (magithub-online-p) + (magithub-confirm 'ci-refresh-when-offline)) + (magithub-cache-without-cache :ci-status + (magithub-ci-status (magithub-ci-status--get-default-ref))) + (magit-refresh)) + +(defun magithub-insert-ci-status-header () + (let* ((ref (magithub-ci-status--get-default-ref)) + (checks (magithub-ci-status ref)) + (indent (make-string 10 ?\ ))) + (when checks + (magit-insert-section (magithub-ci-status + `(magithub-ci-ref . ,ref) + 'collapsed) + (insert (format "%-10s%s %s %s%s" "Status:" + (magithub-ci--status-header checks) + (propertize "on ref" 'face 'magit-dimmed) + (propertize ref 'face 'magit-refname) + (propertize "..." 'face 'magit-dimmed))) + (magit-insert-heading) + (insert (propertize + (format "%-10sas of %s\n" "" + (if-let ((time (magithub-ci--status-last-refreshed-time (magithub-repo) ref))) + (magithub--format-time time) + "???")) + 'face 'magit-dimmed)) + (dolist (status (alist-get 'statuses checks)) + (magit-insert-section (magithub-ci-status + `(magithub-ci-url . ,(alist-get 'target_url status))) + (insert indent (magithub-ci--status-propertized status "*")) + (magit-insert-heading))))))) + +(defun magithub-ci--status-header (checks) + (pcase (alist-get 'total_count checks) + (0 (format "%s %s" + (magithub-ci--status-propertized checks) + (or (alist-get 'magithub-message checks) + (propertize "it seems checks have not yet begun" + 'face 'magit-dimmed)))) + (1 (magithub-ci--status-propertized checks)) + (_ (let* ((overall-status (alist-get 'state checks)) + (status-spec (magithub-ci--status-spec overall-status)) + (display (or (alist-get 'display status-spec) overall-status)) + (statuses (alist-get 'statuses checks)) + (passed (-filter (lambda (s) (string= "success" (alist-get 'state s))) + statuses))) + (propertize (format "%s (%d/%d)" display (length passed) (length statuses)) + 'face (alist-get 'face status-spec)))))) + +(defun magithub-ci--status-spec (status-string) + (or (cdr (assoc-string status-string magithub-ci-status-alist)) + magithub-ci-status--unknown)) + +(defun magithub-ci--status-propertized (status &optional override-status-text) + (let ((status-string (alist-get 'state status)) + (description (alist-get 'description status)) + (context (alist-get 'context status))) + (let-alist (magithub-ci--status-spec status-string) + (concat (propertize (or override-status-text + .display + status-string) + 'face .face) + (when description + (format " %s" description)) + (when context + (propertize (format " %s" context) + 'face 'magit-dimmed)))))) + +(provide 'magithub-ci) +;;; magithub-ci.el ends here diff --git a/magithub-comment.el b/magithub-comment.el new file mode 100644 index 0000000..0de3a80 --- /dev/null +++ b/magithub-comment.el @@ -0,0 +1,261 @@ +;;; magithub-comment.el --- tools for comments -*- lexical-binding: t; -*- + +;; Copyright (C) 2017-2018 Sean Allred + +;; Author: Sean Allred <code@seanallred.com> +;; Keywords: lisp + +;; 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 <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Tools for working with issue comments. + +;;; Code: + +(require 'magit) +(require 'markdown-mode) +(require 'thingatpt) + +(require 'magithub-core) +(require 'magithub-repo) +(require 'magithub-issue) +(require 'magithub-edit-mode) + +(declare-function magithub-issue-view "magithub-issue-view.el" (issue)) + +(defvar magit-magithub-comment-section-map + (let ((m (make-sparse-keymap))) + (set-keymap-parent m magithub-map) + (define-key m [remap magithub-browse-thing] #'magithub-comment-browse) + (define-key m [remap magit-delete-thing] #'magithub-comment-delete) + (define-key m (kbd "SPC") #'magithub-comment-view) + (define-key m [remap magithub-reply-thing] #'magithub-comment-reply) + (define-key m [remap magithub-edit-thing] #'magithub-comment-edit) + m)) + +(defun magithub-comment-browse (comment) + (interactive (list (thing-at-point 'github-comment))) + (unless comment + (user-error "No comment found")) + (let-alist comment + (browse-url .html_url))) + +(declare-function face-remap-remove-relative "face-remap.el" (cookie)) +(defun magithub-comment-delete (comment) + (interactive (list (thing-at-point 'github-comment))) + (unless comment + (user-error "No comment found")) + (let ((repo (magithub-comment-source-repo comment)) + (author (let-alist comment .user.login)) + (me (let-alist (magithub-user-me) .login))) + (unless (or (string= author me) + (magithub-repo-admin-p repo)) + (user-error "You don't have permission to delete this comment")) + (let ((cookie (face-remap-add-relative 'magit-section-highlight + ;;'magit-diff-removed-highlight + ;;:strike-through t + ;;:background "red4" + ;; + 'magithub-deleted-thing + ))) + (unwind-protect (magithub-confirm 'comment-delete) + (face-remap-remove-relative cookie))) + (magithub-request + (ghubp-delete-repos-owner-repo-issues-comments-id repo comment)) + (magithub-cache-without-cache :issues + (magit-refresh-buffer)) + (message "Comment deleted"))) + +(defun magithub-comment-source-issue (comment) + (magithub-cache :comment + `(magithub-request + (ghubp-follow-get ,(alist-get 'issue_url comment))))) + +(defun magithub-comment-source-repo (comment) + (magithub-issue-repo (magithub-comment-source-issue comment))) + +(defun magithub-comment-insert (comment) + "Insert a single issue COMMENT." + (let-alist comment + (magit-insert-section (magithub-comment comment) + (magit-insert-heading (propertize .user.login 'face 'magithub-user)) + (save-excursion + (let ((created-at (magithub--format-time .created_at))) + (backward-char 1) + (insert (make-string (- (current-fill-column) + (current-column) + (length created-at)) + ? )) + (insert (propertize created-at 'face 'magit-dimmed)))) + (insert (magithub-fill-gfm (magithub-wash-gfm (s-trim .body))) + "\n\n")))) + +(defvar magithub-gfm-view-mode-map + (let ((m (make-sparse-keymap))) + (define-key m [remap kill-this-buffer] #'magithub-comment-view-close) + m) + "Keymap for `magithub-gfm-view-mode'.") + +(declare-function gfm-view-mode "ext:markdown-mode.el") +(define-derived-mode magithub-gfm-view-mode gfm-view-mode "M:GFM-View" + "Major mode for viewing GitHub markdown content.") + +(defvar-local magithub-comment-view--parent-buffer nil + "The 'parent' buffer of the current comment-view. +This variable is used to jump back to the issue that contained +the comment; see `magithub-comment-view' and +`magithub-comment-view-close'.") + +(defun magithub-comment-view (comment) + "View COMMENT in a new buffer." + (interactive (list (thing-at-point 'github-comment))) + (let ((prev (current-buffer))) + (with-current-buffer (get-buffer-create "*comment*") + (magithub-gfm-view-mode) + (setq-local magithub-comment-view--parent-buffer prev) + (let ((inhibit-read-only t)) + (erase-buffer) + (insert (magithub-wash-gfm (alist-get 'body comment)))) + (goto-char 0) + (magit-display-buffer (current-buffer))))) + +(defun magithub-comment-view-close () + "Close the current buffer." + (interactive) + (let ((oldbuf magithub-comment-view--parent-buffer)) + (kill-this-buffer) + (magit-display-buffer oldbuf))) + +;;;###autoload +(defun magithub-comment-new (issue &optional discard-draft initial-content) + "Comment on ISSUE in a new buffer. +If prefix argument DISCARD-DRAFT is specified, the draft will not +be considered. + +If INITIAL-CONTENT is specified, it will be inserted as the +initial contents of the reply if there is no draft." + (interactive (let ((issue (magithub-interactive-issue))) + (prog1 (list issue current-prefix-arg) + (unless (derived-mode-p 'magithub-issue-view-mode) + (magithub-issue-view issue))))) + (let* ((issueref (magithub-issue-reference issue)) + (repo (magithub-issue-repo issue))) + (with-current-buffer + (magithub-edit-new (concat "reply to " issueref) + :header (concat "replying to " issueref) + :submit #'magithub-issue-comment-submit + :content initial-content + :prompt-discard-draft discard-draft + :file (magithub-comment--draft-file issue repo)) + (setq-local magithub-issue issue) + (setq-local magithub-repo repo) + (magit-display-buffer (current-buffer))))) + +(defun magithub-comment--draft-file (issue repo) + "Get an appropriate draft file for ISSUE in REPO." + (let-alist issue + (expand-file-name (format "%s-comment" .number) + (magithub-repo-data-dir repo)))) + +(defun magithub-comment--submit-edit (comment repo new-body) + (interactive (list (thing-at-point 'github-comment) + (thing-at-point 'github-repository) + (buffer-string))) + (when (string= new-body "") + (user-error "Can't post an empty comment; try deleting it instead")) + (magithub-confirm 'comment-edit) + (magithub-request + (ghubp-patch-repos-owner-repo-issues-comments-id + repo comment + `((body . ,new-body))))) + +(defun magithub-comment-edit (comment issue repo) + "Edit COMMENT." + (interactive (list (thing-at-point 'github-comment) + (or (thing-at-point 'github-issue) + (thing-at-point 'github-pull-request)) + (thing-at-point 'github-repository))) + (let ((updated (magithub-request (ghubp-follow-get (alist-get 'url comment))))) + (with-current-buffer + (magithub-edit-new (format "*%s: editing comment by %s (%s)*" + (magithub-issue-reference issue) + (let-alist comment .user.login) + (alist-get 'id comment)) + :submit #'magithub-comment--submit-edit + :content (alist-get 'body updated) + :file (magithub-comment--draft-file issue repo)) + (setq-local magithub-issue issue) + (setq-local magithub-repo repo) + (setq-local magithub-comment updated) + (magit-display-buffer (current-buffer))) + + (unless (string= (alist-get 'body comment) + (alist-get 'body updated)) + (message "Comment has changed since information was cached; \ +updated content pulled in for edit")))) + +(defun magithub-comment-reply (comment &optional discard-draft issue) + "Reply to COMMENT on ISSUE. +If prefix argument DISCARD-DRAFT is provided, the current draft +will deleted. + +If ISSUE is not provided, it will be determined from context or +from COMMENT." + (interactive (list (thing-at-point 'github-comment) + current-prefix-arg + (thing-at-point 'github-issue))) + (let-alist comment + (magithub-comment-new + (or issue (magithub-request (ghubp-follow-get .issue_url))) + discard-draft + (let ((reply-body (if (use-region-p) + (buffer-substring (region-beginning) (region-end)) + .body))) + (with-temp-buffer + (insert (string-trim (magithub-wash-gfm reply-body))) + (markdown-blockquote-region (point-min) (point-max)) + (goto-char (point-max)) + (insert "\n\n") + (buffer-string)))))) + +(defun magithub-issue-comment-submit (issue comment &optional repo) + "On ISSUE, submit a new COMMENT. + +COMMENT is the text of the new comment. + +REPO is an optional repo object; it will be deduced from ISSUE if +not provided." + (interactive (list (thing-at-point 'github-issue) + (save-restriction + (widen) + (buffer-substring-no-properties (point-min) (point-max))) + (thing-at-point 'github-repository))) + (unless issue + (user-error "No issue provided")) + (setq repo (or repo + (magithub-issue-repo issue) + (thing-at-point 'github-repository))) + (unless repo + (user-error "No repo detected")) + ;; all required args provided + (magithub-confirm 'comment (magithub-issue-reference issue)) + (magithub-request + (ghubp-post-repos-owner-repo-issues-number-comments + repo issue `((body . ,comment)))) + (magithub-edit-delete-draft) + (message "Success")) + +(provide 'magithub-comment) +;;; magithub-comment.el ends here diff --git a/magithub-completion.el b/magithub-completion.el new file mode 100644 index 0000000..5ef8f7b --- /dev/null +++ b/magithub-completion.el @@ -0,0 +1,102 @@ +;;; magithub-completion.el --- Completion using info provided by magithub -*- lexical-binding: t; -*- + +;; Copyright (C) 2018 Steve Purcell + +;; Author: Steve Purcell <steve@sanityinc.com> +;; Keywords: convenience + +;; 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 <https://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Provides `completion-at-point' functions which complete issue +;; numbers etc when they are entered in commit messages. + +;; Extended information is attached to completions so that `company' +;; can access it via the standard `company-capf' backend. + +;;; Code: + + +(require 'magithub-settings) +(require 'magithub-issue) + + +;;;###autoload +(defun magithub-completion-complete-issues () + "A `completion-at-point' function which completes \"#123\" issue references. +Add this to `completion-at-point-functions' in buffers where you +want this to be available." + (when (magithub-enabled-p) + (when (looking-back "#\\([0-9]*\\)" (- (point) 10)) + (let ((start (match-beginning 1)) + (end (match-end 0)) + (prefix (match-string 1)) + completions) + (dolist (i (magithub--issue-list)) + (let-alist i + (let ((n (number-to-string .number))) + (when (string-prefix-p prefix n) + (push (propertize n :issue i) completions))))) + (list start end (sort completions #'string<) + :exclusive 'no + :company-docsig (lambda (c) + (let-alist (get-text-property 0 :issue c) + .title)) + :annotation-function (lambda (c) + (let-alist (get-text-property 0 :issue c) + .title)) + :company-doc-buffer (lambda (c) + (save-window-excursion + (magithub-issue-visit + (get-text-property 0 :issue c))))))))) + +;;;###autoload +(defun magithub-completion-complete-users () + "A `completion-at-point' function which completes \"@user\" user references. +Add this to `completion-at-point-functions' in buffers where you +want this to be available. The user list is currently simply the +list of all users who created issues or pull requests." + (when (magithub-enabled-p) + (when (looking-back "@\\([_-A-Za-z0-9]*\\)" (- (point) 30)) + (let ((start (match-beginning 1)) + (end (match-end 0)) + (prefix (match-string 1)) + completions) + (dolist (i (magithub--issue-list)) + (let-alist i + (when (string-prefix-p prefix .user.login) + (let ((candidate (copy-sequence .user.login)) + (association (and .author_association + (not (string= "NONE" .author_association)) + .author_association))) + (push (propertize candidate :user .user :association association) + completions))))) + (list start end (sort (cl-remove-duplicates completions :test #'string=) + #'string<) + :exclusive 'no + :company-docsig (lambda (c) (get-text-property 0 :association c)) + :annotation-function (lambda (c) (get-text-property 0 :association c))))))) + +;;;###autoload +(defun magithub-completion-enable () + "Enable completion of info from magithub in the current buffer." + (make-local-variable 'completion-at-point-functions) + (dolist (f '(magithub-completion-complete-issues + magithub-completion-complete-users)) + (add-to-list 'completion-at-point-functions f))) + + +(provide 'magithub-completion) +;;; magithub-completion.el ends here diff --git a/magithub-core.el b/magithub-core.el new file mode 100644 index 0000000..fbbfb09 --- /dev/null +++ b/magithub-core.el @@ -0,0 +1,1253 @@ +;;; magithub-core.el --- core functions for magithub -*- lexical-binding: t; -*- + +;; Copyright (C) 2016-2018 Sean Allred + +;; Author: Sean Allred <code@seanallred.com> +;; Keywords: tools + +;; 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 <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Core functions for Magithub. + +;;; Code: + +(require 'magit) +(require 'dash) +(require 's) +(require 'subr-x) +(require 'ghub) +(require 'ghub+) +(require 'bug-reference) +(require 'cl-lib) +(require 'markdown-mode) +(require 'parse-time) +(require 'thingatpt) +(require 'recentf) + +(require 'magithub-settings) +(require 'magithub-faces) + +(defconst magithub-github-token-scopes '(repo user notifications) + "The authentication scopes Magithub requests.") + +;;; Debugging utilities + +(defvar magithub-debug-mode nil + "Controls what kinds of debugging information shows. +List of symbols. + +`dry-api' - don't actually make API requests +`forms' - show forms being evaluated in the cache") + +(defun magithub-debug-mode (&optional submode) + "True if debug mode is on. +If SUBMODE is supplied, specifically check for that mode in +`magithub-debug-mode'." + (and (listp magithub-debug-mode) + (memq submode magithub-debug-mode))) + +(defun magithub-debug-message (fmt &rest args) + "Print a debug message. +Respects `magithub-debug-mode' and `debug-on-error'." + (when (or magithub-debug-mode debug-on-error) + (let ((print-quoted t)) + (message "magithub: (%s) %s" + (format-time-string "%M:%S.%3N" (current-time)) + (apply #'format fmt args))))) + +(defun magithub-debug--ghub-request-wrapper (oldfun &rest args) + "Report ghub requests as they're being made. +Intended as around-advice for `ghub-requst'." + (magithub-debug-message "ghub-request%S" args) + (unless (magithub-debug-mode 'dry-api) + (apply oldfun args))) +(advice-add #'ghub-request :around #'magithub-debug--ghub-request-wrapper) + +(defcustom magithub-dir + (expand-file-name "magithub" user-emacs-directory) + "Data directory. +Various Magithub data (such as the cache) will be dumped into the +root of this directory. + +If it does not exist, it will be created." + :group 'magithub + :type 'directory) +(add-to-list 'recentf-exclude + (lambda (filename) + (file-in-directory-p filename magithub-dir))) + +;;; Turning Magithub on/off + +(defmacro magithub-in-data-dir (&rest forms) + "Execute forms in `magithub-dir'. +If `magithub-dir' does not yet exist, it and its parents will be +created automatically." + (declare (debug t)) + `(progn + (unless (file-directory-p magithub-dir) + (mkdir magithub-dir t)) + (let ((default-directory magithub-dir)) + ,@forms))) + +;;; Caching; Online/Offline mode + +(defcustom magithub-cache-file "cache" + "Use this file for Magithub's persistent cache." + :group 'magithub + :type 'file) + +(defun magithub-cache-read-from-disk () + "Returns the cache as read from `magithub-cache-file'." + (magithub-in-data-dir + (when (file-readable-p magithub-cache-file) + (with-temp-buffer + (insert-file-contents magithub-cache-file) + (read (current-buffer)))))) + +(defvar magithub-cache--cache + (or (ignore-errors + (magithub-cache-read-from-disk)) + (make-hash-table :test 'equal)) + "The actual cache. +Holds all information ever cached by Magithub. + +Occasionally written to `magithub-cache-file' by +`magithub-cache-write-to-disk'.") + +(defvar magithub-cache--needs-write nil + "Signals that the cache has been updated. +When non-nil, the cache will be written to disk next time the +idle timer runs.") + +(defvar magithub-cache--refreshed-forms nil + "Forms that have been refreshed this session. +See also `magithub--refresh'.") + +(cl-defun magithub-cache (class form &key message after-update) + "The cached value for FORM if available. + +If FORM has not been cached or its CLASS dictates the cache has +expired, FORM will be re-evaluated. + +CLASS: The kind of data this is; see `magithub-cache--refresh'. + +MESSAGE may be specified for intensive functions. We'll display +this with `with-temp-message' while the form is evaluating. + +AFTER-UPDATE is a function to run after the cache is updated." + (declare (indent defun)) + (let* ((no-value-sym (cl-gensym)) + (entry (list (ghubp-get-context) class form)) + (online (magithub-online-p)) + (cached-value (gethash entry magithub-cache--cache no-value-sym)) + (value-does-not-exist (eq cached-value no-value-sym)) + (cached-value (if value-does-not-exist nil cached-value)) + make-request new-value) + + (when online + (if (or (eq magithub-cache--refresh t) + (eq magithub-cache--refresh class)) + ;; if we're refreshing (and we haven't seen this form + ;; before), go ahead and make the request if it's the class + ;; we're refreshing (or t, which encompasses all classes) + (setq make-request (not (member entry magithub-cache--refreshed-forms))) + (setq make-request value-does-not-exist))) + + (or (and make-request + (prog1 (setq new-value (with-temp-message message (eval form))) + (puthash entry new-value magithub-cache--cache) + (unless magithub-cache--needs-write + (setq magithub-cache--needs-write t) + (run-with-idle-timer 600 nil #'magithub-cache-write-to-disk)) + (when magithub-cache--refresh + (push entry magithub-cache--refreshed-forms)) + (if (functionp after-update) + (funcall after-update new-value) + new-value))) + cached-value))) + +(defun magithub-maybe-report-offline-mode () + "Conditionally inserts the OFFLINE header. +If this is a Magithub-enabled repository and we're offline, we +insert a header notifying the user that all data shown is cached. +To aid in determining if the cache should be refreshed, we report +the age of the oldest cached information." + (when (and (magithub-usable-p) + (not (magithub-online-p))) + (magit-insert-section (magithub nil t) + (insert + (format + "Magithub: %s; use %s to refresh GitHub content or %s to go back online%s\n" + (propertize "OFFLINE" 'face 'magit-head) + (propertize + (substitute-command-keys "\\[universal-argument] \\[magit-refresh]") + 'face 'magit-header-line-key) + (propertize + (substitute-command-keys "\\[magithub-dispatch-popup] C o") + 'face 'magit-header-line-key) + (propertize "..." 'face 'magit-dimmed))) + (magit-insert-heading) + (let* ((msg "When Magithub is offline, no API requests are ever made \ +automatically. Even when online, cached API responses never expire, so \ +they must be updated manually with %s.") + (msg (s-word-wrap (- fill-column 10) msg)) + (msg (format msg (propertize + (substitute-command-keys + "\\[universal-argument] \\[magit-refresh]") + 'face 'magit-header-line-key)))) + (insert (format "%s\n" (replace-regexp-in-string + (rx bol) (make-string 10 ?\ ) msg))))))) + +(eval-after-load 'magit + '(add-hook 'magit-status-headers-hook + #'magithub-maybe-report-offline-mode + 'append)) + +(defun magithub-cache--time-out (time) + "Convert TIME into a human-readable string. +Returns \"Xd Xh Xm Xs\" (counting from zero)" + (let ((seconds (time-to-seconds time))) + (format-time-string + (cond + ((< seconds 60) "%-Ss") + ((< seconds 3600) "%-Mm %-Ss") + ((< seconds 86400) "%-Hh %-Mm %-Ss") + (t "%-jd %-Hh %-Mm %-Ss")) + time))) + +(defun magithub-cache-write-to-disk () + "Write the cache to disk. +The cache is written to `magithub-cache-file' in +`magithub-dir'" + (if (active-minibuffer-window) + (run-with-idle-timer 600 nil #'magithub-cache-write-to-disk) ;defer + (when magithub-cache--needs-write + (magithub-in-data-dir + (with-temp-buffer + (insert (prin1-to-string magithub-cache--cache)) + (write-file magithub-cache-file))) + (setq magithub-cache--needs-write nil) + (magithub-debug-message "wrote cache to disk: %S" + (expand-file-name magithub-cache-file + magithub-dir))))) + +(defmacro magithub-cache-without-cache (class &rest body) + "For CLASS, execute BODY without using CLASS's caches. +Use t to ignore previously cached values completely. +See also `magithub-cache--refresh'." + (declare (indent 1) (debug t)) + `(let ((magithub-cache--refresh ,class)) + ,@body)) + +(add-hook 'kill-emacs-hook + #'magithub-cache-write-to-disk) + +;;; API availability checking + +(define-error 'magithub-error "Magithub Error") +(define-error 'magithub-api-timeout "Magithub API Timeout" 'magithub-error) + +(defvar magithub--api-last-checked + ;; see https://travis-ci.org/vermiculus/magithub/jobs/259006323 + ;; (eval-when-compile (date-to-time "1/1/1970")) + '(14445 17280) + "The last time the API was available. +Used to avoid pinging GitHub multiple times a second.") + +(defcustom magithub-api-timeout 3 + "Number of seconds we'll wait for the API to respond." + :group 'magithub + :type 'integer) + +(defcustom magithub-api-low-threshold 15 + "Low threshold for API requests. +This variable is not currently respected; see tarsius/ghub#16. + +If the number of available API requests drops to or below this +threshold, you'll be asked if you'd like to go offline." + :group 'magithub + :type 'integer) + +(defcustom magithub-api-available-check-frequency 10 + "Minimum number of seconds between each API availability check. +While online (see `magithub-go-online'), we check to ensure the +API is available before making a real request. This involves a +`/rate_limit' call (or for some Enterprise instances, a `/meta' +call). Use this setting to configure how often this is done. It +will be done no more frequently than other API actions. + +These calls are guaranteed to not count against your rate limit." + :group 'magithub + :type 'integer) + +(defvar magithub--quick-abort-api-check nil + "When non-nil, we'll assume the API is unavailable. +Do not modify this variable in code outside Magithub.") + +(defvar magithub--api-offline-reason nil + "The reason we're going offline. +Could be one of several strings: + + * authentication issue + + * response timeout + + * generic error + +and possibly others as error handlers are added to +`magithub--api-available-p'.") + +(defun magithub--api-available-p () + "Non-nil if the API is available. +Pings the API a maximum of once every ten seconds." + (setq magithub--api-offline-reason nil) + (when (magithub-enabled-p) + (magithub-debug-message "checking if the API is available") + (prog1 (when + (progn + (magithub-debug-message "making sure authinfo is unlocked") + (ghubp-token 'magithub)) + (if (and magithub--api-last-checked + (< (time-to-seconds (time-since magithub--api-last-checked)) + magithub-api-available-check-frequency)) + (prog1 magithub--api-last-checked + (magithub-debug-message "used cached value for api-last-checked")) + + (magithub-debug-message "cache expired; retrieving new value for api-last-checked") + (setq magithub--api-last-checked (current-time)) + + (let (api-status error-data response) + (condition-case err + (progn + (with-timeout (magithub-api-timeout + (signal 'magithub-api-timeout nil)) + (setq response + ;; /rate_limit is free for GitHub.com. + ;; If rate limiting is disabled + ;; (i.e. GHE), try using /meta which + ;; should (hopefully) always work. See + ;; also issue #107. + (or (ghubp-ratelimit) + (ghub-get "/meta" nil :auth 'magithub)) + + api-status (and response t))) + + (magithub-debug-message + "new value retrieved for api-last-available: %S" response)) + + ;; Sometimes, the API can take a long time to respond + ;; (whether that's GitHub not responding or requests being + ;; blocked by some client-side firewall). Handle this + ;; possibility gracefully. + (magithub-api-timeout + (setq error-data err + magithub--api-offline-reason + (concat "API is not responding quickly; " + "consider customizing `magithub-api-timeout' " + "if this happens often"))) + + ;; Never hurts to be cautious :-) + (error + (setq error-data err) + (setq magithub--api-offline-reason + (format "unknown issue: %S" err)))) + + (when error-data + (magithub-debug-message + "consider reporting unknown error while checking api-available: %S" + error-data)) + + api-status))) + (when magithub--api-offline-reason + (magit-set "false" "magithub.online") + (run-with-idle-timer 2 nil #'magithub--api-offline-reason))))) + +(defun magithub--api-offline-reason () + "Report the reason we're going offline and go offline. +Refresh the status buffer if necessary. + +See `magithub--api-offline-reason'." + (when magithub--api-offline-reason + (message "Magithub is now offline: %s" + magithub--api-offline-reason) + (setq magithub--api-offline-reason nil))) + +(defalias 'magithub-api-rate-limit #'ghubp-ratelimit) + +;;; Repository parsing + +(defcustom magithub-github-hosts + (list "github.com") + "A list of top-level domains that should be recognized as GitHub hosts. +See also `magithub-github-repository-p'." + :group 'magithub + :type '(list string)) + +(defun magithub-github-repository-p () + "Non-nil if \"origin\" points to GitHub or a whitelisted domain. +See also `magithub-github-hosts'." + (when-let ((origin (magit-get "remote" (magithub-settings-context-remote) "url"))) + (-some? (lambda (domain) (s-contains? domain origin)) + magithub-github-hosts))) + +(defalias 'magithub--parse-url 'magithub--repo-parse-url) +(make-obsolete 'magithub--parse-url 'magithub--repo-parse-url "0.1.4") +(defun magithub--repo-parse-url (url) + "Parse URL into its components. +URL may be of several different formats: + +- git@github.com:vermiculus/magithub.git +- https://github.com/vermiculus/magithub" + (and url + (or (and (string-match + ;; git@github.com:vermiculus/magithub.git + (rx bol + (group (+? any)) ;sshuser -- git + "@" + (group (+? any)) ;domain -- github.com + ":" + (group (+? (| alnum "-" "." "_"))) ;owner.login -- vermiculus + "/" + (group (+? (| alnum "-" "." "_"))) ;name -- magithub + (? ".git") + eol) + url) + `((kind . 'ssh) + (ssh-user . ,(match-string 1 url)) + (domain . ,(match-string 2 url)) + (sparse-repo (owner (login . ,(match-string 3 url))) + (name . ,(match-string 4 url))))) + (and (string-match + ;; https://github.com/vermiculus/magithub.git + ;; git://github.com/vermiculus/magithub.git + ;; ssh://git@github.com/vermiculus/magithub + ;; git+ssh://github.com/vermiculus/magithub.git + (rx bol + (or (seq "http" (? "s")) + (seq "ssh") + (seq "git" (? "+ssh"))) + "://" + (group (+? any)) ;domain -- github.com + "/" + (group (+? (| alnum "-" "." "_"))) ;owner.login -- vermiculus + "/" + (group (+? (| alnum "-" "." "_"))) ;name -- magithub + (? ".git") + eol) + url) + `((kind . 'http) + (domain . ,(match-string 1 url)) + (sparse-repo (owner (login . ,(match-string 2 url))) + (name . ,(match-string 3 url)))))))) + +(defun magithub--url->repo (url) + "Tries to parse a remote url into a GitHub repository object" + (cdr (assq 'sparse-repo (magithub--repo-parse-url url)))) + +(defun magithub-source--sparse-repo () + "Returns the sparse repository object for the current context. + +Only information that can be determined without API calls will be +included in the returned object." + (magithub-repo-from-remote--sparse + (magithub-settings-context-remote))) + +(defun magithub-repo-from-remote (remote) + (when-let ((repo (magithub-repo-from-remote--sparse remote))) + (magithub-repo repo))) + +(defun magithub-repo-from-remote--sparse (remote) + (magithub--url->repo (magit-get "remote" remote "url"))) + +(defalias 'magithub-source-repo 'magithub-repo) +(make-obsolete 'magithub-source-repo 'magithub-repo "0.1.4") +(defun magithub-repo (&optional sparse-repo) + "Turn SPARSE-REPO into a full repository object. +If SPARSE-REPO is null, the current context is used. + +SPARSE-REPO may either be a partial repository object (with at +least the `.owner.login' and `.name' keys) or a string identifier +of the form `owner/name' (as in `vermiculus/magithub')." + (if (and (stringp sparse-repo) + (string-match (rx bos + (group (+? (| alnum "-" "." "_"))) ;owner.login -- vermiculus + "/" + (group (+? (| alnum "-" "." "_"))) ;name -- magithub + eos) + sparse-repo)) + (magithub-repo `((owner (login . ,(match-string 1 sparse-repo))) + (name . ,(match-string 2 sparse-repo)))) + (when-let ((sparse-repo (or sparse-repo (magithub-source--sparse-repo)))) + (or (magithub-cache :repo-demographics + `(or (magithub-request + (ghubp-get-repos-owner-repo ',sparse-repo)) + (and (not (magithub--api-available-p)) + sparse-repo))) + (when (magithub-online-p) + (let ((magithub-cache--refresh t)) + (magithub-repo sparse-repo))) + sparse-repo)))) + +;;; Repository utilities + +(defvar magit-magithub-repo-section-map + (let ((m (make-sparse-keymap))) + (define-key m [remap magit-visit-thing] #'magithub-repo-visit) + m)) + +(defun magithub-repo-visit (repo) + "Visit REPO on GitHub." + (interactive (list (thing-at-point 'github-repository))) + (if-let ((url (alist-get 'html_url repo))) + (browse-url url) + (user-error "No URL for repo"))) + +(defun magithub-repo-visit-issues (repo) + "Visit REPO's issues on GitHub." + (interactive (list (thing-at-point 'github-repository))) + (if-let ((url (alist-get 'html_url repo))) + (browse-url (format "%s/issues" url)) + (user-error "No URL for repo"))) + +(defun magithub-repo-name (repo) + "Return the full name of REPO. +If the `full_name' object is present, use that. Otherwise, +concatenate `.owner.login' and `.name' with `/'." + (let-alist repo (or .full_name (concat .owner.login "/" .name)))) + +(defun magithub-repo-admin-p (&optional repo) + "Non-nil if the currently-authenticated user can manage REPO. +REPO defaults to the current repository." + (let-alist (magithub-repo (or repo (thing-at-point 'github-repository))) + .permissions.admin)) + +(defun magithub-repo-push-p (&optional repo) + "Non-nil if the currently-authenticated user can manage REPO. +REPO defaults to the current repository." + (let-alist (magithub-repo (or repo (thing-at-point 'github-repository))) + .permissions.push)) + +(defun magithub--repo-simplify (repo) + "Convert full repository object REPO to a sparse repository object." + (let (login name) + ;; There are syntax problems with things like `,.owner.login' + (let-alist repo + (setq login .owner.login + name .name)) + `((owner (login . ,login)) + (name . ,name)))) + +(defun magithub-repo-remotes () + "Return GitHub repositories in this repository. +`magit-list-remotes' is filtered to those remotes that point to +GitHub repositories." + (delq nil (mapcar (lambda (r) + (when-let ((repo (magithub-repo-from-remote r))) + (cons r repo))) + (magit-list-remotes)))) + +(defun magithub-read-repo (prompt) + "Using PROMPT, read a GitHub repository. +See also `magithub-repo-remotes'." + (let* ((remotes (magithub-repo-remotes)) + (maxlen (->> remotes + (mapcar #'car) + (mapcar #'length) + (apply #'max))) + (fmt (format "%%-%ds (%%s/%%s)" maxlen))) + (magithub-repo + (cdr (magithub--completing-read + prompt (magithub-repo-remotes) + (lambda (remote-repo-pair) + (let-alist (cdr remote-repo-pair) + (format fmt (car remote-repo-pair) .owner.login .name)))))))) + +(defun magithub-repo-remotes-for-repo (repo) + (-filter (lambda (remote) + (let-alist (list (cons 'repo repo) + (cons 'remote (magithub-repo-from-remote remote))) + (and (string= .repo.owner.login + .remote.owner.login) + (string= .repo.name .remote.name)))) + (magit-list-remotes))) + +;;; Feature checking + +(declare-function magithub-pull-request-merge "magithub-issue-tricks" + (pull-request &optional args)) +(declare-function magithub-maybe-insert-ci-status-header "magithub-ci" ()) +(declare-function magithub-issue--insert-pr-section "magithub-issue" ()) +(declare-function magithub-issue--insert-issue-section "magithub-issue" ()) +(declare-function magithub-completion-enable "magithub-completion" ()) +(defconst magithub-feature-list + ;; features must only return nil if they fail to install + `((pull-request-merge . ,(lambda () + (magit-define-popup-action 'magit-am-popup + ?P "Apply patches from pull request" + #'magithub-pull-request-merge) + t)) + + (commit-browse . ,(lambda () + (define-key magit-commit-section-map "w" + #'magithub-commit-browse) + t)) + + (status-checks-header . ,(lambda () + (add-hook 'magit-status-headers-hook + #'magithub-maybe-insert-ci-status-header + t) + t)) + + (completion . ,(lambda () + (dolist (hook '(git-commit-setup-hook magithub-edit-mode-hook)) + (add-hook hook #'magithub-completion-enable)) + t)) + + ;; order is important in this list; pull request section should + ;; come before issues section by default + (pull-requests-section . ,(lambda () + (add-hook 'magit-status-sections-hook + #'magithub-issue--insert-pr-section + t) + t)) + + (issues-section . ,(lambda () + (add-hook 'magit-status-sections-hook + #'magithub-issue--insert-issue-section + t) + t))) + "All Magit-integration features of Magithub. +See `magithub-feature-autoinject'. + +- `pull-request-merge' + Apply patches from pull requests. + (`magithub-pull-request-merge' inserted into `magit-am-popup') + +- `commit-browse' + Browse commits using \\<magithub-map>\\[magithub-browse-thing]. + +- `completion' + Enable `completion-at-point' support for #issue and @user references + where possible. + +- `issues-section' + View issues in the `magit-status' buffer. + +- `pull-requests-section' + View pull requests in the `magit-status' buffer. + +- `status-checks-header' + View project status in the `magit-status' buffer (e.g., CI).") + +(defvar magithub-features nil + "An alist of feature-symbols to Booleans. +When a feature symbol maps to non-nil, that feature is considered +'loaded'. Thus, to disable all messages, prepend '(t . t) to +this list. + +Example: + + ((pull-request-merge . t) (other-feature . nil)) + +signals that `pull-request-merge' is a loaded feature and +`other-feature' has not been loaded and will not be loaded. + +See `magithub-feature-list'.") + +;;;###autoload +(defun magithub-feature-autoinject (feature) + "Configure FEATURE to recommended settings. +If FEATURE is `all' or t, all known features will be loaded. If +FEATURE is a list, then it is a list of FEATURE symbols to load. + +See `magithub-feature-list' for a list of available features and +`magithub-features' for a list of currently-installed features." + (cond + ((memq feature '(t all)) + (mapc #'magithub-feature-autoinject + (mapcar #'car magithub-feature-list))) + ((listp feature) + (mapc #'magithub-feature-autoinject feature)) + (t + (if-let ((install (cdr (assq feature magithub-feature-list)))) + (if (functionp install) + (if-let ((result (funcall install))) + (add-to-list 'magithub-features (cons feature t)) + (error "feature %S failed to install: %S" feature result)) + (error "install form for %S not a function: %S" feature install)) + (user-error "unknown feature %S" feature))))) + +(defun magithub-feature-check (feature) + "Check if a Magithub FEATURE has been configured. +See `magithub-features'." + (if (listp magithub-features) + (let* ((p (assq feature magithub-features))) + (if (consp p) (cdr p) + (cdr (assq t magithub-features)))) + magithub-features)) + +(defun magithub-feature-maybe-idle-notify (&rest feature-list) + "Notify user if any of FEATURES are not yet configured." + (unless (-all? #'magithub-feature-check feature-list) + (let ((m "Magithub features not configured: %S") + (s "see variable `magithub-features' to turn off this message")) + (run-with-idle-timer + 1 nil (lambda () + (message (concat m "; " s) feature-list) + (add-to-list 'feature-list '(t . t) t)))))) + +;;; Getting help + +(defun magithub--meta-new-issue () + "Open a new Magithub issue. +See /.github/ISSUE_TEMPLATE.md in this repository." + (interactive) + (browse-url "https://github.com/vermiculus/magithub/issues/new")) + +(defun magithub--meta-help () + "Open Magithub help." + (interactive) + (browse-url "https://gitter.im/vermiculus/magithub")) + +(defun magithub-error (err-message &optional tag trace) + "Report a Magithub error. + +ERR-MESSAGE is a string to be shown to the user. + +TAG, if provided, is a user-friendly description of the error. +It defaults to ERR-MESSAGE. + +If TRACE is provided, it should be an appropriate backtrace to +describe the error. If not provided, it is retrieved." + (unless (stringp err-message) + ;; just in case. it'd be embarassing if the bug-reporter was + ;; perceived as buggy + (setq err-message (prin1-to-string err-message))) + (setq trace (or trace (with-output-to-string (backtrace))) + tag (or tag err-message)) + (when (magithub-confirm-no-error 'report-error tag) + (with-current-buffer-window + (get-buffer-create "*magithub issue*") + #'display-buffer-pop-up-window nil + (when (fboundp 'markdown-mode) (markdown-mode)) + (insert + (kill-new + (format + "## Automated error report + +%s + +### Description + +%s + +### Backtrace + +``` +%s``` +" + err-message + (read-string "Briefly describe what you were doing: ") + trace)))) + (magithub--meta-new-issue)) + (error err-message)) + +;;; Miscellaneous utilities + +(defcustom magithub-datetime-format "%c" + "The display format string for date-time values. +See also `format-time-string'." + :group 'magithub + :type 'string) + +(defun magithub--parse-time-string (iso8601) + "Parse ISO8601 into a time value. +ISO8601 is expected to not have a TZ component." + (parse-iso8601-time-string (concat iso8601 "+00:00"))) + +(defun magithub--format-time (time) + "Format TIME according to `magithub-datetime-format'. +TIME may be a time value or a string. + +Eventually, TIME will always be a time value." + ;; todo: ghub+ needs to convert time values for defined response fields + (format-time-string + magithub-datetime-format + (or (and (stringp time) + (magithub--parse-time-string time)) + time))) + +(defun magithub--completing-read + (prompt collection &optional format-function predicate require-match default) + "Using PROMPT, get a list of elements in COLLECTION. +This function continues until all candidates have been entered or +until the user enters a value of \"\". Duplicate entries are not +allowed." + (let* ((format-function (or format-function (lambda (o) (format "%S" o)))) + (collection (if (functionp predicate) (-filter predicate collection) collection)) + (collection (magithub--zip collection format-function nil))) + (cdr (assoc-string + (completing-read prompt collection nil require-match + (when default (funcall format-function default))) + collection)))) + +(defun magithub--completing-read-multiple + (prompt collection &optional format-function predicate require-match default) + "Using PROMPT, get a list of elements in COLLECTION. +This function continues until all candidates have been entered or +until the user enters a value of \"\". Duplicate entries are not +allowed." + (let ((this t) (coll (copy-tree collection)) ret) + (while (and collection this) + (setq this (magithub--completing-read + prompt coll format-function + predicate require-match default)) + (when this + (push this ret) + (setq coll (delete this coll)))) + ret)) + +(defconst magithub-hash-regexp + (rx bow (= 40 (| digit (any (?A . ?F) (?a . ?f)))) eow) + "Regexp for matching commit hashes.") + +(defun magithub-usable-p () + "Non-nil if Magithub should do its thing." + (and (magithub-enabled-p) + (magithub-github-repository-p) + (magithub-source--sparse-repo))) + +(defun magithub--zip-case (p e) + "Get an appropriate value for element E given property/function P." + (cond + ((null p) e) + ((functionp p) (funcall p e)) + ((symbolp p) (plist-get e p)) + (t nil))) + +(defun magithub--zip (object-list prop1 prop2) + "Process OBJECT-LIST into an alist defined by PROP1 and PROP2. + +If a prop is a symbol, that property will be used. + +If a prop is a function, it will be called with the +current element of OBJECT-LIST. + +If a prop is nil, the entire element is used." + (delq nil + (-zip-with + (lambda (e1 e2) + (let ((p1 (magithub--zip-case prop1 e1)) + (p2 (magithub--zip-case prop2 e2))) + (unless (or (and prop1 (not p1)) + (and prop2 (not p2))) + (cons (if prop1 p1 e1) + (if prop2 p2 e2))))) + object-list object-list))) + +(defun magithub--satisfies-p (preds obj) + "Non-nil when all functions in PREDS are non-nil for OBJ." + (while (and (listp preds) + (functionp (car preds)) + (funcall (car preds) obj)) + (setq preds (cdr preds))) + (null preds)) + +(defun magithub-section-type (section) + "If SECTION is a magithub-type section, return the type. +For example, if + + (eq (magit-section-type SECTION) \\='magithub-issue) + +return the interned symbol `issue'." + (let* ((type (oref section type)) + (name (symbol-name type))) + (and (string-prefix-p "magithub-" name) + (intern (substring name 9))))) + +(defvar magithub--section-value-at-point-specializations + '((user assignee)) + "Alist of general types to specific types. +Specific types offer more relevant functionality to a given +section, but are inconvenient for +`magithub--section-value-at-point'. This alist defines +equivalencies such that a search for the general type will also +return sections of a specialized type.") + +(define-obsolete-function-alias + 'magithub-thing-at-point + #'magithub--section-value-at-point + "0.1.5") + +;;;###autoload +(defun magithub--section-value-at-point (type) + "Determine the thing of TYPE at point. +This is intended for use as a resolving function for +`thing-at-point'. + +The following symbols are defined, but other values may work with +this function: `github-user', `github-issue', `github-label', +`github-comment', `github-repository', `github-pull-request', +`github-notification'," + (let ((search-sym (intern (concat "magithub-" (symbol-name type)))) + this-section) + (if (and (boundp search-sym) (symbol-value search-sym)) + (symbol-value search-sym) + (setq this-section (magit-current-section)) + (while (and this-section + (not (let ((this-type (magithub-section-type this-section))) + (or + ;; exact match + (eq type this-type) + ;; equivalency + (thread-last magithub--section-value-at-point-specializations + (alist-get type) + (memq this-type)))))) + (setq this-section (oref this-section parent))) + (and this-section (oref this-section value))))) + +(defvar-local magithub-issue nil + "Issue object.") + +(defvar-local magithub-comment nil + "Comment object.") + +(defvar-local magithub-repo nil + "Repository object.") + +;;;###autoload +(put 'github-user 'thing-at-point + (lambda () + (magithub--section-value-at-point 'user))) + +;;;###autoload +(put 'github-issue 'thing-at-point + (lambda () + (or magithub-issue + (magithub--section-value-at-point 'issue)))) + +;;;###autoload +(put 'github-label 'thing-at-point + (lambda () + (magithub--section-value-at-point 'label))) + +;;;###autoload +(put 'github-comment 'thing-at-point + (lambda () + (or magithub-comment + (magithub--section-value-at-point 'comment)))) + +;;;###autoload +(put 'github-notification 'thing-at-point + (lambda () + (magithub--section-value-at-point 'notification))) + +;;;###autoload +(put 'github-repository 'thing-at-point + (lambda () + (or (magithub--section-value-at-point 'repository) + magithub-repo + (magithub-repo)))) + +;;;###autoload +(put 'github-pull-request 'thing-at-point + (lambda () + (or (magithub--section-value-at-point 'pull-request) + (when-let ((issue (thing-at-point 'github-issue))) + (and + (magithub-issue--issue-is-pull-p issue) + (magithub-cache :issues + `(magithub-request + (ghubp-get-repos-owner-repo-pulls-number + ',(magithub-issue-repo issue) + ',issue)))))))) + +(defun magithub-verify-manage-labels (&optional interactive) + "Verify the user has permission to manage labels. +If the authenticated user does not have permission, an error will +be signaled. + +If INTERACTIVE is non-nil, a `user-error' will be raised instead +of a signal (e.g., for interactive forms)." + (let-alist (thing-at-point 'github-repository) + (if .permissions.push t + (if interactive + (user-error "You're not allowed to manage labels in %s" .full_name) + (signal 'error `(unauthorized manage-labels ,(progn .full_name))))))) + +(defun magithub-bug-reference-mode-on () + "In GitHub repositories, configure `bug-reference-mode'." + (interactive) + (when (magithub-usable-p) + (when-let ((repo (magithub-repo))) + (bug-reference-mode 1) + (setq-local bug-reference-bug-regexp "#\\(?2:[0-9]+\\)") + (setq-local bug-reference-url-format + (format "%s/issues/%%s" (alist-get 'html_url repo)))))) + +(defun magithub-filter-all (funcs list) + "Return LIST without elements that fail any element of FUNCS." + (dolist (f funcs) + (setq list (cl-remove-if-not f list))) + list) + +(defcustom magithub-preferred-remote-method 'ssh_url + "Preferred method when cloning or adding remotes. +One of the following: + + `clone_url' (https://github.com/octocat/Hello-World.git) + `git_url' (git://github.com/octocat/Hello-World.git) + `ssh_url' (git@github.com:octocat/Hello-World.git)" + :group 'magithub + :type '(choice + (const :tag "https" clone_url) + (const :tag "git" git_url) + (const :tag "ssh" ssh_url))) + +(defun magithub-repo--clone-url (repo) + "Get the preferred cloning URL from REPO." + (alist-get magithub-preferred-remote-method repo)) + +(defun magithub--wait-for-git (proc &optional seconds) + "Wait for git process PROC, polling every SECONDS seconds." + (let ((seconds (or seconds 0.5))) + (while (process-live-p proc) + (sit-for seconds)))) + +(defmacro magithub--run-git-synchronously (&rest body) + (declare (debug t)) + (let ((valsym (cl-gensym)) final-form) + (while body + (let ((form (pop body))) + (push `(let ((,valsym ,form)) + (if (processp ,valsym) + (magithub--wait-for-git ,valsym) + ,valsym)) + final-form))) + `(progn + ,@(nreverse final-form)))) + +(defun magithub-core-bucket (collection key-func &optional value-func) + "Bucket COLLECTION by ENTRY-FUNC and VALUE-FUNC. + +Each element of COLLECTION is passed through KEY-FUNC to +determine its key in an alist. If specified, the value is +determined by VALUE-FUNC. + +Returns an alist of these keys to lists of values. + +See also `magithub-fnnor-each-bucket'." + (unless value-func + (setq value-func #'identity)) + (let (bucketed) + (dolist (item collection) + (let ((entry (funcall key-func item)) + (val (funcall value-func item))) + (if-let (bucket (assoc entry bucketed)) + (push val (cdr bucket)) + (push (cons entry (list val)) + bucketed)))) + bucketed)) + +(defmacro magithub-core-bucket-multi (collection &rest buckets) + "Chain calls to `magithub-core-bucket'." + (declare (indent 1)) + (let* ((fnelsym (cl-gensym)) + (apply-to fnelsym) + form) + (while buckets + (setq form `(magithub-core-bucket + ,(or form collection) + (lambda (,fnelsym) (funcall ,(pop buckets) ,apply-to))) + apply-to `(car ,apply-to))) + form)) + +(defmacro magithub-for-each-bucket (buckets key values &rest body) + "Do things for each bucket in BUCKETS. + +For each bucket in BUCKETs, bind the key to KEY and its +contents (a list) to VALUES and execute BODY. + +See also `magithub-core-bucket'." + (declare (indent 3) (debug t)) + (let ((buckets-sym (cl-gensym))) + `(let ((,buckets-sym ,buckets)) + (while ,buckets-sym + (-let (((,key . ,values) (pop ,buckets-sym))) + ,@body))))) + +(defmacro magithub-defsort (symbol compare doc accessor) + "Define SYMBOL to be a sort over two objects. +COMPARE is used on the application of ACCESSOR to each argument." + (declare (doc-string 3) (indent 2)) + `(defun ,symbol (a b) ,doc (,(eval compare) + (funcall ,accessor a) + (funcall ,accessor b)))) + +(defun magithub-core-color-completing-read (prompt) + "Generic completing-read for a color." + (let* ((colors (list-colors-duplicates)) + (len (apply #'max (mapcar (lambda (c) (length (car c))) colors))) + (sample (make-string 20 ?\ ))) + (car + (magithub--completing-read + prompt colors + (lambda (colors) + (format (format "%%-%ds %%s" len) (car colors) + (propertize sample 'face `(:background ,(car colors))))))))) + +(defun magit-section-show-level-5 () + "Show surrounding sections up to fifth level." + (interactive) + (magit-section-show-level 5)) + +(defun magit-section-show-level-5-all () + "Show all sections up to fifth level." + (interactive) + (magit-section-show-level -5)) + +(defun magithub--refresh-reset () + "Reset everything to the defaults after refreshing. +To be added to `magit-unwind-refresh-hook'." + (setq magithub-cache--refresh nil) + ;; reclaim some memory + (setq magithub-cache--refreshed-forms nil)) + +(defvar magithub-cache--refresh nil + ;; Can also consider making this a list in the future to refresh + ;; multiple forms. No current use-case for this, though. + "Non-nil when refreshing. +If t, all form classes will be refreshed. Otherwise, if non-nil, +this variable is expected to be `eq' to the class of forms that +should be selectively refreshed.") + +(make-obsolete 'magithub-refresh 'magithub--refresh "0.2") +(defun magithub-refresh () + (interactive (user-error (substitute-command-keys + "This is no longer an interactive function; \ +use \\[universal-argument] \\[magit-refresh] instead :-)")))) + +(defun magithub--refresh () + "Refresh GitHub data. +Use directly at your own peril; this is intended for use with +`magit-pre-refresh-hook'." + (when (and current-prefix-arg + (memq this-command '(magit-refresh + magit-refresh-all + magithub-ci-refresh + magithub-issue-refresh)) + (magithub-usable-p) + (magithub-confirm-no-error 'refresh) + (or (magithub--api-available-p) + (magithub-confirm-no-error 'refresh-when-API-unresponsive))) + ;; `magithub--refresh' is part of `magit-pre-refresh-hook' and our requests + ;; are made as part of `magit-refresh'. There's no way we can let-bind + ;; `magithub-settings--refresh' around that entire form, so we do the next + ;; best thing: use `magit-unwind-refresh-hook' to reset the override back + ;; to its old value. + (setq magithub-cache--refresh t) + (setq magithub-cache--refreshed-forms nil))) + +(defun magithub-wash-gfm (text) + "Wash TEXT as it comes from the API." + (with-temp-buffer + (insert text) + (goto-char (point-min)) + (while (search-forward "
" nil t) + (delete-char -1)) + (s-trim (buffer-string)))) + +(defun magithub-fill-gfm (text) + "Fill TEXT according to GFM rules." + (with-temp-buffer + (delay-mode-hooks + (gfm-mode) ;autoloaded + (insert text) + ;; re font-lock-ensure: see jrblevin/markdown-mode#251 + (font-lock-ensure) + (fill-region (point-min) (point-max)) + (buffer-string)))) + +(defun magithub-indent-text (indent text) + "Indent TEXT by INDENT spaces." + (replace-regexp-in-string (rx bol) (make-string indent ?\ ) text)) + +(defun magithub-commit-browse (rev) + "Browse REV on GitHub. +Interactively, this is the commit at point." + (interactive (list (or (when-let ((rev (magit-rev-verify + (oref (magit-current-section) value)))) + rev) + (thing-at-point 'git-revision)))) + (if-let ((parsed (magit-rev-parse rev))) + (if-let ((commits (magithub-request + (ghubp-get-repos-owner-repo-commits + (magithub-repo) nil + :sha parsed)))) + (let-alist (car commits) + (browse-url .html_url)) + (user-error "No commit %s on remote" parsed)) + (error "Could not parse %S" rev))) + +(defun magithub-add-thing () + "Conceptual command to add a thing (e.g., label, assignee, ...)" + (interactive) + (user-error "There is no thing at point that could be added to")) + +(defun magithub-browse-thing () + "Conceptual command to browse a thing on GitHub" + (interactive) + (user-error "There is no thing at point that could be browsed")) + +(defun magithub-edit-thing () + "Conceptual command to edit a thing (e.g., comment)" + (interactive) + (user-error "There is no thing at point that could be edited")) + +(defun magithub-reply-thing () + "Conceptual command to reply to a thing (e.g., comment)" + (interactive) + (user-error "There is no thing at point that could be replied to")) + +(defvar magithub-map + (let ((m (make-sparse-keymap))) + (define-key m "a" #'magithub-add-thing) + (define-key m "w" #'magithub-browse-thing) + (define-key m "e" #'magithub-edit-thing) + (define-key m "r" #'magithub-reply-thing) + m) + "Parent keymap for Magithub sections.") + +(defmacro magithub-request (&rest body) + "Execute BODY authenticating as Magithub." + (declare (debug t)) + `(ghubp-override-context auth 'magithub + ,@body)) + +(defun magithub-debug-section (section) + (interactive (list (magit-current-section))) + (pp-eval-expression `(oref ,section value))) + +(eval-after-load 'magit + '(progn + (dolist (hook '(magit-revision-mode-hook git-commit-setup-hook)) + (add-hook hook #'magithub-bug-reference-mode-on)) + (add-hook 'magit-pre-refresh-hook #'magithub--refresh) + (add-hook 'magit-unwind-refresh-hook + #'magithub--refresh-reset))) + +(provide 'magithub-core) +;;; magithub-core.el ends here diff --git a/magithub-dash.el b/magithub-dash.el new file mode 100644 index 0000000..5e2bb84 --- /dev/null +++ b/magithub-dash.el @@ -0,0 +1,219 @@ +;;; magithub-dash.el --- magithub dashboard -*- lexical-binding: t; -*- + +;; Copyright (C) 2017-2018 Sean Allred + +;; Author: Sean Allred <code@seanallred.com> +;; Keywords: hypermedia + +;; 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 <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Magithub-Dash is a dashboard for your GitHub activity. + +;;; Code: + +(require 'magit) +(require 'magithub-core) +(require 'magithub-notification) +(require 'magithub-issue) + +(declare-function magithub-dispatch-popup "magithub.el") + +(defcustom magithub-dashboard-show-read-notifications t + "Show read notifications in the dashboard." + :type 'boolean + :group 'magithub) + +(magit-define-popup magithub-dashboard-popup + "Popup console for the dashboard." + 'magithub-commands + :actions '("Notifications" + (?r "Toggle showing read notifications" + magithub-dashboard-show-read-notifications-toggle))) + +(defun magithub-dashboard-show-read-notifications-toggle () + (interactive) + (setq magithub-dashboard-show-read-notifications + (not magithub-dashboard-show-read-notifications)) + (magit-refresh-buffer)) + +;;;###autoload +(defun magithub-dashboard () + "View your GitHub dashboard." + (interactive) + (let ((magit-generate-buffer-name-function + (lambda (&rest _) "*magithub-dash*"))) + (magit-mode-setup #'magithub-dash-mode))) + +(defvaralias 'magithub-dash-map 'magithub-dash-mode-map + "Old name of `magithub-dash-mode-map'. +This will be removed in a future version.") +(defvar magithub-dash-mode-map + (let ((m (make-sparse-keymap))) + (set-keymap-parent m magit-mode-map) + (define-key m (kbd "5") #'magit-section-show-level-5) + (define-key m (kbd "M-5") #'magit-section-show-level-5-all) + (define-key m (kbd ";") #'magithub-dashboard-popup) + (define-key m (kbd "H") #'magithub-dispatch-popup) + m) + "Keymap for `magithub-dash-mode'.") +;; todo: remove on version bump + +(define-derived-mode magithub-dash-mode + magit-mode "Magithub-Dash" + "Major mode for your GitHub dashboard.") + +(defun magithub-dash-refresh-buffer (&rest _args) + "Refresh the dashboard. +Runs `magithub-dash-sections-hook'." + (interactive) + (magit-insert-section (magithub-dash-buf) + (run-hooks 'magithub-dash-sections-hook)) + (let ((inhibit-read-only t)) + (save-excursion + (goto-char (point-max)) + (delete-blank-lines)))) + +(defvar magithub-dash-sections-hook + '(magithub-dash-insert-headers + magithub-dash-insert-notifications + magithub-dash-insert-issues) + "Sections inserted by `magithub-dashboard'.") + +(defvar magithub-dash-headers-hook + '(magithub-dash-insert-user-name-header + magithub-dash-insert-ratelimit-header + magithub-maybe-report-offline-mode) + "Headers inserted by `magithub-dash-insert-headers'.") + +(defun magithub-dash-insert-headers () + "Insert dashboard headers. +See also `magithub-dash-headers-hook'." + (magit-insert-headers magithub-dash-headers-hook)) + +(defun magithub-dash-insert-user-name-header (&optional user) + "Inserts a header for USER's name and login." + (setq user (or user (magithub-user-me))) + (let-alist user + (when .login + (let ((login (propertize .login 'face 'magithub-user))) + (magit-insert-section (magithub-user user) + (insert (format "%-10s" "User:") + (if .name + (format "%s (%s)" .name login) + login) + "\n")))))) + +(defun magithub-dash-insert-ratelimit-header () + "If API requests are being rate-limited, insert relevant information." + (magithub-request + (when-let ((ratelimit (ghubp-ratelimit))) + (when (time-less-p (alist-get 'reset ratelimit) (current-time)) + (ghubp-ratelimit 'no-headers))) + (let-alist (ghubp-ratelimit) + (when .limit + (magit-insert-section (magithub-ratelimit) + (let* ((seconds-until-reset (time-to-seconds + (time-subtract .reset + (current-time)))) + (ratio (/ (float .remaining) .limit))) + (insert + (format "%-10s%s - %d/%d requests; %s until reset\n" "Requests:" + (cond + ((< 0.50 ratio) (propertize "OK" 'face 'success)) + ((< 0.25 ratio) (propertize "Running low..." 'face 'warning)) + (t (propertize "Danger!" 'face 'error))) + .remaining + .limit + (magithub-cache--time-out seconds-until-reset))))))))) + +(defun magithub-dash-insert-notifications (&optional notifications) + "Insert NOTIFICATIONS into the buffer bucketed by repository." + (setq notifications (or notifications + (magithub-notifications + magithub-dashboard-show-read-notifications))) + (if notifications + (let* ((bucketed (magithub-core-bucket + notifications + (apply-partially #'alist-get 'repository))) + (unread (if magithub-dashboard-show-read-notifications + (-filter #'magithub-notification-unread-p notifications) + notifications)) + (hide (not unread)) + (heading (if magithub-dashboard-show-read-notifications + (format "%s (%d unread of %d)" + (propertize "Notifications" + 'face 'magit-section-heading) + (length unread) + (length notifications)) + (format "%s (%d)" + (propertize "Notifications" + 'face 'magit-section-heading) + (length notifications))))) + (magit-insert-section (magithub-notifications notifications hide) + (magit-insert-heading heading) + (magithub-for-each-bucket bucketed repo repo-notifications + (setq hide (null (-filter #'magithub-notification-unread-p + repo-notifications))) + (magit-insert-section (magithub-repo repo hide) + (magit-insert-heading + (concat (propertize (magithub-repo-name repo) 'face 'magithub-repo) + (propertize "..." 'face 'magit-dimmed))) + (mapc #'magithub-notification-insert-section repo-notifications) + (insert "\n"))) + (insert "\n"))) + (magit-insert-section (magithub-notifications) + (magit-insert-heading "Notifications") + (insert (propertize (if magithub-dashboard-show-read-notifications + "No notifications" + "No unread notifications") + 'face 'magit-dimmed) + "\n\n")))) + +(defun magithub-dash-insert-issues (&optional issues title) + "Insert ISSUES bucketed by their source repository. + +If ISSUES is not defined, all issues assigned to the current user +will be used." + (magithub-request + (setq issues (or issues (magithub-cache :issues `(magithub-request + (ghubp-get-issues)))) + title (or title "Issues Assigned to Me")) + (when-let ((user-repo-issue-buckets + ;; bucket by user then by repo + (magithub-core-bucket-multi issues + #'magithub-issue-repo + (lambda (repo) (alist-get 'owner repo))))) + (magit-insert-section (magithub-users-repo-issue-buckets) + (magit-insert-heading + (format "%s (%d)" + (propertize title 'face 'magit-section-heading) + (length issues))) + (magithub-for-each-bucket user-repo-issue-buckets user repo-issue-buckets + (magit-insert-section (magithub-user-repo-issues) + (magit-insert-heading + (propertize (alist-get 'login user) 'face 'magithub-user) + (propertize "/..." 'face 'magit-dimmed)) + (magithub-for-each-bucket repo-issue-buckets repo repo-issues + (magit-insert-section (magithub-repo-issues repo) + (magit-insert-heading + (format "%s:" (propertize (alist-get 'name repo) + 'face 'magithub-repo))) + (magithub-issue-insert-sections repo-issues) + (insert "\n"))))) + (insert "\n"))))) + +(provide 'magithub-dash) +;;; magithub-dash.el ends here diff --git a/magithub-edit-mode.el b/magithub-edit-mode.el new file mode 100644 index 0000000..1462c32 --- /dev/null +++ b/magithub-edit-mode.el @@ -0,0 +1,208 @@ +;;; magithub-edit-mode.el --- message-editing mode -*- lexical-binding: t; -*- + +;; Copyright (C) 2016-2018 Sean Allred + +;; Author: Sean Allred <code@seanallred.com> +;; Keywords: multimedia + +;; 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 <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Edit generic GitHub (markdown) content. To be used for comments, +;; issues, pull requests, etc. + +;;; Code: + +(require 'markdown-mode) +(require 'git-commit) + +(defvar magithub-edit-mode-map + (let ((m (make-sparse-keymap))) + (define-key m (kbd "C-c C-c") #'magithub-edit-submit) + (define-key m (kbd "C-c C-k") #'magithub-edit-cancel) + m) + "Keymap for `magithub-edit-mode'.") + +;;;###autoload +(define-derived-mode magithub-edit-mode gfm-mode "Magithub-Edit" + "Major mode for editing GitHub issues and pull requests.") + +(defvar-local magithub-edit-submit-function nil + "Populated by SUBMIT in `magithub-edit-new'.") +(defvar-local magithub-edit-cancel-function nil + "Populated by CANCEL in `magithub-edit-new'.") +(defvar-local magithub-edit-previous-buffer nil + "The buffer we were in when the edit was initiated.") + +(defface magithub-edit-title + '((t :inherit markdown-header-face-1)) + "Face used for the title in issues and pull requests." + :group 'magithub-faces) + +(defun magithub-edit-submit () + "Submit this post. +Uses `magithub-edit-submit-function' to do so." + (interactive) + (unless (commandp magithub-edit-submit-function t) + (error "No submit function defined")) + (magithub-edit--done magithub-edit-submit-function) + (magithub-cache-without-cache t + (magit-refresh-buffer))) + +(defun magithub-edit-cancel () + "Cancel this post. +Offer to save a draft if the buffer is considered modified, then +call `magithub-edit-cancel-function'." + (interactive) + ;; Offer to save the draft + (if (and (buffer-modified-p) + ;; don't necessarily want to use `magithub-confirm', here + ;; this is potentially a very dangerous action + (y-or-n-p "Save draft? ")) + (save-buffer) + (set-buffer-modified-p nil)) + + ;; If the saved draft is empty, might as well delete it + (when (and (stringp buffer-file-name) + (file-readable-p buffer-file-name) + (string= "" (let ((f buffer-file-name)) + (with-temp-buffer + (insert-file-contents f) + (buffer-string))))) + (magithub-edit-delete-draft)) + + (magithub-edit--done magithub-edit-cancel-function)) + +(defun magithub-edit--done (callback) + "Cleanup this buffer. +If CALLBACK is a command, call it interactively. (This will +usually be the SUBMIT or CANCEL commands from +`magithub-edit-new'.) If that function returns a buffer, switch +to that buffer." + (let ((nextbuf magithub-edit-previous-buffer)) + (when (commandp callback t) + (let ((newbuf (save-excursion + (call-interactively callback)))) + (when (bufferp newbuf) + (setq nextbuf newbuf)))) + (set-buffer-modified-p nil) + (kill-buffer) + (when nextbuf + (let ((switch-to-buffer-preserve-window-point t)) + (switch-to-buffer nextbuf))))) + +(defun magithub-edit-delete-draft () + "Delete the draft for the current edit buffer." + (when (and (stringp buffer-file-name) + (file-exists-p buffer-file-name) + (file-writable-p buffer-file-name)) + (delete-file buffer-file-name magit-delete-by-moving-to-trash) + (message "Deleted %s" buffer-file-name)) + (set-visited-file-name nil)) + +(cl-defun magithub-edit-new (buffer-name &key cancel content file header prompt-discard-draft submit template) + "Generate a new edit buffer called BUFFER-NAME and return it. +'Edit' buffers provide a common interface and handling for +submitting, cancelling, and saving drafts of posts. + +CANCEL is a function to use upon \\[magithub-edit-cancel]. + +CONTENT is initial content for the buffer. It is considered +novel and the buffer will not be closed without prompting to save +a draft. + +FILE is the file to use for drafts of this post. + +HEADER is a title to use in the header line of the new buffer. + +If PROMPT-DISCARD-DRAFT is non-nil, this function will display +the draft before offering to delete it. This option is +recommended when using \\[universal-argument] with the command +that calls this function. + +SUBMIT is a function to use upon \\[magithub-edit-submit]. + +TEMPLATE is like CONTENT, but is not considered novel. We won't +ask to save a draft here if post is cancelled." + (declare (indent 1)) + (let ((prevbuf (current-buffer)) + (file (and (stringp file) + (file-writable-p file) + file)) + draft) + + ;; Load the draft + (setq draft (and (stringp file) + (file-readable-p file) + (with-temp-buffer + (insert-file-contents file) + (buffer-string)))) + (when (string= draft "") + (setq draft nil)) + + ;; Discard the draft if desired + (when (and draft prompt-discard-draft) + (with-current-buffer (get-buffer-create " *draft*") + (erase-buffer) + (insert draft) + (view-buffer-other-window (current-buffer)) + ;; don't necessarily want to use `magithub-confirm', here + ;; this is potentially a very dangerous action + (when (yes-or-no-p "Discard this draft? ") + (setq draft nil) + (when (file-writable-p file) + (delete-file file magit-delete-by-moving-to-trash))) + (kill-buffer (current-buffer)))) + + (with-current-buffer (get-buffer-create buffer-name) + (magithub-edit-mode) + + (setq magithub-edit-previous-buffer prevbuf + magithub-edit-submit-function submit + magithub-edit-cancel-function cancel) + (magit-set-header-line-format + (substitute-command-keys + (let ((line "submit: \\[magithub-edit-submit] | cancel: \\[magithub-edit-cancel]")) + (when header + (setq line (concat line " | " header))) + line))) + + (when file + (let ((orig-name (buffer-name)) + (dir default-directory)) + (set-visited-file-name file) + (rename-buffer orig-name) + (cd dir))) + + (cond + (draft + (insert draft) + (set-buffer-modified-p nil) + (goto-char (point-max)) + (message "Loaded existing draft from %s" file)) + (content + (insert content) + (goto-char (point-max)) + (message "Loaded initial content")) + (template + (insert template) + (set-buffer-modified-p nil) + (goto-char (point-min)) + (message "Loaded template"))) + + (current-buffer)))) + +(provide 'magithub-edit-mode) +;;; magithub-edit-mode.el ends here diff --git a/magithub-faces.el b/magithub-faces.el new file mode 100644 index 0000000..1f53628 --- /dev/null +++ b/magithub-faces.el @@ -0,0 +1,109 @@ +;;; magithub-faces.el --- faces of Magithub -*- lexical-binding: t; -*- + +;; Copyright (C) 2017-2018 Sean Allred + +;; Author: Sean Allred <code@seanallred.com> +;; Keywords: faces + +;; 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 <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Holds all faces for Magithub. + +;;; Code: + +(require 'faces) +(require 'magit) +(require 'git-commit) + +(defface magithub-repo + '((t :inherit magit-branch-remote)) + "Face used for repository names." + :group 'magithub-faces) + +(defface magithub-issue-title + '((t)) + "Face used for issue titles." + :group 'magithub-faces) + +(defface magithub-issue-number + '((t :inherit magit-dimmed)) + "Face used for issue numbers." + :group 'magithub-faces) + +(defface magithub-issue-title-edit + '((t :inherit magithub-issue-title :inherit (git-commit-summary))) + "Face used for post titles during editing." + :group 'magithub-faces) + +(defface magithub-issue-title-with-note + '((t :inherit magithub-issue-title :inherit (git-commit-summary))) + "Face used for issue titles when the issue has an attached note. +See also `magithub-issue-personal-note'." + :group 'magithub-faces) + +(defface magithub-user + '((t :inherit magit-log-author)) + "Face used for usernames." + :group 'magithub-faces) + +(defface magithub-ci-no-status + '((t :inherit magit-dimmed)) + "Face used when CI status is `no-status'." + :group 'magithub-faces) + +(defface magithub-ci-error + '((t :inherit magit-signature-untrusted)) + "Face used when CI status is `error'." + :group 'magithub-faces) + +(defface magithub-ci-pending + '((t :inherit magit-signature-untrusted)) + "Face used when CI status is `pending'." + :group 'magithub-faces) + +(defface magithub-ci-success + '((t :inherit success)) + "Face used when CI status is `success'." + :group 'magithub-faces) + +(defface magithub-ci-failure + '((t :inherit error)) + "Face used when CI status is `failure'" + :group 'magithub-faces) + +(defface magithub-ci-unknown + '((t :inherit magit-signature-untrusted)) + "Face used when CI status is `unknown'." + :group 'magithub-faces) + +(defface magithub-label '((t :box t)) + "The inherited face used for labels. +Feel free to customize any part of this face, but be aware that +`:foreground' will be overridden by `magithub-label-propertize'." + :group 'magithub) + +(defface magithub-notification-reason + '((t :inherit magit-dimmed)) + "Face used for notification reasons." + :group 'magithub-faces) + +(defface magithub-deleted-thing + '((t :background "red4" :inherit magit-section-highlight)) + "Face used for things about to be deleted." + :group 'magithub-faces) + +(provide 'magithub-faces) +;;; magithub-faces.el ends here diff --git a/magithub-issue-post.el b/magithub-issue-post.el new file mode 100644 index 0000000..8cbb641 --- /dev/null +++ b/magithub-issue-post.el @@ -0,0 +1,223 @@ +;;; magithub-issue-post.el --- -*- lexical-binding: t; -*- + +;; Copyright (C) 2017-2018 Sean Allred + +;; Author: Sean Allred <code@seanallred.com> + +;; 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 <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;;; Code: + +(require 'magithub-core) +(require 'magithub-issue) +(require 'magithub-label) +(require 'magithub-edit-mode) + +(declare-function magithub-issue-view "magithub-issue-view.el" (issue)) + +(defvar-local magithub-issue--extra-data nil) + +(defun magithub-issue-post-submit () + (interactive) + (let ((issue (magithub-issue-post--parse-buffer)) + (repo (magithub-repo))) + (when (s-blank-p (alist-get 'title issue)) + (user-error "Title is required")) + (when (magithub-repo-push-p repo) + (when-let ((issue-labels (magithub-label-read-labels "Labels: "))) + (push (cons 'labels issue-labels) issue))) + (magithub-confirm 'submit-issue) + (let ((issue (magithub-request + (ghubp-post-repos-owner-repo-issues repo issue)))) + (magithub-edit-delete-draft) + (magithub-issue-view issue)))) + +(defun magithub-issue-post--parse-buffer () + (let ((lines (split-string (buffer-string) "\n"))) + `((title . ,(s-trim (car lines))) + (body . ,(s-trim (mapconcat #'identity (cdr lines) "\n")))))) + +(defun magithub-issue-new (repo) + (interactive (list (magithub-repo))) + (let* ((repo (magithub-repo repo)) + (name (magithub-repo-name repo))) + (with-current-buffer + (magithub-edit-new (format "*magithub-issue: %s*" name) + :header (format "Creating an issue for %s" name) + :submit #'magithub-issue-post-submit + :file (expand-file-name "new-issue-draft" + (magithub-repo-data-dir repo)) + :template (magithub-issue--template-text "ISSUE_TEMPLATE")) + (font-lock-add-keywords nil `((,(rx bos (group (*? any)) eol) 1 + 'magithub-issue-title-edit t))) + (magithub-bug-reference-mode-on) + (magit-display-buffer (current-buffer))))) + +(defun magithub-pull-request-new-from-issue + (repo issue base head &optional maintainer-can-modify) + "Create a pull request from an existing issue. +REPO is the parent repository of ISSUE. BASE and HEAD are as +they are in `magithub-pull-request-new'." + (interactive (if-let ((issue-at-point (thing-at-point 'github-issue))) + (let-alist (magithub-pull-request-new-arguments) + (let ((allow-maint-mod (magithub-confirm-no-error + 'pr-allow-maintainers-to-submit))) + (magithub-confirm 'submit-pr-from-issue + (magithub-issue-reference issue-at-point) + .user+head .base) + (list .repo issue-at-point .base .head allow-maint-mod))) + (user-error "No issue detected at point"))) + (let ((pull-request `((head . ,head) + (base . ,base) + (issue . ,(alist-get 'number issue))))) + (when maintainer-can-modify + (push (cons 'maintainer_can_modify t) pull-request)) + (magithub-request + (ghubp-post-repos-owner-repo-pulls repo pull-request)))) + +(defun magithub-issue--template-text (template) + (with-temp-buffer + (when-let ((template (magithub-issue--template-find template))) + (insert-file-contents template) + (buffer-string)))) + +(defun magithub-issue--template-find (filename) + "Find an appropriate template called FILENAME and returns its absolute path. + +See also URL +`https://github.com/blog/2111-issue-and-pull-request-templates'" + (let ((default-directory (magit-toplevel)) + combinations) + (dolist (tryname (list filename (concat filename ".md"))) + (dolist (trydir (list default-directory (expand-file-name ".github/"))) + (push (expand-file-name tryname trydir) combinations))) + (-find #'file-readable-p combinations))) + +(defun magithub-remote-branches (remote) + "Return a list of branches on REMOTE." + (let ((regexp (concat (regexp-quote remote) (rx "/" (group (* any)))))) + (--map (and (string-match regexp it) + (match-string 1 it)) + (magit-list-remote-branch-names remote)))) + +(defun magithub-remote-branches-choose (prompt remote &optional default) + "Using PROMPT, choose a branch on REMOTE." + (let ((branches (magithub-remote-branches remote))) + (magit-completing-read + (format "[%s] %s" + (magithub-repo-name (magithub-repo-from-remote remote)) + prompt) + branches + nil t nil nil (and (member default branches) default)))) + +(defun magithub-pull-request-new-arguments () + (unless (magit-get-push-remote) + (user-error "Nothing on remote yet; have you pushed your branch? Aborting")) + (let* ((this-repo (magithub-read-repo "Fork's remote (this is you!) ")) + (this-repo-owner (let-alist this-repo .owner.login)) + (parent-repo (or (alist-get 'parent this-repo) this-repo)) + (this-remote (car (magithub-repo-remotes-for-repo this-repo))) + (on-this-remote (string= (magit-get-push-remote) this-remote)) + (base-remote (car (magithub-repo-remotes-for-repo parent-repo))) + (head-branch (let ((branch (magithub-remote-branches-choose + "Head branch" this-remote + (when on-this-remote + (magit-get-current-branch))))) + (unless (magit-rev-verify (magit-get-push-branch branch)) + (user-error "`%s' has not yet been pushed to your fork (%s)" + branch (magithub-repo-name this-repo))) + branch)) + (base (magithub-remote-branches-choose + "Base branch" base-remote + (or (and on-this-remote + (magit-get-upstream-branch head-branch)) + (let-alist parent-repo .default_branch)))) + (user+head (format "%s:%s" this-repo-owner head-branch))) + (when (magithub-request (ghubp-get-repos-owner-repo-pulls parent-repo nil + :head user+head)) + (user-error "A pull request on %s already exists for head \"%s\"" + (magithub-repo-name parent-repo) + user+head)) + `((repo . ,parent-repo) + (base . ,base) + (head . ,(if (string= this-remote base-remote) + head-branch + user+head)) + (head-no-user . ,head-branch) + (fork . ,this-repo) + (user+head . ,user+head)))) + +(defun magithub-pull-request-new (repo base head head-no-user) + "Create a new pull request." + (interactive (let-alist (magithub-pull-request-new-arguments) + (magithub-confirm 'pre-submit-pr .user+head + (magithub-repo-name .repo) .base) + (list .repo .base .head .head-no-user))) + (let ((is-single-commit + (string= "1" (magit-git-string "rev-list" "--count" (format "%s.." base))))) + (unless is-single-commit + (apply #'magit-log (list (format "%s..%s" base head)) (magit-log-arguments))) + (with-current-buffer + (let ((template (magithub-issue--template-text "PULL_REQUEST_TEMPLATE"))) + (magithub-edit-new (format "*magithub-pull-request: %s into %s:%s*" + head + (magithub-repo-name repo) + base) + :header (let-alist repo (format "PR %s/%s (%s->%s)" + .owner.login .name head base)) + :submit #'magithub-pull-request-submit + :file (expand-file-name "new-pull-request-draft" + (magithub-repo-data-dir repo)) + :template template + :content (when is-single-commit + ;; when we only want to merge one commit + ;; insert that commit message as the initial content + (concat + (with-temp-buffer + (magit-git-insert "show" "-q" head-no-user "--format=%B") + (let ((fill-column (point-max))) + (fill-region (point-min) (point-max)) + (buffer-string))) + template)))) + (font-lock-add-keywords nil `((,(rx bos (group (*? any)) eol) 1 + 'magithub-issue-title-edit t))) + (magithub-bug-reference-mode-on) + (setq magithub-issue--extra-data + `((base . ,base) (head . ,head) (repo . ,repo))) + (magit-display-buffer (current-buffer))))) + +(defun magithub-pull-request-submit () + (interactive) + (let ((pull-request `(,@(magithub-issue-post--parse-buffer) + (base . ,(alist-get 'base magithub-issue--extra-data)) + (head . ,(alist-get 'head magithub-issue--extra-data))))) + (when (s-blank-p (alist-get 'title pull-request)) + (user-error "Title is required")) + (magithub-confirm 'submit-pr) + (when (magithub-confirm-no-error 'pr-allow-maintainers-to-submit) + (push (cons 'maintainer_can_modify t) pull-request)) + (let ((pr (condition-case _ + (magithub-request + (ghubp-post-repos-owner-repo-pulls + (alist-get 'repo magithub-issue--extra-data) + pull-request)) + (ghub-422 + (user-error "This pull request already exists!"))))) + (magithub-edit-delete-draft) + (magithub-issue-view pr)))) + +(provide 'magithub-issue-post) +;;; magithub-issue-post.el ends here diff --git a/magithub-issue-tricks.el b/magithub-issue-tricks.el new file mode 100644 index 0000000..09d0364 --- /dev/null +++ b/magithub-issue-tricks.el @@ -0,0 +1,56 @@ +;;; magithub-issue-tricks.el --- -*- lexical-binding: t; -*- + +;; Copyright (C) 2017-2018 Sean Allred + +;; Author: Sean Allred <code@seanallred.com> + +;; 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 <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;;; Code: + +(require 'magit) +(require 'magithub-issue) + +(defcustom magithub-hub-executable "hub" + "The hub executable used by Magithub." + :group 'magithub + :package-version '(magithub . "0.1") + :type 'string) + +(defmacro magithub-with-hub (&rest body) + `(let ((magit-git-executable magithub-hub-executable) + (magit-pre-call-git-hook nil) + (magit-git-global-arguments nil)) + ,@body)) + +;;;###autoload +(defun magithub-pull-request-merge (pull-request &optional args) + "Merge PULL-REQUEST with ARGS. +See `magithub-pull-request--completing-read'. If point is on a +pull-request object, that object is selected by default." + (interactive (list (magithub-issue-completing-read-pull-requests) + (magit-am-arguments))) + (unless (executable-find magithub-hub-executable) + (user-error "This hasn't been supported in elisp yet; please install/configure `hub'")) + (unless (member pull-request (magithub-pull-requests)) + (user-error "Unknown pull request %S" pull-request)) + (let-alist pull-request + (magithub-with-hub + (magit-run-git-sequencer "am" args .html_url)) + (message "#%d has been applied" .number))) + +(provide 'magithub-issue-tricks) +;;; magithub-issue-tricks.el ends here diff --git a/magithub-issue-view.el b/magithub-issue-view.el new file mode 100644 index 0000000..117a42f --- /dev/null +++ b/magithub-issue-view.el @@ -0,0 +1,177 @@ +;;; magithub-issue-view.el --- view issues -*- lexical-binding: t; -*- + +;; Copyright (C) 2017-2018 Sean Allred + +;; Author: Sean Allred <code@seanallred.com> +;; Keywords: lisp + +;; 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 <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; View issues in magit-like buffers. + +;;; Code: + +(require 'magit-mode) + +(require 'magithub-core) +(require 'magithub-issue) +(require 'magithub-comment) + +(defvar magithub-issue-view-mode-map + (let ((m (make-composed-keymap (list magithub-map) magit-mode-map))) + (define-key m [remap magithub-reply-thing] #'magithub-comment-new) + (define-key m [remap magithub-browse-thing] #'magithub-issue-browse) + (define-key m [remap magit-refresh] #'magithub-issue-view-refresh) + m)) + +(define-derived-mode magithub-issue-view-mode magit-mode + "Issue View" "View GitHub issues with Magithub.") + +(defvar magithub-issue-view-headers-hook + '(magithub-issue-view-insert-title + magithub-issue-view-insert-author + magithub-issue-view-insert-state + magithub-issue-view-insert-asked + magithub-issue-view-insert-labels) + "Header information for each issue.") + +(defvar magithub-issue-view-sections-hook + '(magithub-issue-view-insert-headers + magithub-issue-view-insert-body + magithub-issue-view-insert-comments) + "Sections to be inserted for each issue.") + +(defun magithub-issue-view-refresh () + "Refresh the current issue." + (interactive) + (if (derived-mode-p 'magithub-issue-view-mode) + (progn + ;; todo: find a better means to separate the keymaps of issues + ;; in the status buffer vs issues in their own buffer + (when magithub-issue + (magithub-cache-without-cache :issues + (setq-local magithub-issue + (magithub-issue magithub-repo magithub-issue)) + (magithub-issue-comments magithub-issue))) + (let ((magit-refresh-args (list magithub-issue))) + (magit-refresh)) + (message "refreshed")) + (call-interactively #'magit-refresh))) + +(defun magithub-issue-view-refresh-buffer (issue &rest _) + (setq-local magithub-issue issue) + (setq-local magithub-repo (magithub-issue-repo issue)) + (magit-insert-section (magithub-issue issue) + (run-hooks 'magithub-issue-view-sections-hook))) + +(defun magithub-issue-view-insert-headers () + "Run `magithub-issue-view-headers-hook'." + (magit-insert-headers magithub-issue-view-headers-hook)) + +(defun magithub-issue-view--lock-value (issue &rest _args) + "Provide an identifying value for ISSUE. +See also `magit-buffer-lock-functions'." + (let-alist `((repo . ,(magithub-issue-repo issue)) + (issue . ,issue)) + (list .repo.owner.login .repo.name .issue.number))) +(push (cons 'magithub-issue-view-mode #'magithub-issue-view--lock-value) + magit-buffer-lock-functions) + +(defun magithub-issue-view--buffer-name (_mode issue-lock-value) + "Generate a buffer name for ISSUE-LOCK-VALUE. +See also `magithub-issue-view--lock-value'." + (let ((owner (nth 0 issue-lock-value)) + (repo (nth 1 issue-lock-value)) + (number (nth 2 issue-lock-value))) + (format "*magithub: %s/%s#%d: %s*" + owner + repo + number + (alist-get 'title (magithub-issue `((owner (login . ,owner)) + (name . ,repo)) + number))))) + +;;;###autoload +(defun magithub-issue-view (issue) + "View ISSUE in a new buffer. +Return the new buffer." + (interactive (list (magithub-interactive-issue))) + (let ((magit-generate-buffer-name-function #'magithub-issue-view--buffer-name)) + (magit-mode-setup-internal #'magithub-issue-view-mode (list issue) t) + (current-buffer))) + +(cl-defun magithub-issue-view-insert--generic (title text &optional type section-value &key face) + "Insert a generic header line with TITLE: VALUE" + (declare (indent 2)) + (setq type (or type 'magithub)) + (magit-insert-section ((eval type) section-value) + (insert (format "%-10s" title) + (or (and face (propertize text 'face face)) + text) + ?\n) + (magit-insert-heading))) + +(defun magithub-issue-view-insert-title () + "Insert issue title." + (let-alist magithub-issue + (magithub-issue-view-insert--generic "Title:" .title))) + +(defun magithub-issue-view-insert-author () + "Insert issue author." + (insert (format "%-10s" "Author:")) + (let-alist magithub-issue + (magit-insert-section (magithub-user .user) + (insert (propertize .user.login 'face 'magithub-user) ?\n) + (magit-insert-heading)))) + +(defun magithub-issue-view-insert-state () + "Insert issue state (either \"open\" or \"closed\")." + (let-alist magithub-issue + (magithub-issue-view-insert--generic "State:" .state + :face 'magit-dimmed))) + +(defun magithub-issue-view-insert-asked () + "Insert posted time." + (let-alist magithub-issue + (magithub-issue-view-insert--generic "Posted:" (magithub--format-time .created_at) + :face 'magit-dimmed))) + +(defun magithub-issue-view-insert-labels () + "Insert labels." + (insert (format "%-10s" "Labels:")) + (magithub-label-insert-list (alist-get 'labels magithub-issue)) + (insert ?\n)) + +(defun magithub-issue-view-insert-body () + "Insert issue body." + (let-alist magithub-issue + (magit-insert-section (magithub-issue-body magithub-issue) + (magit-insert-heading "Body") + (if (or (null .body) (string= .body "")) + (insert (propertize "There's nothing here!\n\n" 'face 'magit-dimmed)) + (insert (magithub-fill-gfm (magithub-wash-gfm (s-trim .body))) "\n\n"))))) + +(defun magithub-issue-view-insert-comments () + "Insert issue comments." + (let ((comments (magithub-issue-comments magithub-issue))) + (magit-insert-section (magithub-issue-comments comments) + (magit-insert-heading "Comments:") + (if (null comments) + (insert (propertize "There's nothing here!\n\n" 'face 'magit-dimmed)) + (mapc #'magithub-comment-insert comments))))) + +(provide 'magithub-issue-view) +;;; magithub-issue-view.el ends here diff --git a/magithub-issue.el b/magithub-issue.el new file mode 100644 index 0000000..cf284d4 --- /dev/null +++ b/magithub-issue.el @@ -0,0 +1,602 @@ +;;; magithub-issue.el --- Browse issues with Magithub -*- lexical-binding: t; -*- + +;; Copyright (C) 2016-2018 Sean Allred + +;; Author: Sean Allred <code@seanallred.com> +;; Keywords: tools + +;; 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 <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Jump to issues from `magit-status'! + +;;; Code: + +(require 's) +(require 'dash) +(require 'ghub+) +(require 'cl-lib) +(require 'magit) +(require 'thingatpt) + +(require 'magithub-core) +(require 'magithub-user) +(require 'magithub-label) + +(declare-function magithub-issue-view "magithub-issue-view.el" (issue)) + +(defvar magit-magithub-repo-issues-section-map + (let ((m (make-sparse-keymap))) + (define-key m [remap magit-visit-thing] #'magithub-repo-visit-issues) + m)) + +(defvar magit-magithub-note-section-map + (let ((m (make-sparse-keymap))) + (set-keymap-parent m magithub-map) + (define-key m [remap magit-visit-thing] #'magithub-issue-personal-note) + m)) + +;;; Core + +(defmacro magithub-interactive-issue-or-pr (sym args doc &rest body) + "Declare an interactive form that works on both issues and PRs. +SYM is a postfix for the function symbol. An appropriate prefix +will be added for both the issue-version and PR-version. + +ARGS should be a list of one element, the symbol ISSUE-OR-PR. + +DOC is a doc-string. + +BODY is the function implementation." + (declare (indent defun) + (doc-string 3)) + (unless (eq (car args) 'issue-or-pr) + (error "For clarity, the first argument must be ISSUE-OR-PR")) + (let* ((snam (symbol-name sym)) + (isym (intern (concat "magithub-issue-" snam))) + (psym (intern (concat "magithub-pull-request-" snam)))) + `(list + (defun ,isym ,(cons 'issue (cdr args)) + ,(format (concat doc "\n\nSee also `%S'.") "ISSUE" psym) + (interactive (list (magithub-interactive-issue))) + (let ((issue-or-pr issue)) + ,@body)) + (defun ,psym ,(cons 'pull-request (cdr args)) + ,(format (concat doc "\n\nSee also `%S'.") "PULL-REQUEST" isym) + (interactive (list (magithub-interactive-pull-request))) + (let ((issue-or-pr pull-request)) + ,@body))))) + +(defun magithub--issue-list (&rest params) + "Return a list of issues for the current repository. +The response is unpaginated, so avoid doing this with PARAMS that +will return a ton of issues. + +See also `ghubp-get-repos-owner-repo-issues'." + (cl-assert (cl-evenp (length params))) + (magithub-cache :issues + `(magithub-request + (ghubp-unpaginate + (ghubp-get-repos-owner-repo-issues + ',(magithub-repo) + ,@params))) + :message + "Retrieving issue list...")) + +(defun magithub-issue--issue-is-pull-p (issue) + (not (null (alist-get 'pull_request issue)))) + +(defun magithub-issue--issue-is-issue-p (issue) + (and (alist-get 'number issue) + (not (magithub-issue--issue-is-pull-p issue)))) + +(defun magithub-issue-comments (issue) + "Get comments on ISSUE." + (let ((repo (magithub-issue-repo issue))) + (magithub-cache :issues + `(magithub-request + (ghubp-unpaginate + (ghubp-get-repos-owner-repo-issues-number-comments ',repo ',issue)))))) + +;;; Finding issues and pull requests + +(defun magithub-issues () + "Return a list of issue objects that are actually issues." + (-filter #'magithub-issue--issue-is-issue-p + (magithub--issue-list))) + +(defun magithub-pull-requests () + "Return a list of issue objects that are actually pull requests." + (-filter #'magithub-issue--issue-is-pull-p + (magithub--issue-list))) + +;;; Sorting + +(defcustom magithub-issue-sort-function + #'magithub-issue-sort-ascending + "Function used for sorting issues and pull requests in the +status buffer. Should take two issue-objects as arguments." + :type 'function + :group 'magithub) + +(magithub-defsort magithub-issue-sort-ascending #'< + "Lower issue numbers come first." + (apply-partially #'alist-get :number)) + +(magithub-defsort magithub-issue-sort-descending #'> + "Higher issue numbers come first." + (apply-partially #'alist-get :number)) + +(defun magithub-issue--sort (issues) + "Sort ISSUES by `magithub-issue-sort-function'." + (sort issues magithub-issue-sort-function)) + +;;; Getting issues from the user + +(defun magithub-issue--format-for-read (issue) + "Format ISSUE as a string suitable for completion." + (let-alist issue (format "%3d %s" .number .title))) + +(defun magithub-issue--completing-read (prompt default preds) + "Complete over all open pull requests returning its issue object. +If point is on a pull-request object, that object is selected by +default." + (magithub--completing-read prompt (magithub--issue-list) + #'magithub-issue--format-for-read + (apply-partially #'magithub--satisfies-p preds) + t default)) +(defun magithub-issue-completing-read-issues (&optional default) + "Read an issue in the minibuffer with completion." + (interactive (list (thing-at-point 'github-issue))) + (magithub-issue--completing-read + "Issue: " default (list #'magithub-issue--issue-is-issue-p))) +(defun magithub-issue-completing-read-pull-requests (&optional default) + "Read a pull request in the minibuffer with completion." + (interactive (list (thing-at-point 'github-pull-request))) + (magithub-issue--completing-read + "Pull Request: " default (list #'magithub-issue--issue-is-pull-p))) +(defun magithub-interactive-issue () + (or (thing-at-point 'github-issue) + (magithub-issue-completing-read-issues))) +(defun magithub-interactive-pull-request () + (or (thing-at-point 'github-pull-request) + (magithub-issue-completing-read-pull-requests))) + +(defun magithub-issue-find (number) + "Return the issue or pull request with the given NUMBER." + (-find (lambda (i) (= (alist-get 'number i) number)) + (magithub--issue-list :filter "all" :state "all"))) + +(defun magithub-issue (repo number-or-issue) + "Retrieve in REPO issue NUMBER-OR-ISSUE. +NUMBER-OR-ISSUE is either a number or an issue object. If it's a +number, the issue by that number is retrieved. If it's an issue +object, the same issue is retrieved." + (let ((num (or (and (numberp number-or-issue) + number-or-issue) + (alist-get 'number number-or-issue)))) + (magithub-cache :issues + `(magithub-request + (ghubp-get-repos-owner-repo-issues-number + ',repo '((number . ,num)))) + :message + (format "Getting issue %s#%d..." (magithub-repo-name repo) num)))) + +(defun magithub-issue-personal-note-file (issue-or-pr) + "Return an absolute filename appropriate for ISSUE-OR-PR." + (let-alist `((repo . ,(magithub-repo (magithub-issue-repo issue-or-pr))) + (issue . ,issue-or-pr)) + (expand-file-name + (format "%s/%s/notes/%d.org" .repo.owner.login .repo.name .issue.number) + magithub-dir))) + +(magithub-interactive-issue-or-pr personal-note (issue-or-pr) + "Write a personal note about %s. +This is stored in `magit-git-dir' and is unrelated to +`git-notes'." + (if (null issue-or-pr) + (error "No issue or pull request here") + (let-alist issue-or-pr + (let ((note-file (magithub-issue-personal-note-file issue-or-pr))) + (make-directory (file-name-directory note-file) t) + (with-current-buffer (find-file-other-window note-file) + (rename-buffer (format "*magithub note for #%d*" .number))))))) + +(defun magithub-issue-has-personal-note-p (issue-or-pr) + "Non-nil if a personal note exists for ISSUE-OR-PR." + (let ((filename (magithub-issue-personal-note-file issue-or-pr))) + (and (file-exists-p filename) + (not (string-equal + "" + (string-trim + (with-temp-buffer + (insert-file-contents-literally filename) + (buffer-string)))))))) + +(defun magithub-issue-repo (issue) + "Get a repository object from ISSUE." + (let-alist issue + (or .repository + .base.repo + (save-match-data + (when (string-match (concat (rx bos) + "https://" + (regexp-quote (ghubp-host)) + (rx "/repos/" + (group (+ (not (any "/")))) "/" + (group (+ (not (any "/")))) "/issues/") + (regexp-quote (number-to-string .number)) + (rx eos)) + .url) + (magithub-repo + `((owner (login . ,(match-string 1 .url))) + (name . ,(match-string 2 .url))))))))) + +(defun magithub-issue-reference (issue) + "Return a string like \"owner/repo#number\" for ISSUE." + (let-alist `((repo . ,(magithub-issue-repo issue)) + (issue . ,issue)) + (format "%s/%s#%d" .repo.owner.login .repo.name .issue.number))) + +(defun magithub-issue-from-reference (string) + "Parse an issue from an \"owner/repo#number\" STRING." + (when (string-match (rx bos (group (+ any)) + "/" (group (+ any)) + "#" (group (+ digit)) + eos) + string) + (magithub-issue `((owner (login . ,(match-string 1 string))) + (name . ,(match-string 2 string))) + (string-to-number (match-string 3 string))))) + +(defun magithub-issue-insert-sections (issues) + "Insert ISSUES into the buffer with alignment. +See also `magithub-issue-insert-section'." + (let ((max-num-len (thread-last issues + (ghubp-get-in-all '(number)) + (apply #'max) + (number-to-string) + (length)))) + (--map (magithub-issue-insert-section it max-num-len) + issues))) + +(defun magithub-issue-insert-section (issue &optional pad-num-to-len) + "Insert ISSUE into the buffer. +If PAD-NUM-TO-LEN is non-nil, it is an integer width. For +example, if this section's issue number is \"3\" and the next +section's number is \"401\", pass a padding of 3 to both to align +them. + +See also `magithub-issue-insert-sections'." + (when issue + (setq pad-num-to-len (or pad-num-to-len 0)) + (magit-insert-section (magithub-issue issue t) + (let-alist issue + (magit-insert-heading + (format (format "%%%ds %%s" (1+ pad-num-to-len)) ;1+ accounts for # + (propertize (format "#%d" .number) + 'face 'magithub-issue-number) + (propertize .title + 'face (if (magithub-issue-has-personal-note-p issue) + 'magithub-issue-title-with-note + 'magithub-issue-title)))) + (run-hook-with-args 'magithub-issue-details-hook issue + (format " %s %%-12s" + (make-string pad-num-to-len ?\ ))))))) + +(defvar magithub-issue-details-hook + '(magithub-issue-detail-insert-personal-notes + magithub-issue-detail-insert-created + magithub-issue-detail-insert-updated + magithub-issue-detail-insert-author + magithub-issue-detail-insert-assignees + magithub-issue-detail-insert-labels + magithub-issue-detail-insert-body-preview) + "Detail functions for issue-type sections. +These details appear under issues as expandable content. + +Each function takes two arguments: + + 1. an issue object + 2. a format string for a string label (for alignment)") + +(defun magithub-issue-detail-insert-author (issue fmt) + "Insert the author of ISSUE using FMT." + (let-alist issue + (insert (format fmt "Author:")) + (magit-insert-section (magithub-user (magithub-user .user)) + (insert + (propertize .user.login 'face 'magithub-user))) + (insert "\n"))) + +(defun magithub-issue-detail-insert-created (issue fmt) + "Insert when ISSUE was created using FMT." + (let-alist issue + (insert (format fmt "Created:") + (propertize (magithub--format-time .created_at) + 'face 'magit-dimmed) + "\n"))) + +(defun magithub-issue-detail-insert-updated (issue fmt) + "Insert when ISSUE was created using FMT." + (let-alist issue + (insert (format fmt "Updated:") + (propertize (magithub--format-time .updated_at) + 'face 'magit-dimmed) + "\n"))) + +(defun magithub-issue-detail-insert-assignees (issue fmt) + "Insert the assignees of ISSUE using FMT." + (let-alist issue + (insert (format fmt "Assignees:")) + (if .assignees + (let ((assignees .assignees) assignee) + (while (setq assignee (pop assignees)) + (magit-insert-section (magithub-assignee (magithub-user assignee)) + (insert (propertize (alist-get 'login assignee) + 'face 'magithub-user))) + (when assignees + (insert " ")))) + (magit-insert-section (magithub-assignee) + (insert (propertize "none" 'face 'magit-dimmed)))) + (insert "\n"))) + +(defun magithub-issue-detail-insert-personal-notes (issue fmt) + "Insert a link to ISSUE's notes." + (insert (format fmt "My notes:")) + (magit-insert-section (magithub-note) + (insert (if (magithub-issue-has-personal-note-p issue) + (propertize "visit your note" 'face 'link) + (propertize "create a new note" 'face 'magit-dimmed)))) + (insert "\n")) + +(defun magithub-issue-detail-insert-body-preview (issue fmt) + "Insert a preview of ISSUE's body using FMT." + (let-alist issue + (let (label-string label-len width did-cut maxchar text) + (setq label-string (format fmt "Preview:")) + (insert label-string) + + (if (or (null .body) (string= .body "")) + (insert (concat (propertize "none" 'face 'magit-dimmed) + "\n")) + + (setq label-len (length label-string)) + (setq width (- fill-column label-len)) + (setq maxchar (* 3 width)) + (setq did-cut (< maxchar (length .body))) + (setq maxchar (if did-cut (- maxchar 3) maxchar)) + (setq text (if did-cut + (substring .body 0 (min (length .body) (* 4 width))) + .body)) + (setq text (replace-regexp-in-string "
" "" text)) + (setq text (let ((fill-column width)) + (thread-last text + (magithub-fill-gfm) + (magithub-indent-text label-len) + (s-trim)))) + (insert text) + (when did-cut + (insert (propertize "..." 'face 'magit-dimmed))) + (insert "\n"))))) + +(defun magithub-issue-detail-insert-labels (issue fmt) + "Insert ISSUE's labels using FMT." + (let-alist issue + (insert (format fmt "Labels:")) + (magithub-label-insert-list .labels) + (insert "\n"))) + +;;; Magithub-Status stuff + +(defun magithub-issue-refresh () + "Refresh issues for this repository." + (interactive) + (magithub-cache-without-cache :issues + (magithub--issue-list)) + (when (derived-mode-p 'magit-status-mode) + (magit-refresh))) + +(defvar magit-magithub-issue-section-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map magithub-map) + (define-key map [remap magit-visit-thing] #'magithub-issue-visit) + (define-key map [remap magithub-browse-thing] #'magithub-issue-browse) + (define-key map [remap magithub-reply-thing] #'magithub-comment-new) + (define-key map "L" #'magithub-issue-add-labels) + (define-key map "N" #'magithub-issue-personal-note) + map) + "Keymap for `magithub-issue' sections.") + +(defvar magit-magithub-issues-list-section-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map magithub-map) + (define-key map [remap magit-visit-thing] #'magithub-issue-visit) + (define-key map [remap magithub-browse-thing] #'magithub-issue-browse) + (define-key map [remap magit-refresh] #'magithub-issue-refresh) + map) + "Keymap for `magithub-issues-list' sections.") + +(defvar magit-magithub-pull-request-section-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map magit-magithub-issues-list-section-map) + (define-key map [remap magithub-issue-visit] #'magithub-pull-visit) + (define-key map [remap magithub-issue-browse] #'magithub-pull-browse) + map) + "Keymap for `magithub-pull-request' sections.") + +(defvar magit-magithub-pull-requests-list-section-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map magithub-map) + (define-key map [remap magit-visit-thing] #'magithub-pull-visit) + (define-key map [remap magithub-browse-thing] #'magithub-pull-browse) + (define-key map [remap magit-refresh] #'magithub-issue-refresh) + map) + "Keymap for `magithub-pull-request-list' sections.") + +;; By maintaining these as lists of functions, we're setting +;; ourselves up to be able to dynamically apply new filters from the +;; status buffer (e.g., 'bugs' or 'questions' assigned to me) +(defcustom magithub-issue-issue-filter-functions nil + "List of functions that filter issues. +Each function will be supplied a single issue object. If any +function returns nil, the issue will not be listed in the status +buffer." + :type '(repeat function) + :group 'magithub) + +(defcustom magithub-issue-pull-request-filter-functions nil + "List of functions that filter pull-requests. +Each function will be supplied a single issue object. If any +function returns nil, the issue will not be listed in the status +buffer." + :type '(repeat function) + :group 'magithub) + +(defun magithub-issue-add-labels (issue labels) + "Update ISSUE's labels to LABELS." + (interactive + (when (magithub-verify-manage-labels t) + (let* ((fmt (lambda (l) (alist-get 'name l))) + (issue (or (thing-at-point 'github-issue) + (thing-at-point 'github-pull-request))) + (current-labels (alist-get 'labels issue)) + (to-remove (magithub--completing-read-multiple + "Remove labels: " current-labels fmt))) + (setq current-labels (cl-set-difference current-labels to-remove)) + (list issue (magithub--completing-read-multiple + "Add labels: " (magithub-label-list) fmt + nil nil current-labels))))) + (when (magithub-request + (ghubp-patch-repos-owner-repo-issues-number + (magithub-repo) issue `((labels . ,labels)))) + (setcdr (assq 'labels issue) labels)) + (when (derived-mode-p 'magit-status-mode) + (magit-refresh))) + +;;;###autoload +(defun magithub-issue--insert-issue-section () + "Insert GitHub issues if appropriate." + (when (and (magithub-settings-include-issues-p) + (magithub-usable-p) + (alist-get 'has_issues (magithub-repo))) + (magithub-issue--insert-generic-section + (magithub-issues-list) + "Issues" + (magithub-issues) + magithub-issue-issue-filter-functions))) + +;;;###autoload +(defun magithub-issue--insert-pr-section () + "Insert GitHub pull requests if appropriate." + (when (and (magithub-settings-include-pull-requests-p) + (magithub-usable-p)) + (magithub-feature-maybe-idle-notify + 'pull-request-merge) + (magithub-issue--insert-generic-section + (magithub-pull-requests-list) + "Pull Requests" + (magithub-pull-requests) + magithub-issue-pull-request-filter-functions))) + +(defmacro magithub-issue--insert-generic-section + (spec title list filters) + (let ((sym-filtered (cl-gensym))) + `(when-let ((,sym-filtered (magithub-filter-all ,filters ,list))) + (magit-insert-section ,spec + (insert (format "%s%s:" + (propertize ,title 'face 'magit-section-heading) + (if ,filters + (propertize " (filtered)" 'face 'magit-dimmed) + ""))) + (magit-insert-heading) + (magithub-issue-insert-sections ,sym-filtered) + (insert ?\n))))) + +(defun magithub-issue-browse (issue) + "Visits ISSUE in the browser. +Interactively, this finds the issue at point." + (interactive (list (magithub-interactive-issue))) + (magithub-issue--browse issue)) + +(defun magithub-issue-visit (issue) + "Visits ISSUE in Emacs. +Interactively, this finds the issue at point." + (interactive (list (magithub-interactive-issue))) + (magithub-issue-view issue)) + +(defun magithub-pull-browse (pr) + "Visits PR in the browser. +Interactively, this finds the pull request at point." + (interactive (list (magithub-interactive-pull-request))) + (magithub-issue--browse pr)) + +(defun magithub-pull-visit (pr) + "Visits PR in Emacs. +Interactively, this finds the pull request at point." + (interactive (list (magithub-interactive-pull-request))) + (magithub-issue-view pr)) + +(defun magithub-issue--browse (issue-or-pr) + "Visits ISSUE-OR-PR in the browser. +Interactively, this finds the issue at point." + (when-let ((url (alist-get 'html_url issue-or-pr))) + (browse-url url))) + +(defun magithub-repolist-column-issue (_id) + "Insert the number of open issues in this repository." + (when (magithub-usable-p) + (number-to-string (length (magithub-issues))))) + +(defun magithub-repolist-column-pull-request (_id) + "Insert the number of open pull requests in this repository." + (when (magithub-usable-p) + (number-to-string (length (magithub-pull-requests))))) + +;;; Pull Request handling + + +(defun magithub-pull-request (repo number) + "Retrieve a pull request in REPO by NUMBER." + (magithub-cache :issues + `(magithub-request + (ghubp-get-repos-owner-repo-pulls-number + ',repo '((number . ,number)))) + :message + (format "Getting pull request %s#%d..." + (magithub-repo-name repo) + number))) + +(defun magithub-remote-fork-p (remote) + "True if REMOTE is a fork." + (thread-last remote + (magithub-repo-from-remote) + (alist-get 'fork))) + +(defun magithub-pull-request-checked-out (pull-request) + "True if PULL-REQUEST is currently checked out." + (let-alist pull-request + (let ((remote .user.login) + (branch .head.ref)) + (and (magit-remote-p remote) + (magithub-remote-fork-p remote) + (magit-branch-p branch) + (string= remote (magit-get-push-remote branch)))))) + +(make-obsolete 'magithub-pull-request-checkout 'magit-checkout-pull-request "0.1.6") +(defalias 'magithub-pull-request-checkout #'magit-checkout-pull-request) + +(provide 'magithub-issue) +;;; magithub-issue.el ends here diff --git a/magithub-label.el b/magithub-label.el new file mode 100644 index 0000000..c4d5e66 --- /dev/null +++ b/magithub-label.el @@ -0,0 +1,176 @@ +;;; magithub-labels.el --- -*- lexical-binding: t; -*- + +;; Copyright (C) 2017-2018 Sean Allred + +;; Author: Sean Allred <code@seanallred.com> + +;; 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 <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;;; Code: + +(require 'thingatpt) +(require 'ghub+) + +(require 'magithub-core) + +(defvar magit-magithub-label-section-map + (let ((m (make-sparse-keymap))) + (set-keymap-parent m magithub-map) + (define-key m [remap magit-visit-thing] #'magithub-label-visit) + (define-key m [remap magit-delete-thing] #'magithub-label-remove) + (define-key m [remap magit-section-toggle] (lambda () (interactive))) + (define-key m [remap magithub-browse-thing] #'magithub-label-browse) + (define-key m [remap magithub-add-thing] #'magithub-label-add) + m) + "Keymap for label sections.") + +(defun magithub-label-list () + "Return a list of issue and pull-request labels." + (magithub-cache :label + `(magithub-request + (ghubp-unpaginate + (ghubp-get-repos-owner-repo-labels + ',(magithub-repo)))) + :message + "Loading labels...")) + +(defun magithub-label-read-labels (prompt &optional default) + "Read some issue labels and return a list of strings. +Available issues are provided by `magithub-label-list'. + +DEFAULT is a list of pre-selected labels. These labels are not +prompted for again." + (let ((remaining-labels + (cl-set-difference (magithub-label-list) default + :test (lambda (a b) + (= (alist-get 'name a) + (alist-get 'name b)))))) + (magithub--completing-read-multiple + prompt remaining-labels + (lambda (l) (alist-get 'name l))))) + +(defalias 'magithub-label-visit #'magithub-label-browse) +(defun magithub-label-browse (label) + "Visit LABEL with `browse-url'. +In the future, this will likely be replaced with a search on +issues and pull requests with the label LABEL." + (interactive (list (thing-at-point 'github-label))) + (unless label + (user-error "No label found at point to browse")) + (unless (string= (ghubp-host) ghub-default-host) + (user-error "Label browsing not yet supported on GitHub Enterprise; pull requests welcome!")) + (let-alist (magithub-repo) + (browse-url (format "%s/%s/%s/labels/%s" + (ghubp-base-html-url) + .owner.login .name (alist-get 'name label))))) + +(defcustom magithub-label-color-replacement-alist nil + "Make certain label colors easier to see. +In your theme, you may find that certain colors are very +difficult to see. Customize this list to map GitHub's label +colors to their Emacs replacements." + :group 'magithub + :type '(alist :key-type color :value-type color)) + +(defun magithub-label--get-display-color (label) + "Gets the display color for LABEL. +Respects `magithub-label-color-replacement-alist'." + (let ((original (concat "#" (alist-get 'color label)))) + (if-let ((color (assoc-string original magithub-label-color-replacement-alist t))) + (cdr color) + original))) + +(defun magithub-label-propertize (label) + "Propertize LABEL according to its color. +The face used is dynamically calculated, but it always inherits +from `magithub-label'. Customize that to affect all labels." + (propertize (alist-get 'name label) + 'face (list :foreground (magithub-label--get-display-color label) + :inherit 'magithub-label))) + +(defun magithub-label-color-replace (label new-color) + "For LABEL, define a NEW-COLOR to use in the buffer." + (interactive + (list (thing-at-point 'github-label) + (magithub-core-color-completing-read "Replace label color: "))) + (let ((label-color (concat "#" (alist-get 'color label)))) + (if-let ((cell (assoc-string label-color magithub-label-color-replacement-alist))) + (setcdr cell new-color) + (push (cons label-color new-color) + magithub-label-color-replacement-alist))) + (when (magithub-confirm-no-error 'label-save-customized-colors) + (customize-save-variable 'magithub-label-color-replacement-alist + magithub-label-color-replacement-alist + "Auto-saved by `magithub-label-color-replace'")) + (when (derived-mode-p 'magit-status-mode) + (magit-refresh))) + +(defun magithub-label--verify-manage () + (or (magithub-repo-push-p) + (user-error "You don't have permission to manage labels in this repository"))) + +(defun magithub-label-remove (issue label) + "From ISSUE, remove LABEL." + (interactive (and (magithub-label--verify-manage) + (list (thing-at-point 'github-issue) + (thing-at-point 'github-label)))) + (unless issue + (user-error "No issue here")) + (unless label + (user-error "No label here")) + (let-alist label + (magithub-confirm 'remove-label .name) + (prog1 (magithub-request + (ghubp-delete-repos-owner-repo-issues-number-labels-name + (magithub-issue-repo issue) issue label)) + (magithub-cache-without-cache :issues + (magit-refresh-buffer))))) + +(defun magithub-label-add (issue labels) + "To ISSUE, add LABELS." + (interactive (list (thing-at-point 'github-issue) + (magithub-label-read-labels "Add labels: "))) + (if (not (and issue labels)) + (user-error "No issue/labels") + (magithub-confirm 'add-label + (s-join "," (ghubp-get-in-all '(name) labels)) + (magithub-repo-name (magithub-issue-repo issue)) + (alist-get 'number issue)) + (prog1 (magithub-request + (ghubp-post-repos-owner-repo-issues-number-labels + (magithub-issue-repo issue) issue labels)) + (magithub-cache-without-cache :issues + (magit-refresh))))) + +(defun magithub-label-insert (label) + "Insert LABEL into the buffer. +If you need to insert many labels, use +`magithub-label-insert-list'." + (magit-insert-section (magithub-label label) + (insert (magithub-label-propertize label)))) + +(defun magithub-label-insert-list (label-list) + "Insert LABEL-LIST intro the buffer." + (if (null label-list) + (magit-insert-section (magithub-label) + (insert (propertize "none" 'face 'magit-dimmed))) + (while label-list + (magithub-label-insert (pop label-list)) + (when label-list + (insert " "))))) + +(provide 'magithub-label) +;;; magithub-labels.el ends here diff --git a/magithub-notification.el b/magithub-notification.el new file mode 100644 index 0000000..ad32097 --- /dev/null +++ b/magithub-notification.el @@ -0,0 +1,170 @@ +;;; magithub-notification.el --- notification handling -*- lexical-binding: t; -*- + +;; Copyright (C) 2017-2018 Sean Allred + +;; Author: Sean Allred <code@seanallred.com> +;; Keywords: lisp + +;; 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 <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; View and interact with notifications. + +;;; Code: + +(require 'thingatpt) + +(require 'magithub-issue-view) + +(defvar magit-magithub-notification-section-map + (let ((m (make-sparse-keymap))) + (set-keymap-parent m magithub-map) + (define-key m [remap magit-visit-thing] #'magithub-notification-visit) + (define-key m [remap magithub-browse-thing] #'magithub-notification-browse) + (define-key m [remap magit-refresh] #'magithub-notification-refresh) + m)) + +(defvar magit-magithub-notifications-section-map + (let ((m (make-sparse-keymap))) + (set-keymap-parent m magithub-map) + (define-key m [remap magit-refresh] #'magithub-notification-refresh) + m)) + +(defun magithub-notifications (&optional include-read only-participating since before) + "Get notifications for the currently-authenticated user. +If INCLUDE-READ is non-nil, read notifications are returned as +well. + +If ONLY-PARTICIPATING is non-nil, only return notifications that +the user is directly participating in. + +If SINCE/BEFORE are non-nil, they are time values. Only +notifications received since/before this value will be returned. +See also Info node `(elisp)Time of Day'." + (let (args) + (when include-read + (push '(:all "true") args)) + (when only-participating + (push '(:participating "true") args)) + (when since + (push `(:since ,(format-time-string "%FT%T%z" since)) args)) + (when before + (push `(:before ,(format-time-string "%FT%T%z" before)) args)) + (magithub-cache :notification + `(magithub-request + (ghubp-unpaginate + (ghubp-get-notifications ,@(apply #'append args))))))) + +(defun magithub-notification-refresh () + (interactive) + (magithub-cache-without-cache :notification + (magit-refresh)) + (message "(magithub) notifications refreshed")) + +(defun magithub-notification-read-p (notification) + "Non-nil if NOTIFICATION has been read." + (not (magithub-notification-unread-p notification))) + +(defun magithub-notification-unread-p (notification) + "Non-nil if NOTIFICATION has been not been read." + (alist-get 'unread notification)) + +(defconst magithub-notification-reasons + '(("assign" . "You were assigned to the Issue.") + ("author" . "You created the thread.") + ("comment" . "You commented on the thread.") + ("invitation" . "You accepted an invitation to contribute to the repository.") + ("manual" . "You subscribed to the thread (via an Issue or Pull Request).") + ("mention" . "You were specifically @mentioned in the content.") + ("state_change" . "You changed the thread state (for example, closing an Issue or merging a Pull Request).") + ("subscribed" . "You're watching the repository.") + ("team_mention" . "You were on a team that was mentioned.")) + "Human-readable description of possible notification reasons. +Stripped from the GitHub API Docs: + + URL `https://developer.github.com/v3/activity/notifications/#notification-reasons'.") + +(defun magithub-notification-reason (notification &optional expanded) + "Get the reason NOTIFICATION exists. +If EXPANDED is non-nil, use `magithub-notification-reasons' to +get a more verbose explanation." + (let-alist notification + (if expanded + (cdr (assoc-string .reason magithub-notification-reasons + "(Unknown)")) + .reason))) + +(defalias 'magithub-notification-visit #'magithub-notification-browse) +(defun magithub-notification-browse (notification) + "Visits the URL pointed to by NOTIFICATION." + (interactive (list (thing-at-point 'github-notification))) + (magithub-request + (if notification + (let-alist notification + (cond + ((member .subject.type '("Issue" "PullRequest")) + (ghubp-patch-notifications-threads-id notification) + (magithub-issue-view (ghubp-follow-get .subject.url))) + (t (if-let ((url (or .subject.latest_comment_url .subject.url)) + (html-url (alist-get 'html_url (ghubp-follow-get url)))) + (browse-url html-url) + (user-error "No target URL found"))))) + (user-error "No notification here")))) + +(defvar magithub-notification-details-hook + '(magithub-notification-detail-insert-type + magithub-notification-detail-insert-updated + magithub-notification-detail-insert-expanded-reason) + "Detail functions for notification-type sections. +These details appear under notifications as expandable content. + +Each function takes the notification object as its only +argument.") + +(defun magithub-notification-insert-section (notification) + "Insert NOTIFICATION as a section into the buffer." + (let-alist notification + (magit-insert-section (magithub-notification notification (not .unread)) + (magit-insert-heading + (format "%-12s %s" + (propertize (magithub-notification-reason notification) + 'face 'magithub-notification-reason + 'help-echo (magithub-notification-reason notification t)) + (propertize (concat .subject.title "\n") + 'face (if .unread 'highlight)))) + (run-hook-with-args 'magithub-notification-details-hook notification)))) + +(defun magithub-notification-detail-insert-type (notification) + "Insert NOTIFICATION's type." + (let-alist notification + (insert (format "%-12s %s\n" "Type:" + (propertize .subject.type 'face 'magit-dimmed))))) + +(defun magithub-notification-detail-insert-updated (notification) + "Insert a timestamp of when NOTIFICATION was last updated." + (let-alist notification + (insert (format "%-12s %s\n" "Updated:" + (propertize .updated_at 'face 'magit-dimmed))))) + +(defun magithub-notification-detail-insert-expanded-reason (notification) + "Insert NOTIFICATION's expanded reason. +See also `magithub-notification-reasons'." + (insert (format "%-12s %s\n" "Reason:" + (propertize (or (magithub-notification-reason notification t) + "(no description available)") + 'face 'magit-dimmed)))) + +(provide 'magithub-notification) +;;; magithub-notification.el ends here diff --git a/magithub-orgs.el b/magithub-orgs.el new file mode 100644 index 0000000..395ab5c --- /dev/null +++ b/magithub-orgs.el @@ -0,0 +1,36 @@ +;;; magithub-orgs.el --- Organization handling -*- lexical-binding: t; -*- + +;; Copyright (C) 2016-2018 Sean Allred + +;; Author: Sean Allred <code@seanallred.com> +;; Keywords: tools + +;; 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 <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Utilities for dealing with organizations. + +;;; Code: + +(require 'magithub-core) + +(defun magithub-orgs-list () + "List organizations for the currently authenticated user." + (magithub-cache :user-demographics + `(magithub-request + (ghubp-get-user-orgs)))) + +(provide 'magithub-orgs) +;;; magithub-orgs.el ends here diff --git a/magithub-repo.el b/magithub-repo.el new file mode 100644 index 0000000..9432835 --- /dev/null +++ b/magithub-repo.el @@ -0,0 +1,51 @@ +;;; magithub-repo.el --- repo tools -*- lexical-binding: t; -*- + +;; Copyright (C) 2017-2018 Sean Allred + +;; Author: Sean Allred <code@seanallred.com> +;; Keywords: lisp + +;; 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 <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Basic tools for working with repositories. + +;;; Code: + +(require 'magit) +(require 'thingatpt) + +(require 'magithub-core) + +(defvar magit-magithub-repo-section-map + (let ((m (make-sparse-keymap))) + (set-keymap-parent m magithub-map) + (define-key m [remap magithub-browse-thing] #'magithub-repo-browse) + m)) + +(defun magithub-repo-browse (repo) + (interactive (list (thing-at-point 'github-repo))) + (unless repo + (user-error "No repository found at point")) + (let-alist repo + (browse-url .html_url))) + +(defun magithub-repo-data-dir (repo) + (let-alist repo + (expand-file-name (format "%s/%s/" .owner.login .name) + magithub-dir))) + +(provide 'magithub-repo) +;;; magithub-repo.el ends here diff --git a/magithub-settings.el b/magithub-settings.el new file mode 100644 index 0000000..0c2ed12 --- /dev/null +++ b/magithub-settings.el @@ -0,0 +1,380 @@ +;;; magithub-settings.el --- repo-specific user settings -*- lexical-binding: t; -*- + +;; Copyright (C) 2018 Sean Allred + +;; Author: Sean Allred <code@seanallred.com> +;; Keywords: tools + +;; 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 <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;;; Code: + +(require 'magit) + +(defconst magithub-settings-section "magithub" + "This string prefixes all Magithub-related git settings.") +(defconst magithub-settings-prefix "magithub" + "This string prefixes all Magithub-related git settings.") + +(defmacro magithub-settings--simple (popup key variable docstring choices default) + (declare (indent 3) (doc-string 4)) + (unless (stringp variable) + (error "VARIABLE must be a string: %S" variable)) + (let* ((variable (concat magithub-settings-section "." variable)) + (Nset (concat "magithub-settings--set-" variable)) + (Nfmt (concat "magithub-settings--format-" variable))) + (let ((Sset (intern Nset)) + (Sfmt (intern Nfmt)) + (docstring (format "%s\n\nThis is the Git variable %S." docstring variable))) + `(progn + (defun ,Sset () ,docstring (interactive) + (magit--set-popup-variable ,variable ,choices ,default)) + (defun ,(intern Nfmt) () ,(format "See `%s'." Nset) + (magit--format-popup-variable:choices ,variable ,choices ,default)) + (magit-define-popup-variable ',popup ,key ,variable ',Sset ',Sfmt) + ,variable)))) + +(defun magithub-settings--value-or (variable default &optional accessor) + (declare (indent 2)) + (if (magit-get variable) + (funcall (or accessor #'magit-get) variable) + default)) + +;;;###autoload (autoload 'magithub-settings-popup "magithub-settings" nil t) +(magit-define-popup magithub-settings-popup + "Popup console for managing Magithub settings." + 'magithub-commands) + +(magithub-settings--simple magithub-settings-popup ?e "enabled" + "Enable/disable all Magithub functionality." + '("true" "false") "true") + +(defun magithub-enabled-p () + "Returns non-nil if Magithub content is available." + (magithub-settings--value-or "magithub.enabled" t + #'magit-get-boolean)) + +(magithub-settings--simple magithub-settings-popup ?o "online" + "Controls whether Magithub is online or offline. + +- `true': requests are made to GitHub for missing data +- `false': no requests are made to GitHub + +In both cases, when there is data in the cache, that data is +used. Refresh the buffer with a prefix argument to disregard the +cache while refreshing: \\<magit-mode-map>\\[universal-argument] \\[magit-refresh]" + '("true" "false") "true") + +(defun magithub-online-p () + "See `magithub-settings--set-magithub.online'. +Returns the value as t or nil." + (magithub-settings--value-or "magithub.online" t + #'magit-get-boolean)) + + +(magithub-settings--simple magithub-settings-popup ?s "status.includeStatusHeader" + "When true, the project status header is included in +`magit-status-headers-hook'." + '("true" "false") "true") + +(defun magithub-settings-include-status-p () + "Non-nil if the project status header should be included." + (magithub-settings--value-or "magithub.status.includeStatusHeader" t + #'magit-get-boolean)) + + +(magithub-settings--simple magithub-settings-popup ?i "status.includeIssuesSection" + "When true, project issues are included in +`magit-status-sections-hook'." + '("true" "false") "true") + +(defun magithub-settings-include-issues-p () + "Non-nil if the issues section should be included." + (magithub-settings--value-or "magithub.status.includeIssuesSection" t + #'magit-get-boolean)) + + +(magithub-settings--simple magithub-settings-popup ?p "status.includePullRequestsSection" + "When true, project pull requests are included in +`magit-status-sections-hook'." + '("true" "false") "true") + +(defun magithub-settings-include-pull-requests-p () + "Non-nil if the pull requests section should be included." + (magithub-settings--value-or "magithub.status.includePullRequestsSection" t + #'magit-get-boolean)) + + +(magithub-settings--simple magithub-settings-popup ?x "contextRemote" + "Use REMOTE as the proxy. +When set, the proxy is used whenever a GitHub repository is needed." + (magit-list-remotes) "origin") + +(defun magithub-settings-context-remote () + "Determine the correct remote to use for issue-tracking." + (magithub-settings--value-or "magithub.contextRemote" "origin")) + +(defvar magithub-confirmation + ;; todo: future enhancement - could allow prompt message to be a function. + '((pre-submit-pr short "You are about to create a pull request to merge branch `%s' into %s:%s; is this what you wanted to do?") + (submit-pr long "Are you sure you want to submit this pull request?") + (submit-pr-from-issue long "Are you sure you wish to create a PR based on %s by merging `%s' into `%s'?") + (pr-allow-maintainers-to-submit short "Allow maintainers to modify this pull request?") + (submit-issue long "Are you sure you want to submit this issue?") + (remove-label short "Remove label {%s} from this issue?") + (add-label short "Add label(s) {%s} to %s#%s?") + (create-repo-as-private long "Will this be a private repository?") + (init-repo-after-create short "Not inside a Git repository; initialize one here?") + (fork long "Fork this repository?") + (fork-create-spinoff short "Create a spinoff branch?") + (fork-add-me-as-remote short "Add %s as a remote in this repository?") + (fork-set-upstream-to-me short "Set upstream to %s?") + (clone long "Clone %s to %s?") + (clone-fork-set-upstream-to-parent short "This repository appears to be a fork of %s; set upstream to that remote?") + (clone-fork-set-proxy-to-upstream short "Use upstream as a proxy for issues, etc.?") + (clone-open-magit-status short "%s/%s has finished cloning to %s. Open?") + (clone-create-directory short "%s does not exist. Create it?") + (ci-refresh-when-offline short "Magithub offline; refresh statuses anyway?") + (refresh short "Refresh GitHub data?") + (refresh-when-API-unresponsive short "GitHub doesn't seem to be responding, are you sure?") + (label-save-customized-colors short "Save customization?") + (user-email short "Email @%s at \"%s\"?") + (user-email-self short "Email yourself?") + (assignee-add long "Assign '%s' to %s#%d?") + (assignee-remove long "Remove '%s' from %s#%d?") + (comment short "Submit this comment to %s?") + (comment-edit short "Commit this edit?") + (comment-delete long "Are you sure you wish to delete this comment?") + (report-error short "%s Report? (A bug report will be placed in your clipboard.)")) + "Alist of actions/decisions to their default behaviors and associated prompts. + +These behaviors can be overridden with (man)git-config. + +A behavior is one of the following symbols: + + `long' + use `yes-or-no-p' to confirm each time + + `short' + use `y-or-n-p' to confirm each time + + `allow' + always allow action + + `deny' + always deny action") + +(defun magithub-confirm (action &rest prompt-format-args) + "Confirm ACTION using Git config settings. +See `magithub--confirm'." + (magithub--confirm action prompt-format-args nil)) + +(defun magithub-confirm-no-error (action &rest prompt-format-args) + "Confirm ACTION using Git config settings. +See `magithub--confirm'." + (magithub--confirm action prompt-format-args t)) + +(defun magithub-settings--from-confirmation-action (action) + "Create a magithub.confirm.* setting from ACTION." + (concat + magithub-settings-section + ".confirm." + (let ((pascal-case (replace-regexp-in-string "-" "" (upcase-initials (symbol-name action))))) + ;; we have PascalCase, we want camelCase + (concat (downcase (substring pascal-case 0 1)) + (substring pascal-case 1))))) + +(defvar magithub-confirm-y-or-n-p-map + (let ((m (make-keymap))) + (define-key m (kbd "C-g") 'quit) ;don't know how to remap keyboard-quit here + (define-key m "q" 'quit) + (define-key m (kbd "C-u") 'cycle) + (define-key m "y" 'allow) + (define-key m "n" 'deny) + m)) + +(defvar magithub-confirm-yes-or-no-p-map + (let ((m (make-sparse-keymap))) + (set-keymap-parent m minibuffer-local-map) + (define-key m [remap universal-argument] #'magithub--confirm-cycle-set-default-interactive) + m)) + +(defvar magithub-confirm--current-cycle nil + "Control how a response should be saved. +This variable should never be set globally; always let-bind it! + + nil + Do not save the response + + `local' + Save response locally + + `global' + Save response globally") + +(defun magithub-confirm-yes-or-no-p (prompt var) + "Like `yes-or-no-p', but optionally save response to VAR." + (let ((p (concat prompt (substitute-command-keys " (yes, no, or \\[universal-argument]*) "))) + magithub-confirm--current-cycle old-cycle done answer changed) + (while (not done) + (setq changed (not (eq old-cycle magithub-confirm--current-cycle)) + old-cycle magithub-confirm--current-cycle + answer (read-from-minibuffer + (magithub--confirm-get-prompt-with-cycle + p var magithub-confirm--current-cycle) + ;; default in what was already entered if the save-behavior changed + (when changed answer) + magithub-confirm-yes-or-no-p-map nil + 'yes-or-no-p-history)) + ;; If the user activated `magithub--confirm-cycle-set-default-interactive', + ;; `magithub-confirm--current-cycle' will have been updated. + (when (and (eq old-cycle magithub-confirm--current-cycle) + (stringp answer)) + (setq answer (downcase (s-trim answer))) + (if (member answer '("yes" "no")) + (setq done t) + (message "Please answer yes or no. ") + (sleep-for 2)))) + (when magithub-confirm--current-cycle + (magithub--confirm-cycle-save-var-value + var (pcase answer + ("yes" "allow") + ("no" "deny")))) + (string= answer "yes"))) + +(defun magithub-confirm-y-or-n-p (prompt var) + "Like `y-or-n-p', but optionally save response to VAR." + (let ((cursor-in-echo-area t) + (newprompt (format "%s (y, n, C-u*) " prompt)) + magithub-confirm--current-cycle done answer varval explain) + (while (not done) + (setq newprompt + (if explain + (format "%s (please answer y or n or use C-u to cycle through and set default answers) " prompt) + (format "%s (y, n, C-u*) " prompt)) + explain nil + answer + (lookup-key magithub-confirm-y-or-n-p-map + (vector + (read-key (magithub--confirm-get-prompt-with-cycle + newprompt var magithub-confirm--current-cycle))))) + (pcase answer + (`quit (keyboard-quit)) + (`cycle (magithub--confirm-cycle-set-default)) + (`allow (setq done t varval "allow")) + (`deny (setq done t varval "deny")) + (_ (setq explain t)))) + (when (stringp varval) + (magithub--confirm-cycle-save-var-value var varval)) + (eq answer 'allow))) + +(defun magithub--confirm-cycle-save-var-value (var val) + "Save VAR with VAL locally or globally. +See `magithub-confirm--current-cycle'." + (pcase magithub-confirm--current-cycle + (`local (magit-set val var)) + (`global (magit-set val "--global" var)))) + +(defun magithub--confirm-cycle-set-default-interactive () + "In `magithub--confirm-yes-or-no-p', update behavior." + (interactive) + (magithub--confirm-cycle-set-default) + (exit-minibuffer)) + +(defun magithub--confirm-cycle-set-default () + (setq magithub-confirm--current-cycle + (cadr (member magithub-confirm--current-cycle + '(nil local global))))) + +(defun magithub--confirm-get-prompt-with-cycle (prompt var cycle) + "Get an appropriate PROMPT associated with VAR for CYCLE. +See `magithub-confirm--current-cycle'." + (propertize + (pcase cycle + (`local (format "%s[and don't ask again: git config %s] " prompt var)) + (`global (format "%s[and don't ask again: git config --global %s] " prompt var)) + (_ prompt)) + 'face 'minibuffer-prompt)) + +(defun magithub--confirm (action prompt-format-args noerror) + "Confirm ACTION using Git config settings. + +When PROMPT-FORMAT-ARGS is non-nil, the prompt piece of ACTION's +confirmation spec is passed through `format' with these +arguments. + +Unless NOERROR is non-nil, denying ACTION will result in a user +error to abort the action. + +This is like `magit-confirm', but a little more powerful. It +might belong in Magit, but we'll see how it goes." + (let ((spec (alist-get action magithub-confirmation)) + var default prompt setting choice) + (unless spec + (magithub-error "No confirmation settings for %S" spec)) + (unless (= 2 (length spec)) + (magithub-error "Spec for %S must have 2 members: %S" action spec)) + (setq default (symbol-name (nth 0 spec)) + prompt (nth 1 spec) + var (magithub-settings--from-confirmation-action action)) + (when prompt-format-args + (setq prompt (apply #'format prompt prompt-format-args))) + (when (and (null noerror) (string= "deny" default)) + (magithub-error (format "The default for %S is deny, but this will cause an error" action))) + + (setq setting (magithub-settings--value-or var default)) + (when (and (string= setting "deny") + (null noerror)) + (let ((raw (magit-git-string "config" "--show-origin" var)) + washed) + (when (string-match (rx bos (group (+ any)) (+ space) (group (+ any)) eos) raw) + (setq washed (format "%s => %s" + (match-string 1 raw) + (match-string 2 raw)))) + (user-error "Abort per %s [%s]" var (or washed raw)))) + + (setq choice + (pcase setting + ("long" (magithub-confirm-yes-or-no-p prompt var)) + ("short" (magithub-confirm-y-or-n-p prompt var)) + ("allow" t) + ("deny" nil))) + + (or choice + (unless noerror + (user-error "Abort"))))) + +(defun magithub-confirm-set-default-behavior (action default &optional globally) + "Set the default behavior of ACTION to DEFAULT. + +If GLOBALLY is non-nil, make this configuration apply globally. + +See `magithub-confirmation' for valid values of DEFAULT." + (unless (alist-get action magithub-confirmation) + (error "Action not defined: %S" action)) + (let* ((var (magithub-settings--from-confirmation-action action)) + (args (list var))) + (when globally + (push "--global" args)) + (apply #'magit-set + (if (memq default '(long short allow deny)) + (symbol-name default) + (error "Invalid default behavior: %S" default)) + args) + default)) + +(provide 'magithub-settings) +;;; magithub-settings.el ends here diff --git a/magithub-user.el b/magithub-user.el new file mode 100644 index 0000000..8aa2371 --- /dev/null +++ b/magithub-user.el @@ -0,0 +1,149 @@ +;;; magithub-user.el --- Inspect users -*- lexical-binding: t; -*- + +;; Copyright (C) 2016-2018 Sean Allred + +;; Author: Sean Allred <code@seanallred.com> +;; Keywords: lisp + +;; 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 <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Code for dealing with the current user and other users. + +;;; Code: + +(require 'ghub+) +(require 'cl-lib) +(require 'thingatpt) + +(require 'magithub-core) + +(defvar magit-magithub-user-section-map + (let ((m (make-sparse-keymap))) + (set-keymap-parent m magithub-map) + (define-key m [remap magit-visit-thing] #'magithub-user-visit) + (define-key m [remap magithub-browse-thing] #'magithub-user-browse) + (define-key m "m" #'magithub-user-email) + m)) + +(defvar magit-magithub-assignee-section-map + (let ((m (make-sparse-keymap))) + (set-keymap-parent m magit-magithub-user-section-map) + (define-key m "a" #'magithub-assignee-add) + (define-key m [remap magit-delete-thing] #'magithub-assignee-remove) + m)) + +(defun magithub-user-me () + "Return the currently-authenticated user." + (magithub-cache :user-demographics + `(magithub-request + (ghubp-get-user)) + :message + "user object for the currently-authenticated user")) + +(defun magithub-user (user) + "Return the full object for USER." + (magithub-cache :user-demographics + `(magithub-request + (ghubp-get-users-username ',user)))) + +(defun magithub-assignee--verify-manage () + (or (magithub-repo-push-p) + (user-error "You don't have permission to manage assignees in this repository"))) + +(defun magithub-assignee-add (issue user) + (interactive (when (magithub-assignee--verify-manage) + (let ((issue (magit-section-parent-value (magit-current-section)))) + (list issue + (magithub-user-choose-assignee + "Choose an assignee: " + (magithub-issue-repo issue)))))) + (let-alist `((repo . ,(magithub-issue-repo issue)) + (issue . ,issue) + (user . ,user)) + (magithub-confirm 'assignee-add + .user.login + (magithub-repo-name .repo) + .issue.number) + (prog1 (magithub-request + (ghubp-post-repos-owner-repo-issues-number-assignees + .repo .issue (list .user))) + (let ((sec (magit-current-section))) + (magithub-cache-without-cache :issues + (magit-refresh-buffer)) + (magit-section-show sec))))) + +(defun magithub-assignee-remove (issue user) + (interactive (when (magithub-assignee--verify-manage) + (list (thing-at-point 'github-issue) + (thing-at-point 'github-user)))) + (let-alist `((repo . ,(magithub-issue-repo issue)) + (issue . ,issue) + (user . ,user)) + (magithub-confirm .user.login + (magithub-repo-name .repo) + .issue.number) + (prog1 (magithub-request + (ghubp-delete-repos-owner-repo-issues-number-assignees .repo .issue (list .user))) + (magithub-cache-without-cache :issues + (magit-refresh-buffer))))) + +(defun magithub-user-choose (prompt &optional default-user) + (let (ret-user new-username) + (while (not ret-user) + (setq new-username + (magit-read-string-ns + (concat prompt + (if new-username (format " ['%s' not found]" new-username))) + (alist-get 'login default-user))) + (when-let ((try (condition-case _ + (magithub-request + (ghubp-get-users-username `((login . ,new-username)))) + (ghub-404 nil)))) + (setq ret-user try))) + ret-user)) + +(defun magithub-user-choose-assignee (prompt &optional repo default-user) + (magithub--completing-read + prompt + (magithub-request + (ghubp-get-repos-owner-repo-assignees repo)) + (lambda (user) (let-alist user .login)) + nil t default-user)) + +(defalias 'magithub-user-visit #'magithub-user-browse) +(defun magithub-user-browse (user) + "Open USER on GitHub." + (interactive (list (thing-at-point 'github-user))) + (if user + (browse-url (alist-get 'html_url user)) + (user-error "No user here"))) + +(defun magithub-user-email (user) + "Email USER." + (interactive (list (thing-at-point 'github-user))) + (when (string= (alist-get 'login (magithub-user-me)) + (alist-get 'login user)) + (magithub-confirm 'user-email-self)) + (unless user + (user-error "No user here")) + (let-alist user + (unless .email + (user-error "No email found; target user may be private")) + (magithub-confirm 'user-email .login .email) + (browse-url (format "mailto:%s" .email)))) + +(provide 'magithub-user) +;;; magithub-user.el ends here diff --git a/magithub.el b/magithub.el new file mode 100644 index 0000000..b13202c --- /dev/null +++ b/magithub.el @@ -0,0 +1,294 @@ +;;; magithub.el --- Magit interfaces for GitHub -*- lexical-binding: t; -*- + +;; Copyright (C) 2016-2018 Sean Allred + +;; Author: Sean Allred <code@seanallred.com> +;; Keywords: git, tools, vc +;; Homepage: https://github.com/vermiculus/magithub +;; Package-Requires: ((emacs "25") (magit "2.12") (s "1.12.0") (ghub+ "0.3") (git-commit "2.12") (markdown-mode "2.3")) +;; Package-Version: 0.1.7 + +;; 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 <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Magithub is a Magit-based interface to GitHub. +;; +;; Integrated into Magit workflows, Magithub lets you interact with +;; your GitHub repositories and manage your work/play from emacs: +;; +;; - push brand-new local repositories up to GitHub +;; - create forks of existing repositories +;; - submit pull requests upstream +;; - view and create issues +;; - view, create, and edit comments +;; - view status checks (e.g., Travis CI) +;; - manage labels and assignees +;; - view/visit notifications +;; - write personal notes on issues for reference later +;; - and probably more... +;; +;; Press `H' in the status buffer to get started -- happy hacking! + +;;; Code: + +(require 'magit) +(require 'magit-process) +(require 'cl-lib) +(require 's) +(require 'dash) +(require 'ghub+) + +(require 'magithub-core) +(require 'magithub-issue) +(require 'magithub-ci) +(require 'magithub-issue-post) +(require 'magithub-issue-tricks) +(require 'magithub-orgs) +(require 'magithub-dash) + +;;;###autoload (autoload 'magithub-dispatch-popup "magithub" nil t) +(magit-define-popup magithub-dispatch-popup + "Popup console for dispatching other Magithub popups." + 'magithub-commands + :variables '((?C "Settings..." magithub-settings-popup)) + :actions '("Actions" + (?d "Dashboard" magithub-dashboard) + (?H "Browse on GitHub" magithub-browse) + (?c "Create on GitHub" magithub-create) + (?f "Fork this repo" magithub-fork) + (?i "Submit an issue" magithub-issue-new) + (?p "Submit a pull request" magithub-pull-request-new) + "Meta" + (?& "Request a feature or report a bug" magithub--meta-new-issue) + (?h "Ask for help on Gitter" magithub--meta-help))) + +;;;###autoload +(eval-after-load 'magit + '(progn + (magit-define-popup-action 'magit-dispatch-popup + ?H "Magithub" #'magithub-dispatch-popup ?!) + (define-key magit-status-mode-map + "H" #'magithub-dispatch-popup))) + +(defun magithub-browse () + "Open the repository in your browser." + (interactive) + (unless (magithub-github-repository-p) + (user-error "Not a GitHub repository")) + (magithub-repo-visit (magithub-repo))) + +(defvar magithub-after-create-messages + '("Don't be shy!" + "Don't let your dreams be dreams!") + "One of these messages will be displayed after you create a +GitHub repository.") + +(defun magithub-create (repo &optional org) + "Create REPO on GitHub. + +If ORG is non-nil, it is an organization object under which to +create the new repository. You must be a member of this +organization." + (interactive (if (or (not (magit-toplevel)) (magithub-github-repository-p)) + (list nil nil) + (let* ((ghub-username (ghubp-username)) ;performance + (account (magithub--read-user-or-org)) + (priv (magithub-confirm-no-error 'create-repo-as-private)) + (reponame (magithub--read-repo-name account)) + (desc (read-string "Description (optional): "))) + (list + `((name . ,reponame) + (private . ,priv) + (description . ,desc)) + (unless (string= ghub-username account) + `((login . ,account))))))) + (when (magithub-github-repository-p) + (error "Already in a GitHub repository")) + (if (not (magit-toplevel)) + (when (magithub-confirm-no-error 'init-repo-after-create) + (magit-init default-directory) + (call-interactively #'magithub-create)) + (with-temp-message "Creating repository on GitHub..." + (setq repo + (magithub-request + (if org + (ghubp-post-orgs-org-repos org repo) + (ghubp-post-user-repos repo))))) + (magithub--random-message "Creating repository on GitHub...done!") + (magit-status-internal default-directory) + (magit-remote-add "origin" (magithub-repo--clone-url repo)) + (magit-refresh) + (when (magit-rev-verify "HEAD") + (magit-push-popup)))) + +(defun magithub--read-user-or-org () + "Prompt for an account with completion. + +Candidates will include the current user and all organizations, +public and private, of which they're a part. If there is only +one candidate (i.e., no organizations), the single candidate will +be returned without prompting the user." + (let ((user (ghubp-username)) + (orgs (ghubp-get-in-all '(login) + (magithub-orgs-list))) + candidates) + (setq candidates orgs) + (when user (push user candidates)) + (cl-case (length candidates) + (0 (user-error "No accounts found")) + (1 (car candidates)) + (t (completing-read "Account: " candidates nil t))))) + +(defun magithub--read-repo-name (for-user) + (let* ((prompt (format "Repository name: %s/" for-user)) + (dirnam (file-name-nondirectory (substring default-directory 0 -1))) + (valid-regexp (rx bos (+ (any alnum "." "-" "_")) eos)) + ret) + ;; This is not very clever, but it gets the job done. I'd like to + ;; either have instant feedback on what's valid or not allow users + ;; to enter invalid names at all. Could code from Ivy be used? + (while (not (s-matches-p valid-regexp + (setq ret (read-string prompt nil nil dirnam)))) + (message "invalid name") + (sit-for 1)) + ret)) + +(defun magithub--random-message (&optional prefix) + (let ((msg (nth (random (length magithub-after-create-messages)) + magithub-after-create-messages))) + (if prefix (format "%s %s" prefix msg) msg))) + +(defun magithub-fork () + "Fork 'origin' on GitHub." + (interactive) + (unless (magithub-github-repository-p) + (user-error "Not a GitHub repository")) + (magithub-confirm 'fork) + (let* ((repo (magithub-repo)) + (fork (with-temp-message "Forking repository on GitHub..." + (magithub-request + (ghubp-post-repos-owner-repo-forks repo))))) + (when (magithub-confirm-no-error 'fork-create-spinoff) + (call-interactively #'magit-branch-spinoff)) + (magithub--random-message + (let-alist repo (format "%s/%s forked!" .owner.login .name))) + (let-alist fork + (when (magithub-confirm-no-error 'fork-add-me-as-remote .owner.login) + (magit-remote-add .owner.login (magithub-repo--clone-url fork)) + (magit-set .owner.login "branch" (magit-get-current-branch) "pushRemote"))) + (let-alist repo + (when (magithub-confirm-no-error 'fork-set-upstream-to-me .owner.login) + (call-interactively #'magit-set-branch*merge/remote))))) + +(defvar magithub-clone-history nil + "History for `magithub-clone' prompt.") + +(defun magithub-clone--get-repo () + "Prompt for a user and a repository. +Returns a sparse repository object." + (let ((user (ghubp-username)) + (repo-regexp (rx bos (group (+ (not (any " ")))) + "/" (group (+ (not (any " ")))) eos)) + repo) + (while (not (and repo (string-match repo-regexp repo))) + (setq repo (read-from-minibuffer + (concat + "Clone GitHub repository " + (if repo "(format is \"user/repo\"; C-g to quit)" "(user/repo)") + ": ") + (when user (concat user "/")) + nil nil 'magithub-clone-history))) + `((owner (login . ,(match-string 1 repo))) + (name . ,(match-string 2 repo))))) + +(defcustom magithub-clone-default-directory nil + "Default directory to clone to when using `magithub-clone'. +When nil, the current directory at invocation is used." + :type 'directory + :group 'magithub) + +(defun magithub-clone (repo dir) + "Clone REPO. +Banned inside existing GitHub repositories if +`magithub-clone-default-directory' is nil. + +See also `magithub-preferred-remote-method'." + (interactive (let* ((repo (magithub-clone--get-repo)) + (repo (or (magithub-request + (ghubp-get-repos-owner-repo repo)) + (let-alist repo + (user-error "Repository %s/%s does not exist" + .owner.login .name)))) + (name (alist-get 'name repo)) + (dirname (read-directory-name + "Destination: " + magithub-clone-default-directory + name nil name))) + (list repo dirname))) + ;; Argument validation + (unless (called-interactively-p 'any) + (unless (setq repo (magithub-request + (ghubp-get-repos-owner-repo repo))) + (let-alist repo + (user-error "Repository %s/%s does not exist" + .owner.login .name)))) + (let ((parent (file-name-directory dir))) + (unless (file-exists-p parent) + (when (magithub-confirm 'clone-create-directory parent) + (mkdir parent t)))) + (unless (file-writable-p dir) + (user-error "%s is not writable" dir)) + + (let-alist repo + (when (magithub-confirm-no-error 'clone .full_name dir) + (let (set-upstream set-proxy) + (setq set-upstream + (and .fork (magithub-confirm-no-error + 'clone-fork-set-upstream-to-parent + .parent.full_name)) + set-proxy + (and set-upstream (magithub-confirm-no-error + 'clone-fork-set-proxy-to-upstream))) + (condition-case _ + (let ((default-directory dir) + (magit-clone-set-remote.pushDefault t)) + (mkdir dir t) + (magit-clone (magithub-repo--clone-url repo) dir) + (while (process-live-p magit-this-process) + (magit-process-buffer) + (message "Waiting for clone to finish...") + (sit-for 1)) + (when set-upstream + (let ((upstream "upstream")) + (when set-proxy (magit-set upstream "magithub.proxy")) + (magit-remote-add upstream (magithub-repo--clone-url .parent)) + (magit-set-branch*merge/remote (magit-get-current-branch) + upstream))))))))) + +(defun magithub-clone--finished (user repo dir) + "After finishing the clone, allow the user to jump to their new repo." + (when (magithub-confirm-no-error 'clone-open-magit-status user repo dir) + (magit-status-internal (s-chop-suffix "/" dir)))) + +(defun magithub-visit-thing () + (interactive) + (user-error + (with-temp-buffer + (use-local-map magithub-map) + (substitute-command-keys + "Deprecated; use `\\[magithub-browse-thing]' instead")))) + +(provide 'magithub) +;;; magithub.el ends here diff --git a/magithub.org b/magithub.org new file mode 100644 index 0000000..1bcb863 --- /dev/null +++ b/magithub.org @@ -0,0 +1,708 @@ +#+TITLE: Magithub -- Magit interfaces for GitHub +#+AUTHOR: Sean Allred +#+EMAIL: code@seanallred.com +#+DATE: 2017-2018 +#+LANGUAGE: en + +#+TEXINFO_DIR_CATEGORY: Emacs +#+TEXINFO_DIR_TITLE: Magithub: (magithub). +#+TEXINFO_DIR_DESC: Magit interfaces for GitHub +#+SUBTITLE: for version 0.1.5 (0.1.5-106-ge4a004c+1) +#+BIND: ox-texinfo+-before-export-hook ox-texinfo+-update-version-strings + +#+TEXINFO_DEFFN: t +#+OPTIONS: H:4 num:4 toc:2 + +You may also be interested in [[https://github.com/vermiculus/magithub/tree/master/RelNotes][the most current release notes]]. + +Magithub provides an integrated GitHub experience through Magit's familiar +interface. Just as Magit hopes to 'outsmart git', Magithub hopes to add +smarts to GitHub for performing common tasks. + +Happy hacking! + +#+TEXINFO: @noindent +This manual is for Magithub version 0.1.5 (0.1.5-106-ge4a004c+1). + +#+BEGIN_QUOTE +Copyright (C) 2017-2018 Sean Allred <code@seanallred.com> + +You can redistribute this document 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 document 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. +#+END_QUOTE + +* Installation +** _ :ignore: + +Magithub can be installed from [[http://melpa.milkbox.net/#/magithub][MELPA]] using =M-x list-packages= or by +evaluating the following: + +#+BEGIN_SRC elisp + (package-install 'magithub) +#+END_SRC + +Here is the basic recommended [[https://github.com/jwiegley/use-package][=use-package=]] configuration: + +#+BEGIN_SRC elisp + (use-package magithub + :after magit + :ensure t + :config (magithub-feature-autoinject t)) +#+END_SRC + +If you prefer to install the package manually, this can of course be done +via the usual means. + +For more information, see [[info:emacs#Packages]]. + +** Authentication + +Given GitHub's rate-limiting policy, Magithub is unlikely to ever support +running without authenticating. As such, you /must/ authenticate before you +use Magithub. (As of #107, Magithub will not even attempt go online until +you're properly authenticated.) + +To authenticate, you can simply start using Magithub; Ghub should walk you +through the authentication process unless you use two-factor authentication. +(Your token is stored in one of your ~auth-sources~; see [[https://magit.vc/manual/ghub/How-Ghub-uses-Auth_002dSource.html#How-Ghub-uses-Auth_002dSource][Ghub's manual]] for +details.) + +If you do use two-factor authentication, you must + +1. Manually create a GitHub token (from https://github.com/settings/tokens) + for scopes `repo`, `notifications` and `user` (see variable + ~magithub-github-token-scopes~) +2. Store it for Magithub per user in one of your ~auth-sources~ + (e.g. =~/.authinfo=). Write a line like this: + + #+BEGIN_EXAMPLE + machine api.github.com login YOUR_GITHUB_USERNAME^magithub password YOUR_GITHUB_TOKEN + #+END_EXAMPLE + +Beware that writing the token in plaintext in =~/.authinfo= (or elsewhere) is +not secure against attackers with access to that file. For details and +better alternatives (like using GPG), see Ghub's manual on [[https://magit.vc/manual/ghub/Manually-Creating-and-Storing-a-Token.html#Manually-Creating-and-Storing-a-Token][Manually Creating +and Storing a Token]] and [[https://magit.vc/manual/ghub/How-Ghub-uses-Auth_002dSource.html#How-Ghub-uses-Auth_002dSource][How Ghub uses Auth-Source]]. + +If you want to authenticate Ghub without using Magithub, you can simply +evaluate the following: + +#+BEGIN_SRC emacs-lisp + (require 'magithub) + (ghub-get "/user" nil :auth 'magithub) +#+END_SRC + +After Ghub walks you through the authentication process during evaluation, +the ~ghub-get~ form should return familiar information (your login, email, +etc.). + +If you're having trouble /authenticating/, [[https://github.com/magit/ghub/issues/new][open a Ghub issue]] or drop by +[[https://gitter.im/vermiculus/magithub][Magithub's]] or [[https://gitter.im/magit/magit][Magit's]] Gitter channel. + +** Enterprise Support + +For GitHub Enterprise support, you'll need to add your enterprise domain to +~magithub-github-hosts~ so that Magithub can detect when it's in a GitHub +repository. You will also need to configure your =~/.authinfo= file +appropriately to authenticate to your domain; see Ghub's manual for details. + +* Introduction +** _ :ignore: + +Magithub tries to follow closely Magit's lead in general interface. Most of +its functionality is developed to tightly integrate with its section/ +framework. See [[https://magit.vc/manual/magit/Sections.html#Sections][Magit's documentation]] for information on how to navigate +using this framework. + +Magithub's functionality uses section-specific keymaps to expose +functionality. Where it makes sense, the following keys will map to +functions that 'do the right thing': + +- Key: w, magithub-browse-thing + + Open a browser to the thing at point. For instance, when point is on + issue 42 in your-favorite/github-repo, we'll open + =http://github.com/your-favorite/github-repo/issue/42=. + +- Key: a, magithub-add-thing + + Add something to the thing at point. For instance, on a list of labels, + you can add more labels. + +- Key: e, magithub-edit-thing + + Edit the thing at point, such as an issue. + +- Key: r, magithub-reply-thing + + Reply to the thing at point, such as a comment. + +Magithub also considers the similar placeholder commands introduced by Magit +which you may already be familiar with: + +- Key: k, magit-delete-thing +- Key: RET, magit-visit-thing + +These concepts are intended to provide a more consistent experience +throughout Magithub within Magit by categorizing your broader interactions +with all GitHub content. As with Magit, more commands are added as the +situation calls for it. + +** Note + +By default, Magithub enables itself in all repositories where =origin= points +to GitHub. + +- User Option: magithub-enabled-by-default + + When non-nil, Magithub is enabled by default. This is the fallback value + of git variable =magithub.enabled= is not set in this repository. + +- User Option: magithub-github-hosts + + A list of top-level domains that should be recognized as GitHub hosts. + +** Brief Tutorial + +Here's a script that will guide you through the major features of Magithub. +This is not a replacement for the documentation, but rather an example +workflow to whet your appetite. + +*** Clone a repository +#+BEGIN_EXAMPLE +M-x magithub-clone RET vermiculus/my-new-repository +#+END_EXAMPLE +Cloning a repository this way gets the clone URL from GitHub and forwards +that on to ~magit-clone~. If the repository is a fork, you're prompted to add +the parent is added under the =upstream= remote. + +Fork behavior may change in the future. It may be more appropriate to +actually/ clone the source repository and add your remote as a fork. This +will cover the 90% case (the 10% case being active forks of unmaintained +projects). + +*** Viewing project status +You are dropped into a status buffer for =vermiculus/my-new-repository=. You +see some open issues and pull requests. You move your cursor to an issue of +interest and =TAB= to expand it, seeing the author, when it was +created/updated, any labels, and a preview of the issue contents. + +If =vermiculus/my-new-repository= used any status checks, you would see those +statuses as a header in this buffer. + +*** Viewing and replying to an issue +You =RET= on the issue and are taken to a dedicated buffer for that issue. +You can now see its full contents as well as all comments. You'd like to +leave a comment -- a suggestion for a fix or an additional use-case to +consider -- you press =r= to open a new buffer to /reply/ to this issue. You +write your comment and =C-c C-c= to submit. But, oh no! You didn't turn on +=flyspell-mode= in markdown buffers, so you submitted a spelling error. A +simple =e= on the comment will /edit/ it. After submitting again with =C-c C-c=, +everything is well. + +Right now, other activity on the issue is not inserted into this buffer. +Press =w= to open the issue in your browser. + +*** Creating an issue +You notice a small issue in how some feature is implemented, so back in the +status buffer, you use =H i= to create a new issue. (While inside the GitHub +repository, you could've used any key bound to ~magithub-issue-new~.) The +first line is the title of the new issue; everything else is the body. You +submit the issue with =C-c C-c=. + +You come back a little while later to leave additional details -- you reply +to your own issue in a comment, but realize you should just edit your +original issue to avoid confusion. You =k= to /kill/ / delete the comment. + +*** Creating a pull request +Since you care about this project and want to help it succeed, you decide to +fix this issue yourself. You checkout a new branch (=b c my-feature RET=) and +get to work. + +Because you're so /awesome/, you're ready to push your commit to fix your +issue. After realizing you don't have push permissions to this repository, +you create a fork using =H f=. You push your branch to your new remote (named +after your username) and create a pull request with =H p=. You select the +head branch as =my-feature= and the base branch as =master= (or whatever the +production/staging branch is for the project). You fill out the pull +request template provided by the project (and inserted into your PR) and off +you go! + +* Status Buffer Integration + +The part of Magithub you're likely to interact with the most is +embedded right into Magit's status buffer. + +- Key: H, magithub-dispatch-popup + + Access many Magithub entry-points. See [[*Dispatch Popup]] for more details. + +- Key: H C e, FIXME + + Toggle status buffer integration in this repository. + +There are two integrations turned on by default: + +** Project Status + +Many services (such as Travis CI and CircleCI) will post statuses to +commits. A summary of these statuses are visible in the status buffer +headers. + +- Key: RET, magithub-ci-visit +- Key: w, magithub-ci-visit + + Visit the service's summary of this status. For example, a status posted + by Travis CI will open that build on Travis. + +- Key: g, magithub-ci-refresh + + Refresh statuses from GitHub and then refresh the current buffer. + +- Key: H C s, FIXME + + Enable/disable status checks in this repository. + +** Open Issues and Pull Requests + +These will also display in the status buffer. There's a lot of +functionality available right from an issue section. + +- Key: g, magithub-issue-refresh + + Refresh issues and pull requests from GitHub and then refresh the current + buffer. + +- Key: RET, magithub-issue-visit + + Open a new buffer to view an issue and its comments. + +- Key: w, magithub-issue-browse +- Key: w, magithub-pull-browse + + Browse this issue / pull request on GitHub. + +- Key: N, magithub-issue-personal-note + + Opens a buffer for offline note-taking. + +- Key: L, magithub-issue-add-labels + + Add labels to the issue. + +- Key: a, magithub-label-add +- Key: k, magithub-label-remove + + When point is on a label section, you can add/remove labels (provided you + have permission to do so). + +- Command: magithub-label-color-replace + + Labels are colored as they would be on GitHub. In some themes, this + produces an illegible or otherwise undesirable color. This command can + help you find a substitute for labels of this color. + +- Variable: magithub-issue-details-hook + + Control which issue details display in the status buffer. Functions + intended for this variable use the =magithub-issue-detail-insert-*= prefix. + + Performance note: judicious use of this variable can improve your overall + Magit experience in large buffers. + +- User Option: magithub-issue-issue-filter-functions +- User Option: magithub-issue-pull-request-filter-functions + + These are lists of functions which must all return non-nil for an issue/PR + to be displayed in the status buffer. They all receive the issue/PR + object as their sole argument. For example, you might want to filter out + issues labels =enhancement= from your list: + + #+BEGIN_SRC emacs-lisp + (setq magithub-issue-issue-filter-functions + (list (lambda (issue) ; don't show enhancement requests + (not + (member "enhancement" + (let-alist issue + (ghubp-get-in-all '(name) .labels))))))) + #+END_SRC + +*** Manipulating the Cache + When point is on a Magithub-controlled section (like the status header): + | Default Key | Description | + |-------------+--------------------------------------------| + | =g= | Refresh only this section's GitHub content | + | =C-u g= | Like =g=, but works on the whole buffer | + +*** Offline Mode + | Default Key | Description | + |-------------+---------------------| + | =H C c= | Toggle offline mode | + + Offline mode was introduced for those times when you're on the go, but you'd + still like to have an overview of GitHub data in your status buffer. It's + also useful for folks who want to explicitly control when Emacs communicates + with GitHub -- for this purpose, you can use =C-u g= (discussed above) to pull + data from GitHub while in offline mode. + + To start into offline mode everywhere, use + #+BEGIN_SRC sh + git config --global magithub.cache always + #+END_SRC + + See the documentation for function ~magithub-settings--set-magithub.cache~ + for details on appropriate values. + +*** Controlling Sections + + Sections like the issue list and the status header can be toggled with the + interactive functions of the form =magithub-toggle-*=. These functions have + no default keybinding. + + Since status checks can be API-hungry and not all projects use them, you can + disable the status header at the repository-level with =H ~=; see the Status + Checks section for more information. + +* Dispatch Popup + +Much of Magithub's functionality, including configuration options, is behind +this popup. In Magit status buffers, it's bound to =H=. + +- Key: d, magithub-dashboard + + See [[*Dashboard]]. + +- Key: c, magithub-create + + Push a local repository up to GitHub. + +- Key: H, magithub-browse + + Open the current repository in your browser. + +- Key: f, magithub-fork + + Fork this repository on GitHub. This will add your fork as a remote under + your username. For example, if user =octocat= forked Magit, we would see a + new remote called =octocat= pointing to =octocat/magit=. + +- Key: i, magithub-issue-new +- Key: p, magithub-pull-request-new + + Open a new buffer to create an issue or open a pull request. See + [[*Creating Content]]. + +** Configuration + +Per-repository configuration is controlled via git variables reachable from +the dispatch popup via =H C=. Use =? <key>= to get online help for each +variable in that popup. + +- Key: C e, FIXME + + Turn Magithub on/off (completely). + +- Key: C s, FIXME + + Turn the project status header on/off. + +- Key: C c, FIXME + + Control whether Magithub is considered 'online'. This controls the + behavior of the the cache. This may go away in the future. See + [[*Manipulating the Cache]] for more details. + +- Key: C i, FIXME + + Toggle the issues section. + +- Key: C p, FIXME + + Toggle the pull requests section. + +- Key: C x, FIXME + + Set the 'proxy' used for this repository. See [[*Proxies]]. + +** Meta + +Since Magithub is so integrated with Magit, there's often confusion about +whom to ask for support (especially for users of preconfigured Emacsen like +Spacemacs and Prelude). Hopefully, these functions can direct you to the +appropriate spot. + +- Key: &, magithub--meta-new-issue + + Open the browser to create a new issue for Magithub functionality + described in this document. + +- Key: h, magithub--meta-help + + Open the browser to ask for help on Gitter, a GitHub-focused chatroom. + +* 'Features' + +Given that some features of Magithub are not desired by or appropriate for +every type of user, there are features that are not turned on by default. +These are features that are injected into standard Magit popups. + +The list of available features is available in constant +~magithub-feature-list~. Despite its name, this is an alist of symbols (i.e., +'features') to functions that install the feature. While the documentation +for each feature lives in that symbol, you would normally not otherwise +interact with it. + +- Function: magithub-feature-autoinject + + This function is the expected interface to install features. You will + normally use + #+BEGIN_SRC emacs-lisp + (magithub-feature-autoinject t) + #+END_SRC + in your configuration to install all features, but you have the option of + installing them one at a time using the symbols from constant + ~magithub-feature-list~ or as a list of those symbols: + #+BEGIN_SRC emacs-lisp + (magithub-feature-autoinject 'commit-browse) + (magithub-feature-autoinject '(commit-browse pull-request-merge)) + #+END_SRC + +* Cloning + +- Command: magithub-clone + + Clone a repository from GitHub. + +- User Option: magithub-clone-default-directory + + The default destination directory to use for cloning. + +- User Option: magithub-preferred-remote-method + + This option is a symbol indicating the preferred cloning method (between + HTTPS, SSH, and the =git://= protocol). + +* Dashboard + +The dashboard shows you information pertaining to /you/: +- notifications +- issues and pull requests you're assigned per repository +as well as contextual information like the logged-in user and [[https://developer.github.com/v3/#rate-limiting][rate-limiting]] +information. + +- Command: magithub-dashboard + + View your dashboard. + +- Key: ;, magithub-dashboard-popup + + Configure your global dashboard settings. + +- User Option: magithub-dashboard-show-read-notifications + + When non-nil, we'll show read notifications in the dashboard. + +* Creating Content + +It's great to read about what's been happening, but it's even better to +contribute your own thoughts and activity! + +- Key: H i, magithub-issue-new +- Key: H p, magithub-pull-request-new + + Create issues and pull requests. If you have push access to the + repository, you'll have the opportunity to add labels before you submit + the issue. + + Creating a pull request requires a HEAD branch, a BASE branch, and to know + which remote points to your fork. + +- Key: r, magithub-comment-new +- Key: r, magithub-comment-reply + + On an issue or pull request section, ~magithub-comment-new~ will allow you + to post a comment to that issue/PR. If point is already on a comment, + ~magithub-comment-reply~ will quote the comment at point for you. + +* Caching + +Caching is a complicated topic with a long Magithub history of, well, +failure. As of today, all data retrieved from the API is cached by +default. Using =g= on Magithub sections will usually refresh the information +in the buffer pertaining to that section. Otherwise, =C-u g= in any Magit +buffer will refresh all GitHub data in that buffer. + +This behavior may change in the future, but for now, it's the most stable +option. See + +* Proxies + +It's not uncommon to have repositories where the bug-tracker is in a +separate repository. For these cases, you can use the idea of 'proxies'. A +proxy is a remote (with a GitHub-associated URL) that you choose to use for +all GitHub API requests concerning the /actual/ current repository. This is +manifest in the git variable =magithub.proxy=. + +- Function: magithub-proxy-set-default + + If you consistently use a specific remote name for the bug tracker, you + can set it globally. + +All GitHub requests specific to the current repository context are routed +through ~magithub-repo~ which respects this proxy. + +* Configuring + +Magithub uses a standardized configuration scheme implemented using Git +variables. This allows your Magithub configuration to use all the powerful +features of =git-config(1)= and allows tight integration into Magit's existing +repository configuration workflows. + +To get the most up-to-date list of configuration options, use +#+BEGIN_SRC example +M-x apropos-command RET magithub-settings--set +#+END_SRC +to summarize them all. If an important option is missing from this manual, +reports and pull requests are welcome! + +The decision to implement these as Git variables stems from the varying size +of project repositories: it is extremely common to contribute to +exceptionally large repositories where including, say, the 'issues' section +would bring Emacs to its knees -- but it is equally common to work on +smaller repositories where such concern is negligible and the issues section +is a nice feature. + +* Unfiled +** Content +*** Working with Repositories +**** DONE General +| Default Key | Description | +|--------------------+------------------------------------------------| +| =H H= | Opens the current repository in the browser | +| =H c= | Creates the current local repository on GitHub | +| =M-x magithub-clone= | Clone a repository | + +=magithub-clone= may appear to be a thin wrapper over =magit-clone=, but it's +quite a bit smarter than that. We'll of course respect +=magithub-preferred-remote-method= when cloning the repository, but we can +also detect when the repository is a fork and can create and set an upstream +remote accordingly (similar to =M-x magithub-fork=). + +**** DONE Issues +| Default Key | Description | +|-------------+--------------------------| +| =H i= | Create a new issue | +| =RET= | Open the issue in GitHub | + +You can filter issues with =magithub-issue-issue-filter-functions=: +#+BEGIN_SRC emacs-lisp + (setq magithub-issue-issue-filter-functions + (list (lambda (issue) ; don't show enhancement requests + (not + (member "enhancement" + (let-alist issue + (ghubp-get-in-all '(name) .labels))))))) +#+END_SRC +Each function in the =*-functions= list must return non-nil for the issue to +appear in the issue list. See also the documentation for that variable. + +**** DONE Forking and Pull Requests +| Default Key | Description | +|-------------+-------------------------------| +| =H f= | Fork the current repository | +| =H p= | Submit pull requests upstream | + +You can also filter pull requests with +=magithub-issue-pull-request-filter-functions=. See the section on +issue-filtering for an example. + +**** TODO Labels +| Default Key | Description | +|----------------------------------+-------------------------------------------| +| =M-x magithub-label-color-replace= | Choose a new color for the label at point | + +By default, Magithub will adopt the color used by GitHub when showing +labels. In some themes, this doesn't provide enough contrast. Use =M-x +magithub-label-color-replace= to replace the current label's color with +another one. (This will apply to all labels in all repositories, but will +of course not apply to all /shades/ of the original color.) + +**** TODO Status Checks +| Default Key | Description | +|-------------+--------------------------------------------------| +| =RET= | Visit the status's dashboard in your browser | +| =TAB= | On the status header, show individual CI details | +| =H ~= | Toggle status integration for this repository | + +When the status buffer first opens, the status header is inserted at the top +and probably looks something like this: +#+BEGIN_EXAMPLE +Status: Success +#+END_EXAMPLE + +You can get a breakdown of which checks succeeded and which failed by using +=TAB=: +#+BEGIN_EXAMPLE +Status: Success + Checks for ref: develop + Success The Travis CI build passed continuous-integration/travis-ci/push +#+END_EXAMPLE + +Pressing =RET= on the header will take you to the dashboard associated with +that status check. If there's more than one status check here, you'll be +prompted to choose a check (e.g., Travis, Circle, CLA, ...). Of course, if +you expand the header to show the individual checks, =RET= on those will take +you straight to that check. + +*** TODO Your Dashboard +Check out =M-x magithub-dashboard= to view your notifications and issues +assigned to you + +** TODO 'Tricks' + +Most of Magithub is implemented in pure Elisp now, but there are a few +lingering goodies that haven't been ported (since their real logic is +non-trivial). These definitions are relegated to =magithub-issue-tricks.el=. + +Make sure to install [[https://hub.github.com][=hub=]] and add it to your ~exec-path~ if you intend to use +these functions. After installation, use =hub browse= from a directory with a +GitHub repository to force the program to authenticate -- this avoids some +weirdness on the Emacs side of things. + +* _ Copying +:PROPERTIES: +:COPYING: t +:END: + +#+BEGIN_QUOTE +Copyright (C) 2017-2018 Sean Allred <code@seanallred.com> + +You can redistribute this document 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 document 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. +#+END_QUOTE + +* _ :ignore: + +# IMPORTANT: Also update ORG_ARGS and ORG_EVAL in the Makefile. +# Local Variables: +# fill-column: 76 +# eval: (require 'ox-extra nil t) +# eval: (require 'ox-texinfo+ nil t) +# eval: (and (featurep 'ox-extra) (ox-extras-activate '(ignore-headlines))) +# indent-tabs-mode: nil +# org-src-preserve-indentation: nil +# End: diff --git a/magithub.texi b/magithub.texi new file mode 100644 index 0000000..86d0a42 --- /dev/null +++ b/magithub.texi @@ -0,0 +1,1011 @@ +\input texinfo @c -*- texinfo -*- +@c %**start of header +@setfilename magithub.info +@settitle Magithub -- Magit interfaces for GitHub +@documentencoding UTF-8 +@documentlanguage en +@c %**end of header + +@copying +@quotation +Copyright (C) 2017-2018 Sean Allred <code@@seanallred.com> + +You can redistribute this document 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 document 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. +@end quotation +@end copying + +@dircategory Emacs +@direntry +* Magithub: (magithub). Magit interfaces for GitHub. +@end direntry + +@finalout +@titlepage +@title Magithub -- Magit interfaces for GitHub +@subtitle for version 0.1.5 (0.1.5-106-ge4a004c+1) +@author Sean Allred +@page +@vskip 0pt plus 1filll +@insertcopying +@end titlepage + +@contents + +@ifnottex +@node Top +@top Magithub -- Magit interfaces for GitHub + +You may also be interested in @uref{https://github.com/vermiculus/magithub/tree/master/RelNotes, the most current release notes}. + +Magithub provides an integrated GitHub experience through Magit's familiar +interface. Just as Magit hopes to 'outsmart git', Magithub hopes to add +smarts to GitHub for performing common tasks. + +Happy hacking! + +@noindent +This manual is for Magithub version 0.1.5 (0.1.5-106-ge4a004c+1). + +@quotation +Copyright (C) 2017-2018 Sean Allred <code@@seanallred.com> + +You can redistribute this document 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 document 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. +@end quotation +@end ifnottex + +@menu +* Installation:: +* Introduction:: +* Status Buffer Integration:: +* Dispatch Popup:: +* 'Features':: +* Cloning:: +* Dashboard:: +* Creating Content:: +* Caching:: +* Proxies:: +* Unfiled:: + +@detailmenu +--- The Detailed Node Listing --- + +Installation + +* Authentication:: +* Enterprise Support:: + +Introduction + +* Note:: +* Brief Tutorial:: + +Brief Tutorial + +* Clone a repository:: +* Viewing project status:: +* Viewing and replying to an issue:: +* Creating an issue:: +* Creating a pull request:: + + +Status Buffer Integration + +* Project Status:: +* Open Issues and Pull Requests:: + +Open Issues and Pull Requests + +* Manipulating the Cache:: +* Offline Mode:: +* Controlling Sections:: + + +Dispatch Popup + +* Configuration:: +* Meta:: + +Unfiled + +* Content:: +* 'Tricks':: + +Content + +* Working with Repositories:: +* Your Dashboard:: + + +@end detailmenu +@end menu + +@node Installation +@chapter Installation + +Magithub can be installed from @uref{http://melpa.milkbox.net/#/magithub, MELPA} using @samp{M-x list-packages} or by +evaluating the following: + +@lisp +(package-install 'magithub) +@end lisp + +Here is the basic recommended @uref{https://github.com/jwiegley/use-package, @samp{use-package}} configuration: + +@lisp +(use-package magithub + :after magit + :ensure t + :config (magithub-feature-autoinject t)) +@end lisp + +If you prefer to install the package manually, this can of course be done +via the usual means. + +For more information, see @ref{Packages,,,emacs,}. + +@menu +* Authentication:: +* Enterprise Support:: +@end menu + +@node Authentication +@section Authentication + +Given GitHub's rate-limiting policy, Magithub is unlikely to ever support +running without authenticating. As such, you @emph{must} authenticate before you +use Magithub. (As of #107, Magithub will not even attempt go online until +you're properly authenticated.) + +To authenticate, you can simply start using Magithub; Ghub should walk you +through the authentication process unless you use two-factor authentication. +(Your token is stored in one of your @code{auth-sources}; see @uref{https://magit.vc/manual/ghub/How-Ghub-uses-Auth_002dSource.html#How-Ghub-uses-Auth_002dSource, Ghub's manual} for +details.) + +If you do use two-factor authentication, you must + +@itemize +@item +Manually create a GitHub token (from @uref{https://github.com/settings/tokens}) +for scopes `repo`, `notifications` and `user` (see variable +@code{magithub-github-token-scopes}) + +@item +Store it for Magithub per user in one of your @code{auth-sources} +(e.g. @samp{~/.authinfo}). Write a line like this: + +@example +machine api.github.com login YOUR_GITHUB_USERNAME^magithub password YOUR_GITHUB_TOKEN +@end example +@end itemize + +Beware that writing the token in plaintext in @samp{~/.authinfo} (or elsewhere) is +not secure against attackers with access to that file. For details and +better alternatives (like using GPG), see Ghub's manual on @uref{https://magit.vc/manual/ghub/Manually-Creating-and-Storing-a-Token.html#Manually-Creating-and-Storing-a-Token, Manually Creating +and Storing a Token} and @uref{https://magit.vc/manual/ghub/How-Ghub-uses-Auth_002dSource.html#How-Ghub-uses-Auth_002dSource, How Ghub uses Auth-Source}. + +If you want to authenticate Ghub without using Magithub, you can simply +evaluate the following: + +@lisp +(require 'magithub) +(ghub-get "/user" nil :auth 'magithub) +@end lisp + +After Ghub walks you through the authentication process during evaluation, +the @code{ghub-get} form should return familiar information (your login, email, +etc.). + +If you're having trouble @emph{authenticating}, @uref{https://github.com/magit/ghub/issues/new, open a Ghub issue} or drop by +@uref{https://gitter.im/vermiculus/magithub, Magithub's} or @uref{https://gitter.im/magit/magit, Magit's} Gitter channel. + +@node Enterprise Support +@section Enterprise Support + +For GitHub Enterprise support, you'll need to add your enterprise domain to +@code{magithub-github-hosts} so that Magithub can detect when it's in a GitHub +repository. You will also need to configure your @samp{~/.authinfo} file +appropriately to authenticate to your domain; see Ghub's manual for details. + +@node Introduction +@chapter Introduction + +Magithub tries to follow closely Magit's lead in general interface. Most of +its functionality is developed to tightly integrate with its section/ +framework. See @uref{https://magit.vc/manual/magit/Sections.html#Sections, Magit's documentation} for information on how to navigate +using this framework. + +Magithub's functionality uses section-specific keymaps to expose +functionality. Where it makes sense, the following keys will map to +functions that 'do the right thing': + +@table @asis +@kindex w +@cindex magithub-browse-thing +@item @kbd{w} @tie{}@tie{}@tie{}@tie{}(@code{magithub-browse-thing}) + +Open a browser to the thing at point. For instance, when point is on +issue 42 in your-favorite/github-repo, we'll open +@samp{http://github.com/your-favorite/github-repo/issue/42}. + +@kindex a +@cindex magithub-add-thing +@item @kbd{a} @tie{}@tie{}@tie{}@tie{}(@code{magithub-add-thing}) + +Add something to the thing at point. For instance, on a list of labels, +you can add more labels. + +@kindex e +@cindex magithub-edit-thing +@item @kbd{e} @tie{}@tie{}@tie{}@tie{}(@code{magithub-edit-thing}) + +Edit the thing at point, such as an issue. + +@kindex r +@cindex magithub-reply-thing +@item @kbd{r} @tie{}@tie{}@tie{}@tie{}(@code{magithub-reply-thing}) + +Reply to the thing at point, such as a comment. +@end table + +Magithub also considers the similar placeholder commands introduced by Magit +which you may already be familiar with: + +@table @asis +@kindex k +@cindex magit-delete-thing +@item @kbd{k} @tie{}@tie{}@tie{}@tie{}(@code{magit-delete-thing}) +@kindex RET +@cindex magit-visit-thing +@item @kbd{RET} @tie{}@tie{}@tie{}@tie{}(@code{magit-visit-thing}) +@end table + +These concepts are intended to provide a more consistent experience +throughout Magithub within Magit by categorizing your broader interactions +with all GitHub content. As with Magit, more commands are added as the +situation calls for it. + +@menu +* Note:: +* Brief Tutorial:: +@end menu + +@node Note +@section Note + +By default, Magithub enables itself in all repositories where @samp{origin} points +to GitHub. + +@defopt magithub-enabled-by-default + +When non-nil, Magithub is enabled by default. This is the fallback value +of git variable @samp{magithub.enabled} is not set in this repository. +@end defopt + +@defopt magithub-github-hosts + +A list of top-level domains that should be recognized as GitHub hosts. +@end defopt + +@node Brief Tutorial +@section Brief Tutorial + +Here's a script that will guide you through the major features of Magithub. +This is not a replacement for the documentation, but rather an example +workflow to whet your appetite. + +@menu +* Clone a repository:: +* Viewing project status:: +* Viewing and replying to an issue:: +* Creating an issue:: +* Creating a pull request:: +@end menu + +@node Clone a repository +@subsection Clone a repository + +@example +M-x magithub-clone RET vermiculus/my-new-repository +@end example +Cloning a repository this way gets the clone URL from GitHub and forwards +that on to @code{magit-clone}. If the repository is a fork, you're prompted to add +the parent is added under the @samp{upstream} remote. + +Fork behavior may change in the future. It may be more appropriate to +actually/ clone the source repository and add your remote as a fork. This +will cover the 90% case (the 10% case being active forks of unmaintained +projects). + +@node Viewing project status +@subsection Viewing project status + +You are dropped into a status buffer for @samp{vermiculus/my-new-repository}. You +see some open issues and pull requests. You move your cursor to an issue of +interest and @samp{TAB} to expand it, seeing the author, when it was +created/updated, any labels, and a preview of the issue contents. + +If @samp{vermiculus/my-new-repository} used any status checks, you would see those +statuses as a header in this buffer. + +@node Viewing and replying to an issue +@subsection Viewing and replying to an issue + +You @samp{RET} on the issue and are taken to a dedicated buffer for that issue. +You can now see its full contents as well as all comments. You'd like to +leave a comment -- a suggestion for a fix or an additional use-case to +consider -- you press @samp{r} to open a new buffer to @emph{reply} to this issue. You +write your comment and @samp{C-c C-c} to submit. But, oh no! You didn't turn on +@samp{flyspell-mode} in markdown buffers, so you submitted a spelling error. A +simple @samp{e} on the comment will @emph{edit} it. After submitting again with @samp{C-c C-c}, +everything is well. + +Right now, other activity on the issue is not inserted into this buffer. +Press @samp{w} to open the issue in your browser. + +@node Creating an issue +@subsection Creating an issue + +You notice a small issue in how some feature is implemented, so back in the +status buffer, you use @samp{H i} to create a new issue. (While inside the GitHub +repository, you could've used any key bound to @code{magithub-issue-new}.) The +first line is the title of the new issue; everything else is the body. You +submit the issue with @samp{C-c C-c}. + +You come back a little while later to leave additional details -- you reply +to your own issue in a comment, but realize you should just edit your +original issue to avoid confusion. You @samp{k} to @emph{kill} / delete the comment. + +@node Creating a pull request +@subsection Creating a pull request + +Since you care about this project and want to help it succeed, you decide to +fix this issue yourself. You checkout a new branch (@samp{b c my-feature RET}) and +get to work. + +Because you're so @emph{awesome}, you're ready to push your commit to fix your +issue. After realizing you don't have push permissions to this repository, +you create a fork using @samp{H f}. You push your branch to your new remote (named +after your username) and create a pull request with @samp{H p}. You select the +head branch as @samp{my-feature} and the base branch as @samp{master} (or whatever the +production/staging branch is for the project). You fill out the pull +request template provided by the project (and inserted into your PR) and off +you go! + +@node Status Buffer Integration +@chapter Status Buffer Integration + +The part of Magithub you're likely to interact with the most is +embedded right into Magit's status buffer. + +@table @asis +@kindex H +@cindex magithub-dispatch-popup +@item @kbd{H} @tie{}@tie{}@tie{}@tie{}(@code{magithub-dispatch-popup}) + +Access many Magithub entry-points. See @ref{Dispatch Popup} for more details. + +@kindex H e +@cindex FIXME +@item @kbd{H e} @tie{}@tie{}@tie{}@tie{}(@code{FIXME}) + +Toggle status buffer integration in this repository. +@end table + +There are two integrations turned on by default: + +@menu +* Project Status:: +* Open Issues and Pull Requests:: +@end menu + +@node Project Status +@section Project Status + +Many services (such as Travis CI and CircleCI) will post statuses to +commits. A summary of these statuses are visible in the status buffer +headers. + +@table @asis +@kindex RET +@cindex magithub-ci-visit +@item @kbd{RET} @tie{}@tie{}@tie{}@tie{}(@code{magithub-ci-visit}) +@kindex w +@cindex magithub-ci-visit +@item @kbd{w} @tie{}@tie{}@tie{}@tie{}(@code{magithub-ci-visit}) + +Visit the service's summary of this status. For example, a status posted +by Travis CI will open that build on Travis. + +@kindex g +@cindex magithub-ci-refresh +@item @kbd{g} @tie{}@tie{}@tie{}@tie{}(@code{magithub-ci-refresh}) + +Refresh statuses from GitHub and then refresh the current buffer. + +@kindex H s +@cindex FIXME +@item @kbd{H s} @tie{}@tie{}@tie{}@tie{}(@code{FIXME}) + +Enable/disable status checks in this repository. +@end table + +@node Open Issues and Pull Requests +@section Open Issues and Pull Requests + +These will also display in the status buffer. There's a lot of +functionality available right from an issue section. + +@table @asis +@kindex g +@cindex magithub-issue-refresh +@item @kbd{g} @tie{}@tie{}@tie{}@tie{}(@code{magithub-issue-refresh}) + +Refresh issues and pull requests from GitHub and then refresh the current +buffer. + +@kindex RET +@cindex magithub-issue-visit +@item @kbd{RET} @tie{}@tie{}@tie{}@tie{}(@code{magithub-issue-visit}) + +Open a new buffer to view an issue and its comments. + +@kindex w +@cindex magithub-issue-browse +@item @kbd{w} @tie{}@tie{}@tie{}@tie{}(@code{magithub-issue-browse}) +@kindex w +@cindex magithub-pull-browse +@item @kbd{w} @tie{}@tie{}@tie{}@tie{}(@code{magithub-pull-browse}) + +Browse this issue / pull request on GitHub. + +@kindex N +@cindex magithub-issue-personal-note +@item @kbd{N} @tie{}@tie{}@tie{}@tie{}(@code{magithub-issue-personal-note}) + +Opens a buffer for offline note-taking. + +@kindex L +@cindex magithub-issue-add-labels +@item @kbd{L} @tie{}@tie{}@tie{}@tie{}(@code{magithub-issue-add-labels}) + +Add labels to the issue. + +@kindex a +@cindex magithub-label-add +@item @kbd{a} @tie{}@tie{}@tie{}@tie{}(@code{magithub-label-add}) +@kindex k +@cindex magithub-label-remove +@item @kbd{k} @tie{}@tie{}@tie{}@tie{}(@code{magithub-label-remove}) + +When point is on a label section, you can add/remove labels (provided you +have permission to do so). + +@end table + +@cindex magithub-label-color-replace +@deffn Command magithub-label-color-replace + +Labels are colored as they would be on GitHub. In some themes, this +produces an illegible or otherwise undesirable color. This command can +help you find a substitute for labels of this color. +@end deffn + +@defvar magithub-issue-details-hook + +Control which issue details display in the status buffer. Functions +intended for this variable use the @samp{magithub-issue-detail-insert-*} prefix. + +Performance note: judicious use of this variable can improve your overall +Magit experience in large buffers. +@end defvar + +@defopt magithub-issue-issue-filter-functions +@end defopt +@defopt magithub-issue-pull-request-filter-functions + +These are lists of functions which must all return non-nil for an issue/PR +to be displayed in the status buffer. They all receive the issue/PR +object as their sole argument. For example, you might want to filter out +issues labels @samp{enhancement} from your list: + +@lisp +(setq magithub-issue-issue-filter-functions + (list (lambda (issue) ; don't show enhancement requests + (not + (member "enhancement" + (let-alist issue + (ghubp-get-in-all '(name) .labels))))))) +@end lisp +@end defopt + +@menu +* Manipulating the Cache:: +* Offline Mode:: +* Controlling Sections:: +@end menu + +@node Manipulating the Cache +@subsection Manipulating the Cache + +When point is on a Magithub-controlled section (like the status header): +@multitable {aaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@headitem Default Key +@tab Description +@item @samp{g} +@tab Refresh only this section's GitHub content +@item @samp{C-u g} +@tab Like @samp{g}, but works on the whole buffer +@end multitable + +@node Offline Mode +@subsection Offline Mode + +@multitable {aaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaa} +@headitem Default Key +@tab Description +@item @samp{H C c} +@tab Toggle offline mode +@end multitable + +Offline mode was introduced for those times when you're on the go, but you'd +still like to have an overview of GitHub data in your status buffer. It's +also useful for folks who want to explicitly control when Emacs communicates +with GitHub -- for this purpose, you can use @samp{C-u g} (discussed above) to pull +data from GitHub while in offline mode. + +To start into offline mode everywhere, use +@example +git config --global magithub.cache always +@end example + +See the documentation for function @code{magithub-settings--set-magithub.cache} +for details on appropriate values. + +@node Controlling Sections +@subsection Controlling Sections + +Sections like the issue list and the status header can be toggled with the +interactive functions of the form @samp{magithub-toggle-*}. These functions have +no default keybinding. + +Since status checks can be API-hungry and not all projects use them, you can +disable the status header at the repository-level with @samp{H ~}; see the Status +Checks section for more information. + +@node Dispatch Popup +@chapter Dispatch Popup + +Much of Magithub's functionality, including configuration options, is behind +this popup. In Magit status buffers, it's bound to @samp{H}. + +@table @asis +@kindex d +@cindex magithub-dashboard +@item @kbd{d} @tie{}@tie{}@tie{}@tie{}(@code{magithub-dashboard}) + +See @ref{Dashboard}. + +@kindex c +@cindex magithub-create +@item @kbd{c} @tie{}@tie{}@tie{}@tie{}(@code{magithub-create}) + +Push a local repository up to GitHub. + +@kindex H +@cindex magithub-browse +@item @kbd{H} @tie{}@tie{}@tie{}@tie{}(@code{magithub-browse}) + +Open the current repository in your browser. + +@kindex f +@cindex magithub-fork +@item @kbd{f} @tie{}@tie{}@tie{}@tie{}(@code{magithub-fork}) + +Fork this repository on GitHub. This will add your fork as a remote under +your username. For example, if user @samp{octocat} forked Magit, we would see a +new remote called @samp{octocat} pointing to @samp{octocat/magit}. + +@kindex i +@cindex magithub-issue-new +@item @kbd{i} @tie{}@tie{}@tie{}@tie{}(@code{magithub-issue-new}) +@kindex p +@cindex magithub-pull-request-new +@item @kbd{p} @tie{}@tie{}@tie{}@tie{}(@code{magithub-pull-request-new}) + +Open a new buffer to create an issue or open a pull request. See +@ref{Creating Content}. +@end table + +@menu +* Configuration:: +* Meta:: +@end menu + +@node Configuration +@section Configuration + +Per-repository configuration is controlled via git variables reachable from +the dispatch popup via @samp{H C}. Use @samp{? <key>} to get online help for each +variable in that popup. + +@table @asis +@kindex C e +@cindex FIXME +@item @kbd{C e} @tie{}@tie{}@tie{}@tie{}(@code{FIXME}) + +Turn Magithub on/off (completely). + +@kindex C s +@cindex FIXME +@item @kbd{C s} @tie{}@tie{}@tie{}@tie{}(@code{FIXME}) + +Turn the project status header on/off. + +@kindex C c +@cindex FIXME +@item @kbd{C c} @tie{}@tie{}@tie{}@tie{}(@code{FIXME}) + +Control whether Magithub is considered 'online'. This controls the +behavior of the the cache. This may go away in the future. See +Controlling the Cache for more details. FIXME there is no such node. + +@kindex C i +@cindex FIXME +@item @kbd{C i} @tie{}@tie{}@tie{}@tie{}(@code{FIXME}) + +Toggle the issues section. + +@kindex C p +@cindex FIXME +@item @kbd{C p} @tie{}@tie{}@tie{}@tie{}(@code{FIXME}) + +Toggle the pull requests section. + +@kindex C x +@cindex FIXME +@item @kbd{C x} @tie{}@tie{}@tie{}@tie{}(@code{FIXME}) + +Set the 'proxy' used for this repository. See @ref{Proxies}. +@end table + +@node Meta +@section Meta + +Since Magithub is so integrated with Magit, there's often confusion about +whom to ask for support (especially for users of preconfigured Emacsen like +Spacemacs and Prelude). Hopefully, these functions can direct you to the +appropriate spot. + +@table @asis +@kindex & +@cindex magithub--meta-new-issue +@item @kbd{&} @tie{}@tie{}@tie{}@tie{}(@code{magithub--meta-new-issue}) + +Open the browser to create a new issue for Magithub functionality +described in this document. + +@kindex h +@cindex magithub--meta-help +@item @kbd{h} @tie{}@tie{}@tie{}@tie{}(@code{magithub--meta-help}) + +Open the browser to ask for help on Gitter, a GitHub-focused chatroom. +@end table + +@node 'Features' +@chapter 'Features' + +Given that some features of Magithub are not desired by or appropriate for +every type of user, there are features that are not turned on by default. +These are features that are injected into standard Magit popups. + +The list of available features is available in constant +@code{magithub-feature-list}. Despite its name, this is an alist of symbols (i.e., +'features') to functions that install the feature. While the documentation +for each feature lives in that symbol, you would normally not otherwise +interact with it. + +@defun magithub-feature-autoinject + +This function is the expected interface to install features. You will +normally use +@lisp +(magithub-feature-autoinject t) +@end lisp +in your configuration to install all features, but you have the option of +installing them one at a time using the symbols from constant +@code{magithub-feature-list} or as a list of those symbols: +@lisp +(magithub-feature-autoinject 'commit-browse) +(magithub-feature-autoinject '(commit-browse pull-request-merge)) +@end lisp +@end defun + +@node Cloning +@chapter Cloning + +@cindex magithub-clone +@deffn Command magithub-clone + +Clone a repository from GitHub. +@end deffn + +@defopt magithub-clone-default-directory + +The default destination directory to use for cloning. +@end defopt + +@defopt magithub-preferred-remote-method + +This option is a symbol indicating the preferred cloning method (between +HTTPS, SSH, and the @samp{git://} protocol). +@end defopt + +@node Dashboard +@chapter Dashboard + +The dashboard shows you information pertaining to @emph{you}: +@itemize +@item +notifications + +@item +issues and pull requests you're assigned per repository +@end itemize +as well as contextual information like the logged-in user and @uref{https://developer.github.com/v3/#rate-limiting, rate-limiting} +information. + +@cindex magithub-dashboard +@deffn Command magithub-dashboard + +View your dashboard. +@end deffn + +@table @asis +@kindex ; +@cindex magithub-dashboard-popup +@item @kbd{;} @tie{}@tie{}@tie{}@tie{}(@code{magithub-dashboard-popup}) + +Configure your global dashboard settings. + +@end table + +@defopt magithub-dashboard-show-read-notifications + +When non-nil, we'll show read notifications in the dashboard. +@end defopt + +@node Creating Content +@chapter Creating Content + +It's great to read about what's been happening, but it's even better to +contribute your own thoughts and activity! + +@table @asis +@kindex H i +@cindex magithub-issue-new +@item @kbd{H i} @tie{}@tie{}@tie{}@tie{}(@code{magithub-issue-new}) +@kindex H p +@cindex magithub-pull-request-new +@item @kbd{H p} @tie{}@tie{}@tie{}@tie{}(@code{magithub-pull-request-new}) + +Create issues and pull requests. If you have push access to the +repository, you'll have the opportunity to add labels before you submit +the issue. + +Creating a pull request requires a HEAD branch, a BASE branch, and to know +which remote points to your fork. + +@kindex r +@cindex magithub-comment-new +@item @kbd{r} @tie{}@tie{}@tie{}@tie{}(@code{magithub-comment-new}) +@kindex r +@cindex magithub-comment-reply +@item @kbd{r} @tie{}@tie{}@tie{}@tie{}(@code{magithub-comment-reply}) + +On an issue or pull request section, @code{magithub-comment-new} will allow you +to post a comment to that issue/PR. If point is already on a comment, +@code{magithub-comment-reply} will quote the comment at point for you. +@end table + +@node Caching +@chapter Caching + +Caching is a complicated topic with a long Magithub history of, well, +failure. As of today, all data retrieved from the API is cached by +default. Using @samp{g} on Magithub sections will usually refresh the information +in the buffer pertaining to that section. Otherwise, @samp{C-u g} in any Magit +buffer will refresh all GitHub data in that buffer. + +This behavior may change in the future, but for now, it's the most stable +option. See + +@node Proxies +@chapter Proxies + +It's not uncommon to have repositories where the bug-tracker is in a +separate repository. For these cases, you can use the idea of 'proxies'. A +proxy is a remote (with a GitHub-associated URL) that you choose to use for +all GitHub API requests concerning the @emph{actual} current repository. This is +manifest in the git variable @samp{magithub.proxy}. + +@defun magithub-proxy-set-default + +If you consistently use a specific remote name for the bug tracker, you +can set it globally. +@end defun + +All GitHub requests specific to the current repository context are routed +through @code{magithub-repo} which respects this proxy. + +@node Unfiled +@chapter Unfiled + +@menu +* Content:: +* 'Tricks':: +@end menu + +@node Content +@section Content + +@menu +* Working with Repositories:: +* Your Dashboard:: +@end menu + +@node Working with Repositories +@subsection Working with Repositories + +@menu +* General:: +* Issues:: +* Forking and Pull Requests:: +* Labels:: +* Status Checks:: +@end menu + +@node General +@subsubsection @strong{DONE} General + +@multitable {aaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@headitem Default Key +@tab Description +@item @samp{H H} +@tab Opens the current repository in the browser +@item @samp{H c} +@tab Creates the current local repository on GitHub +@item @samp{M-x magithub-clone} +@tab Clone a repository +@end multitable + +@samp{magithub-clone} may appear to be a thin wrapper over @samp{magit-clone}, but it's +quite a bit smarter than that. We'll of course respect +@samp{magithub-preferred-remote-method} when cloning the repository, but we can +also detect when the repository is a fork and can create and set an upstream +remote accordingly (similar to @samp{M-x magithub-fork}). + +@node Issues +@subsubsection @strong{DONE} Issues + +@multitable {aaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaa} +@headitem Default Key +@tab Description +@item @samp{H i} +@tab Create a new issue +@item @samp{RET} +@tab Open the issue in GitHub +@end multitable + +You can filter issues with @samp{magithub-issue-issue-filter-functions}: +@lisp +(setq magithub-issue-issue-filter-functions + (list (lambda (issue) ; don't show enhancement requests + (not + (member "enhancement" + (let-alist issue + (ghubp-get-in-all '(name) .labels))))))) +@end lisp +Each function in the @samp{*-functions} list must return non-nil for the issue to +appear in the issue list. See also the documentation for that variable. + +@node Forking and Pull Requests +@subsubsection @strong{DONE} Forking and Pull Requests + +@multitable {aaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@headitem Default Key +@tab Description +@item @samp{H f} +@tab Fork the current repository +@item @samp{H p} +@tab Submit pull requests upstream +@end multitable + +You can also filter pull requests with +@samp{magithub-issue-pull-request-filter-functions}. See the section on +issue-filtering for an example. + +@node Labels +@subsubsection @strong{TODO} Labels + +@multitable {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@headitem Default Key +@tab Description +@item @samp{M-x magithub-label-color-replace} +@tab Choose a new color for the label at point +@end multitable + +By default, Magithub will adopt the color used by GitHub when showing +labels. In some themes, this doesn't provide enough contrast. Use @samp{M-x +magithub-label-color-replace} to replace the current label's color with +another one. (This will apply to all labels in all repositories, but will +of course not apply to all @emph{shades} of the original color.) + +@node Status Checks +@subsubsection @strong{TODO} Status Checks + +@multitable {aaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@headitem Default Key +@tab Description +@item @samp{RET} +@tab Visit the status's dashboard in your browser +@item @samp{TAB} +@tab On the status header, show individual CI details +@item @samp{H ~} +@tab Toggle status integration for this repository +@end multitable + +When the status buffer first opens, the status header is inserted at the top +and probably looks something like this: +@example +Status: Success +@end example + +You can get a breakdown of which checks succeeded and which failed by using +@samp{TAB}: +@example +Status: Success + Checks for ref: develop + Success The Travis CI build passed continuous-integration/travis-ci/push +@end example + +Pressing @samp{RET} on the header will take you to the dashboard associated with +that status check. If there's more than one status check here, you'll be +prompted to choose a check (e.g., Travis, Circle, CLA, @dots{}). Of course, if +you expand the header to show the individual checks, @samp{RET} on those will take +you straight to that check. + +@node Your Dashboard +@subsection @strong{TODO} Your Dashboard + +Check out @samp{M-x magithub-dashboard} to view your notifications and issues +assigned to you + +@node 'Tricks' +@section @strong{TODO} 'Tricks' + +Most of Magithub is implemented in pure Elisp now, but there are a few +lingering goodies that haven't been ported (since their real logic is +non-trivial). These definitions are relegated to @samp{magithub-issue-tricks.el}. + +Make sure to install @uref{https://hub.github.com, @samp{hub}} and add it to your @code{exec-path} if you intend to use +these functions. After installation, use @samp{hub browse} from a directory with a +GitHub repository to force the program to authenticate -- this avoids some +weirdness on the Emacs side of things. + +@bye diff --git a/screenshots.md b/screenshots.md new file mode 100644 index 0000000..0e73b1f --- /dev/null +++ b/screenshots.md @@ -0,0 +1,28 @@ +# Screenshots + +This file indexes screenshots and captured workflows from various +stages in development. Some features may have been added or changed +than what appears here, although I hope to keep it up-to-date! + +## Creating a Repository + +![Creating](images/create.gif) + +## Submitting a Pull Request + +![submitting a Pull Request](images/pull-request.gif) + +## CI Status + +![CI Pending](images/ci-pending.png) + +![CI Failure](images/ci-failure.png) + +![CI Success](images/ci-success.png) + +--- + +![Dispatch](images/scr1.png)|![Creating](images/scr2.png) +:-------------------------:|:-------------------------: +![Forking](images/scr3.png)|![Pushing](images/scr4.png) +![Issues and Pull Requests](images/scr5.png)| diff --git a/test/magithub-test.el b/test/magithub-test.el new file mode 100644 index 0000000..2e1c190 --- /dev/null +++ b/test/magithub-test.el @@ -0,0 +1,49 @@ +;;; magithub-tests.el --- tests for Magithub + +;; Copyright (C) 2016-2018 Sean Allred +;; +;; License: GPLv3 + +;;; Code: + +(require 'ert) +(require 'magithub-core) +(require 'ghub+) + +(load "test-helper.el") + +(setq ghubp-request-override-function + #'magithub-mock-ghub-request) + +(defmacro magithub-test-cache-with-new-cache (plist &rest body) + (declare (indent 1)) + `(let ((magithub-cache-class-refresh-seconds-alist ',plist) + (magithub-cache--cache (make-hash-table))) + ,@body)) + +(ert-deftest magithub-test-cache () + (magithub-test-cache-with-new-cache ((:test . 30)) + (should (equal t (magithub-cache :test t))))) + +(ert-deftest magithub-test-origin-parse () + "Tests issue #105." + (let ((repo '((owner (login . "vermiculus")) + (name . "magithub")))) + (should (equal repo (magithub--url->repo "git@github.com:vermiculus/magithub.git"))) + (should (equal repo (magithub--url->repo "git@github.com:vermiculus/magithub"))) + (should (equal repo (magithub--url->repo "git+ssh://github.com/vermiculus/magithub"))) + (should (equal repo (magithub--url->repo "ssh://git@github.com/vermiculus/magithub"))))) + +(ert-deftest magithub-test-source-repo () + "Test basic API functionality. +This tests everything from checking API availability to +determining that we're in a GitHub repository to actually making +cached API calls." + (let ((magithub--api-last-checked (current-time))) + (should (magithub-source--sparse-repo)) + (should (magithub-repo)) + (should (let ((magithub-cache--refresh t)) ; force API call + (magithub-repo))) + (should (magithub-repo)))) ; force cache read + +;;; magithub-test.el ends here diff --git a/test/mock-data/get/repos.d/vermiculus.d/magithub.81a9dfc7 b/test/mock-data/get/repos.d/vermiculus.d/magithub.81a9dfc7 new file mode 100644 index 0000000..e438e73 --- /dev/null +++ b/test/mock-data/get/repos.d/vermiculus.d/magithub.81a9dfc7 @@ -0,0 +1,95 @@ +((id . 68352724) + (name . "magithub") + (full_name . "vermiculus/magithub") + (owner + (login . "vermiculus") + (id . 2082195) + (avatar_url . "https://avatars3.githubusercontent.com/u/2082195?v=4") + (gravatar_id . "") + (url . "https://api.github.com/users/vermiculus") + (html_url . "https://github.com/vermiculus") + (followers_url . "https://api.github.com/users/vermiculus/followers") + (following_url . "https://api.github.com/users/vermiculus/following{/other_user}") + (gists_url . "https://api.github.com/users/vermiculus/gists{/gist_id}") + (starred_url . "https://api.github.com/users/vermiculus/starred{/owner}{/repo}") + (subscriptions_url . "https://api.github.com/users/vermiculus/subscriptions") + (organizations_url . "https://api.github.com/users/vermiculus/orgs") + (repos_url . "https://api.github.com/users/vermiculus/repos") + (events_url . "https://api.github.com/users/vermiculus/events{/privacy}") + (received_events_url . "https://api.github.com/users/vermiculus/received_events") + (type . "User") + (site_admin)) + (private) + (html_url . "https://github.com/vermiculus/magithub") + (description . "Magit interfaces for GitHub") + (fork) + (url . "https://api.github.com/repos/vermiculus/magithub") + (forks_url . "https://api.github.com/repos/vermiculus/magithub/forks") + (keys_url . "https://api.github.com/repos/vermiculus/magithub/keys{/key_id}") + (collaborators_url . "https://api.github.com/repos/vermiculus/magithub/collaborators{/collaborator}") + (teams_url . "https://api.github.com/repos/vermiculus/magithub/teams") + (hooks_url . "https://api.github.com/repos/vermiculus/magithub/hooks") + (issue_events_url . "https://api.github.com/repos/vermiculus/magithub/issues/events{/number}") + (events_url . "https://api.github.com/repos/vermiculus/magithub/events") + (assignees_url . "https://api.github.com/repos/vermiculus/magithub/assignees{/user}") + (branches_url . "https://api.github.com/repos/vermiculus/magithub/branches{/branch}") + (tags_url . "https://api.github.com/repos/vermiculus/magithub/tags") + (blobs_url . "https://api.github.com/repos/vermiculus/magithub/git/blobs{/sha}") + (git_tags_url . "https://api.github.com/repos/vermiculus/magithub/git/tags{/sha}") + (git_refs_url . "https://api.github.com/repos/vermiculus/magithub/git/refs{/sha}") + (trees_url . "https://api.github.com/repos/vermiculus/magithub/git/trees{/sha}") + (statuses_url . "https://api.github.com/repos/vermiculus/magithub/statuses/{sha}") + (languages_url . "https://api.github.com/repos/vermiculus/magithub/languages") + (stargazers_url . "https://api.github.com/repos/vermiculus/magithub/stargazers") + (contributors_url . "https://api.github.com/repos/vermiculus/magithub/contributors") + (subscribers_url . "https://api.github.com/repos/vermiculus/magithub/subscribers") + (subscription_url . "https://api.github.com/repos/vermiculus/magithub/subscription") + (commits_url . "https://api.github.com/repos/vermiculus/magithub/commits{/sha}") + (git_commits_url . "https://api.github.com/repos/vermiculus/magithub/git/commits{/sha}") + (comments_url . "https://api.github.com/repos/vermiculus/magithub/comments{/number}") + (issue_comment_url . "https://api.github.com/repos/vermiculus/magithub/issues/comments{/number}") + (contents_url . "https://api.github.com/repos/vermiculus/magithub/contents/{+path}") + (compare_url . "https://api.github.com/repos/vermiculus/magithub/compare/{base}...{head}") + (merges_url . "https://api.github.com/repos/vermiculus/magithub/merges") + (archive_url . "https://api.github.com/repos/vermiculus/magithub/{archive_format}{/ref}") + (downloads_url . "https://api.github.com/repos/vermiculus/magithub/downloads") + (issues_url . "https://api.github.com/repos/vermiculus/magithub/issues{/number}") + (pulls_url . "https://api.github.com/repos/vermiculus/magithub/pulls{/number}") + (milestones_url . "https://api.github.com/repos/vermiculus/magithub/milestones{/number}") + (notifications_url . "https://api.github.com/repos/vermiculus/magithub/notifications{?since,all,participating}") + (labels_url . "https://api.github.com/repos/vermiculus/magithub/labels{/name}") + (releases_url . "https://api.github.com/repos/vermiculus/magithub/releases{/id}") + (deployments_url . "https://api.github.com/repos/vermiculus/magithub/deployments") + (created_at . "2016-09-16T04:32:34Z") + (updated_at . "2017-09-29T11:26:51Z") + (pushed_at . "2017-09-18T20:06:43Z") + (git_url . "git://github.com/vermiculus/magithub.git") + (ssh_url . "git@github.com:vermiculus/magithub.git") + (clone_url . "https://github.com/vermiculus/magithub.git") + (svn_url . "https://github.com/vermiculus/magithub") + (homepage . "") + (size . 1841) + (stargazers_count . 290) + (watchers_count . 290) + (language . "Emacs Lisp") + (has_issues . t) + (has_projects . t) + (has_downloads . t) + (has_wiki . t) + (has_pages) + (forks_count . 27) + (mirror_url) + (open_issues_count . 35) + (forks . 27) + (open_issues . 35) + (watchers . 290) + (default_branch . "master") + (permissions + (admin . t) + (push . t) + (pull . t)) + (allow_squash_merge . t) + (allow_merge_commit . t) + (allow_rebase_merge . t) + (network_count . 27) + (subscribers_count . 15)) diff --git a/test/test-helper.el b/test/test-helper.el new file mode 100644 index 0000000..52a5cbd --- /dev/null +++ b/test/test-helper.el @@ -0,0 +1,69 @@ +;;; Allow loading package files +(require 'cl-lib) + +(defun magithub-in-test-dir (file) + "Expand FILE in the test directory." + (let ((dir default-directory)) + (while (and (not (string= dir "/")) + (not (file-exists-p (expand-file-name ".git" dir)))) + (setq dir (file-name-directory (directory-file-name dir)))) + (when (string= dir "/") + (error "Project root not found")) + (setq dir (expand-file-name "test" dir)) + (expand-file-name file dir))) + +(defun magithub-mock-data-crunch (data) + "Crunch DATA into a string appropriate for a filename." + (substring (sha1 (prin1-to-string data)) 0 8)) + +(cl-defun magithub-mock-ghub-request (method resource &optional params + &key query payload headers unpaginate + noerror reader username auth host) + "Mock a call to the GitHub API. + +If the call has not been mocked and the AUTOTEST environment +variable is not set, offer to save a snapshot of the real API's +response." + (message "(mock-ghub-request %S %S %S :query %S :payload %S :headers %S :unpaginate %S :noerror %S :reader %S :username %S :auth %S :host %S)" + method resource params query payload headers unpaginate noerror reader username auth host) + (when (not (magithub-online-p)) + (error "Did not respect online/offline")) + (let* ((parts (cdr (s-split "/" resource))) + (directory (mapconcat (lambda (s) (concat s ".d")) + (butlast parts) "/")) + (filename (magithub-in-test-dir + (format "mock-data/%s/%s/%s.%s" + (downcase method) + directory + (car (last parts)) + (magithub-mock-data-crunch + (list method resource params query payload headers + unpaginate noerror reader username auth host)))))) + (if (file-readable-p filename) + (prog1 (with-temp-buffer + (insert-file-contents-literally filename) + (read (current-buffer))) + (message "Found %S" filename)) + (message "Did not find %S" filename) + (if (and (not (getenv "AUTOTEST")) + (y-or-n-p (format "Request not mocked; mock now?"))) + (progn + (make-directory directory t) + (let ((real-data (ghub-request method resource params + :query query + :payload payload + :headers headers + :unpaginate unpaginate + :noerror noerror + :reader reader + :username username + :auth auth + :host host))) + (pp-display-expression real-data "*GitHub API Response*") + (if (y-or-n-p "API response displayed; is this ok?") + (with-temp-buffer + (insert (pp-to-string real-data)) + (write-file filename) + (message "Wrote %s" filename)) + (error "API response rejected")))) + (error "Unmocked test!"))))) |