summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatteo F. Vescovi <mfv@debian.org>2018-06-11 19:23:49 -0300
committerMatteo F. Vescovi <mfv@debian.org>2018-06-11 19:23:49 -0300
commit7dd1e35e345b4a318efcb14a25a8f5e3fdd0bd9e (patch)
treea9a20af2b86de431bfb9a1fefb82545c298d23a3
Import magithub_0.1.7.orig.tar.xz
[dgit import orig magithub_0.1.7.orig.tar.xz]
-rw-r--r--.github/ISSUE_TEMPLATE.md7
-rw-r--r--.github/PULL_REQUEST_TEMPLATE.md9
-rw-r--r--.gitignore9
-rw-r--r--.travis.yml35
-rw-r--r--CONTRIBUTING.md57
-rw-r--r--Cask11
-rw-r--r--LICENSE675
-rw-r--r--Makefile112
-rw-r--r--README.md80
-rw-r--r--RelNotes/0.1.6.org144
-rw-r--r--RelNotes/0.2.org5
-rw-r--r--images/ci-failure.pngbin0 -> 26714 bytes
-rw-r--r--images/ci-pending.pngbin0 -> 28482 bytes
-rw-r--r--images/ci-success.pngbin0 -> 35618 bytes
-rw-r--r--images/create.gifbin0 -> 381545 bytes
-rw-r--r--images/pull-request.gifbin0 -> 232907 bytes
-rw-r--r--images/scr1.pngbin0 -> 77750 bytes
-rw-r--r--images/scr2.pngbin0 -> 98526 bytes
-rw-r--r--images/scr3.pngbin0 -> 85937 bytes
-rw-r--r--images/scr4.pngbin0 -> 96013 bytes
-rw-r--r--images/scr5.pngbin0 -> 138421 bytes
-rw-r--r--images/status.pngbin0 -> 301110 bytes
-rw-r--r--magithub-ci.el270
-rw-r--r--magithub-comment.el261
-rw-r--r--magithub-completion.el102
-rw-r--r--magithub-core.el1253
-rw-r--r--magithub-dash.el219
-rw-r--r--magithub-edit-mode.el208
-rw-r--r--magithub-faces.el109
-rw-r--r--magithub-issue-post.el223
-rw-r--r--magithub-issue-tricks.el56
-rw-r--r--magithub-issue-view.el177
-rw-r--r--magithub-issue.el602
-rw-r--r--magithub-label.el176
-rw-r--r--magithub-notification.el170
-rw-r--r--magithub-orgs.el36
-rw-r--r--magithub-repo.el51
-rw-r--r--magithub-settings.el380
-rw-r--r--magithub-user.el149
-rw-r--r--magithub.el294
-rw-r--r--magithub.org708
-rw-r--r--magithub.texi1011
-rw-r--r--screenshots.md28
-rw-r--r--test/magithub-test.el49
-rw-r--r--test/mock-data/get/repos.d/vermiculus.d/magithub.81a9dfc795
-rw-r--r--test/test-helper.el69
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
diff --git a/Cask b/Cask
new file mode 100644
index 0000000..453402c
--- /dev/null
+++ b/Cask
@@ -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"))
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f276855
--- /dev/null
+++ b/LICENSE
@@ -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
new file mode 100644
index 0000000..c1d556b
--- /dev/null
+++ b/images/ci-failure.png
Binary files differ
diff --git a/images/ci-pending.png b/images/ci-pending.png
new file mode 100644
index 0000000..f35377f
--- /dev/null
+++ b/images/ci-pending.png
Binary files differ
diff --git a/images/ci-success.png b/images/ci-success.png
new file mode 100644
index 0000000..1198eb7
--- /dev/null
+++ b/images/ci-success.png
Binary files differ
diff --git a/images/create.gif b/images/create.gif
new file mode 100644
index 0000000..bebb010
--- /dev/null
+++ b/images/create.gif
Binary files differ
diff --git a/images/pull-request.gif b/images/pull-request.gif
new file mode 100644
index 0000000..2ee912e
--- /dev/null
+++ b/images/pull-request.gif
Binary files differ
diff --git a/images/scr1.png b/images/scr1.png
new file mode 100644
index 0000000..090dd2a
--- /dev/null
+++ b/images/scr1.png
Binary files differ
diff --git a/images/scr2.png b/images/scr2.png
new file mode 100644
index 0000000..edaf721
--- /dev/null
+++ b/images/scr2.png
Binary files differ
diff --git a/images/scr3.png b/images/scr3.png
new file mode 100644
index 0000000..34b59bc
--- /dev/null
+++ b/images/scr3.png
Binary files differ
diff --git a/images/scr4.png b/images/scr4.png
new file mode 100644
index 0000000..554caad
--- /dev/null
+++ b/images/scr4.png
Binary files differ
diff --git a/images/scr5.png b/images/scr5.png
new file mode 100644
index 0000000..794dc23
--- /dev/null
+++ b/images/scr5.png
Binary files differ
diff --git a/images/status.png b/images/status.png
new file mode 100644
index 0000000..b876622
--- /dev/null
+++ b/images/status.png
Binary files differ
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!")))))